diff --git a/src/data/ability.ts b/src/data/ability.ts index d224791d7c9..daaa82baf68 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -574,6 +574,24 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr { } } +/** + * Reduces the accuracy of status moves used against the Pokémon with this ability to 50%. + * Used by Wonder Skin. + * + * @extends PreDefendAbAttr + */ +export class WonderSkinAbAttr extends PreDefendAbAttr { + applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { + const moveAccuracy = args[0] as Utils.NumberHolder; + if (move.category === MoveCategory.STATUS && moveAccuracy.value >= 50) { + moveAccuracy.value = 50; + return true; + } + + return false; + } +} + export class MoveImmunityStatChangeAbAttr extends MoveImmunityAbAttr { private stat: BattleStat; private levels: integer; @@ -4250,8 +4268,8 @@ export function initAbilities() { .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)), new Ability(Abilities.WONDER_SKIN, 5) - .ignorable() - .unimplemented(), + .attr(WonderSkinAbAttr) + .ignorable(), new Ability(Abilities.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user, target, move) => !!target.getLastXMoves(1).find(m => m.turn === target.scene.currentBattle.turn) || user.scene.currentBattle.turnCommands[target.getBattlerIndex()].command !== Command.FIGHT, 1.3), new Ability(Abilities.ILLUSION, 5) diff --git a/src/phases.ts b/src/phases.ts index e976b133fea..011b0e07f54 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -26,7 +26,7 @@ import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -3012,6 +3012,7 @@ export class MoveEffectPhase extends PokemonPhase { const moveAccuracy = new Utils.NumberHolder(this.move.getMove().accuracy); applyMoveAttrs(VariableAccuracyAttr, user, target, this.move.getMove(), moveAccuracy); + applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, this.move.getMove(), { value: false }, moveAccuracy); if (moveAccuracy.value === -1) { return true; diff --git a/src/test/abilities/wonder_skin.test.ts b/src/test/abilities/wonder_skin.test.ts new file mode 100644 index 00000000000..b5572bb4117 --- /dev/null +++ b/src/test/abilities/wonder_skin.test.ts @@ -0,0 +1,138 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { TurnEndPhase, } from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; +import Move, { allMoves } from "#app/data/move.js"; +import { MoveAbilityBypassAbAttr, WonderSkinAbAttr } from "#app/data/ability.js"; +import { NumberHolder } from "#app/utils.js"; +import Pokemon from "#app/field/pokemon.js"; + +describe("Abilities - Wonder Skin", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.WONDER_SKIN); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.CHARM]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + }); + + it("lowers accuracy of status moves to 50%", async () => { + await game.startBattle([Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + + const appliedAccuracy = getAppliedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedAccuracy).not.toBe(undefined); + expect(appliedAccuracy).not.toBe(100); + expect(appliedAccuracy).toBe(50); + }); + + it("does not lower accuracy of non-status moves", async () => { + await game.startBattle([Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); + + const appliedAccuracy = getAppliedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.TACKLE]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedAccuracy).not.toBe(undefined); + expect(appliedAccuracy).toBe(100); + expect(appliedAccuracy).not.toBe(50); + }); + + it("does not affect pokemon with Mold Breaker", async () => { + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.MOLD_BREAKER); + + await game.startBattle([Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + + const appliedAccuracy = getAppliedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedAccuracy).not.toBe(undefined); + expect(appliedAccuracy).toBe(100); + expect(appliedAccuracy).not.toBe(50); + }); + + it("does not affect pokemon with Teravolt", async () => { + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TERAVOLT); + + await game.startBattle([Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + + const appliedAccuracy = getAppliedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedAccuracy).not.toBe(undefined); + expect(appliedAccuracy).toBe(100); + expect(appliedAccuracy).not.toBe(50); + }); + + it("does not affect pokemon with Turboblaze", async () => { + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TURBOBLAZE); + + await game.startBattle([Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + + const appliedAccuracy = getAppliedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedAccuracy).not.toBe(undefined); + expect(appliedAccuracy).toBe(100); + expect(appliedAccuracy).not.toBe(50); + }); +}); + +/** + * Calculates the adjusted applied accuracy of a move. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used by the attacker. + * @returns The adjusted accuracy of the move. + */ +const getAppliedMoveAccuracy = (defender: Pokemon, attacker: Pokemon, move: Move) => { + const accuracyHolder = new NumberHolder(move.accuracy); + + /** + * Simulate ignoring ability + * @see MoveAbilityBypassAbAttr + */ + if (attacker.hasAbilityWithAttr(MoveAbilityBypassAbAttr)) { + return accuracyHolder.value; + } + + const wonderSkinInstance = new WonderSkinAbAttr(); + + wonderSkinInstance.applyPreDefend(defender, false, attacker, move, { value: false }, [ accuracyHolder ]); + + return accuracyHolder.value; +};