From f298ec3111962870179b3f2077574900e108043d Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:14:42 +0800 Subject: [PATCH] [Enhancement] Decouple move accuracy and accuracy multiplier calculation from phases.ts (#2899) * refactor accuracy calc * update doc * move accuracy multiplier calculation outside phases * update wonder skin unit test * rename method * add docs * add unit tests * address feedback * rename method * fix imports * improve tests * add test for ohko move accuracy --- src/data/move.ts | 42 ++++++++- src/field/pokemon.ts | 39 +++++++- src/phases.ts | 53 ++--------- src/test/abilities/wonder_skin.test.ts | 122 ++++++------------------- src/test/arena/arena_gravity.test.ts | 83 +++++++++++++++++ src/test/arena/weather_fog.test.ts | 51 +++++++++++ src/test/moves/double_team.test.ts | 55 +++++++++++ 7 files changed, 303 insertions(+), 142 deletions(-) create mode 100644 src/test/arena/arena_gravity.test.ts create mode 100644 src/test/arena/weather_fog.test.ts create mode 100644 src/test/moves/double_team.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index 4a77d89f9f1..caed4b4a496 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -10,9 +10,9 @@ import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; +import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; import { allAbilities } from "./ability"; -import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; +import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; import { Stat } from "./pokemon-stat"; import { TerrainType } from "./terrain"; @@ -656,6 +656,44 @@ export default class Move implements Localizable { return score; } + /** + * Calculates the accuracy of a move in battle based on various conditions and attributes. + * + * @param user {@linkcode Pokemon} The Pokémon using the move. + * @param target {@linkcode Pokemon} The Pokémon being targeted by the move. + * @returns The calculated accuracy of the move. + */ + calculateBattleAccuracy(user: Pokemon, target: Pokemon) { + const moveAccuracy = new Utils.NumberHolder(this.accuracy); + + applyMoveAttrs(VariableAccuracyAttr, user, target, this, moveAccuracy); + applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, this, { value: false }, moveAccuracy); + + if (moveAccuracy.value === -1) { + return moveAccuracy.value; + } + + const isOhko = this.hasAttr(OneHitKOAccuracyAttr); + + if (!isOhko) { + user.scene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); + } + + if (user.scene.arena.weather?.weatherType === WeatherType.FOG) { + /** + * The 0.9 multiplier is PokeRogue-only implementation, Bulbapedia uses 3/5 + * See Fog {@link https://bulbapedia.bulbagarden.net/wiki/Fog} + */ + moveAccuracy.value = Math.floor(moveAccuracy.value * 0.9); + } + + if (!isOhko && user.scene.arena.getTag(ArenaTagType.GRAVITY)) { + moveAccuracy.value = Math.floor(moveAccuracy.value * 1.67); + } + + return moveAccuracy.value; + } + /** * Calculates the power of a move in battle based on various conditions and attributes. * diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ecda7602569..894eac1654c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr } from "../data/ability"; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1738,6 +1738,43 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1]; } + /** + * Calculates the accuracy multiplier of the user against a target. + * + * This method considers various factors such as the user's accuracy level, the target's evasion level, + * abilities, and modifiers to compute the final accuracy multiplier. + * + * @param target {@linkcode Pokemon} - The target Pokémon against which the move is used. + * @param sourceMove {@linkcode Move} - The move being used by the user. + * @returns The calculated accuracy multiplier. + */ + getAccuracyMultiplier(target: Pokemon, sourceMove: Move): number { + const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]); + const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); + + applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccuracyLevel); + applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, targetEvasionLevel); + applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, targetEvasionLevel); + applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel); + this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel); + + const accuracyMultiplier = new Utils.NumberHolder(1); + if (userAccuracyLevel.value !== targetEvasionLevel.value) { + accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value + ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 + : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); + } + + applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, sourceMove); + + const evasionMultiplier = new Utils.NumberHolder(1); + applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); + + accuracyMultiplier.value /= evasionMultiplier.value; + + return accuracyMultiplier.value; + } + apply(source: Pokemon, move: Move): HitResult { let result: HitResult; const damage = new Utils.NumberHolder(0); diff --git a/src/phases.ts b/src/phases.ts index 56481502712..70e840d769e 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,11 +1,11 @@ import BattleScene, { bypassLogin } from "./battle-scene"; import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from "./utils"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; import { Mode } from "./ui/ui"; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; -import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, PokemonMoveAccuracyBoosterModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier } from "./modifier/modifier"; +import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier } from "./modifier/modifier"; import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball"; import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims"; @@ -24,9 +24,8 @@ import { getPokemonMessage, getPokemonNameWithAffix } from "./messages"; import { Starter } from "./ui/starter-select-ui-handler"; 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, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, 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, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -3079,54 +3078,16 @@ export class MoveEffectPhase extends PokemonPhase { return false; } - const moveAccuracy = new Utils.NumberHolder(this.move.getMove().accuracy); + const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user, target); - applyMoveAttrs(VariableAccuracyAttr, user, target, this.move.getMove(), moveAccuracy); - applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, this.move.getMove(), { value: false }, moveAccuracy); - - if (moveAccuracy.value === -1) { + if (moveAccuracy === -1) { return true; } - const isOhko = this.move.getMove().hasAttr(OneHitKOAccuracyAttr); - - if (!isOhko) { - user.scene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); - } - - if (this.scene.arena.weather?.weatherType === WeatherType.FOG) { - moveAccuracy.value = Math.floor(moveAccuracy.value * 0.9); - } - - if (!isOhko && this.scene.arena.getTag(ArenaTagType.GRAVITY)) { - moveAccuracy.value = Math.floor(moveAccuracy.value * 1.67); - } - - const userAccuracyLevel = new Utils.IntegerHolder(user.summonData.battleStats[BattleStat.ACC]); - const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccuracyLevel); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, user, null, targetEvasionLevel); - applyAbAttrs(IgnoreOpponentEvasionAbAttr, user, null, targetEvasionLevel); - applyMoveAttrs(IgnoreOpponentStatChangesAttr, user, target, this.move.getMove(), targetEvasionLevel); - this.scene.applyModifiers(TempBattleStatBoosterModifier, this.player, TempBattleStat.ACC, userAccuracyLevel); - + const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove()); const rand = user.randSeedInt(100, 1); - const accuracyMultiplier = new Utils.NumberHolder(1); - if (userAccuracyLevel.value !== targetEvasionLevel.value) { - accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value - ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 - : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); - } - - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, user, BattleStat.ACC, accuracyMultiplier, this.move.getMove()); - - const evasionMultiplier = new Utils.NumberHolder(1); - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); - - accuracyMultiplier.value /= evasionMultiplier.value; - - return rand <= moveAccuracy.value * accuracyMultiplier.value; + return rand <= moveAccuracy * accuracyMultiplier; } getUserPokemon(): Pokemon { diff --git a/src/test/abilities/wonder_skin.test.ts b/src/test/abilities/wonder_skin.test.ts index 477b03f0450..25e215db484 100644 --- a/src/test/abilities/wonder_skin.test.ts +++ b/src/test/abilities/wonder_skin.test.ts @@ -3,14 +3,12 @@ import Phaser from "phaser"; import GameManager from "#app/test/utils/gameManager"; import overrides from "#app/overrides"; import { Species } from "#enums/species"; -import { TurnEndPhase, } from "#app/phases"; +import { MoveEffectPhase } 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"; +import { allMoves } from "#app/data/move.js"; +import { allAbilities } from "#app/data/ability.js"; describe("Abilities - Wonder Skin", () => { let phaserGame: Phaser.Game; @@ -29,113 +27,51 @@ describe("Abilities - Wonder Skin", () => { 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, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.WONDER_SKIN); 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]); + const moveToCheck = allMoves[Moves.CHARM]; + vi.spyOn(moveToCheck, "calculateBattleAccuracy"); + + await game.startBattle([Species.PIKACHU]); game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + await game.phaseInterceptor.to(MoveEffectPhase); - const mockedAccuracy = getMockedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); - - await game.phaseInterceptor.to(TurnEndPhase); - - expect(mockedAccuracy).not.toBe(undefined); - expect(mockedAccuracy).not.toBe(100); - expect(mockedAccuracy).toBe(50); + expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(50); }); it("does not lower accuracy of non-status moves", async () => { - await game.startBattle([Species.MAGIKARP]); + const moveToCheck = allMoves[Moves.TACKLE]; + vi.spyOn(moveToCheck, "calculateBattleAccuracy"); + + await game.startBattle([Species.PIKACHU]); game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); + await game.phaseInterceptor.to(MoveEffectPhase); - const mockedAccuracy = getMockedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.TACKLE]); - - await game.phaseInterceptor.to(TurnEndPhase); - - expect(mockedAccuracy).not.toBe(undefined); - expect(mockedAccuracy).toBe(100); - expect(mockedAccuracy).not.toBe(50); + expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100); }); - it("does not affect pokemon with Mold Breaker", async () => { - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.MOLD_BREAKER); + const bypassAbilities = [Abilities.MOLD_BREAKER, Abilities.TERAVOLT, Abilities.TURBOBLAZE]; - await game.startBattle([Species.MAGIKARP]); + bypassAbilities.forEach(ability => { + it(`does not affect pokemon with ${allAbilities[ability].name}`, async () => { + const moveToCheck = allMoves[Moves.CHARM]; - game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(ability); + vi.spyOn(moveToCheck, "calculateBattleAccuracy"); - const mockedAccuracy = getMockedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); + await game.startBattle([Species.PIKACHU]); + game.doAttack(getMovePosition(game.scene, 0, Moves.CHARM)); + await game.phaseInterceptor.to(MoveEffectPhase); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(mockedAccuracy).not.toBe(undefined); - expect(mockedAccuracy).toBe(100); - expect(mockedAccuracy).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 mockedAccuracy = getMockedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); - - await game.phaseInterceptor.to(TurnEndPhase); - - expect(mockedAccuracy).not.toBe(undefined); - expect(mockedAccuracy).toBe(100); - expect(mockedAccuracy).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 mockedAccuracy = getMockedMoveAccuracy(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[Moves.CHARM]); - - await game.phaseInterceptor.to(TurnEndPhase); - - expect(mockedAccuracy).not.toBe(undefined); - expect(mockedAccuracy).toBe(100); - expect(mockedAccuracy).not.toBe(50); + expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100); + }); }); }); - -/** - * Calculates the mocked accuracy of a move. - * Note this does not consider other accuracy calculations - * except the power multiplier from Wonder Skin. - * Bypassed by MoveAbilityBypassAbAttr {@linkcode MoveAbilityBypassAbAttr} - * - * @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 getMockedMoveAccuracy = (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; -}; diff --git a/src/test/arena/arena_gravity.test.ts b/src/test/arena/arena_gravity.test.ts new file mode 100644 index 00000000000..27ed2fcba6c --- /dev/null +++ b/src/test/arena/arena_gravity.test.ts @@ -0,0 +1,83 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { + MoveEffectPhase, + TurnEndPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { allMoves } from "#app/data/move.js"; +import { ArenaTagType } from "#app/enums/arena-tag-type.js"; +import { Abilities } from "#app/enums/abilities.js"; + +describe("Arena - Gravity", () => { + 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, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.GRAVITY, Moves.FISSURE]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.UNNERVE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(new Array(4).fill(Moves.SPLASH)); + }); + + it("non-OHKO move accuracy is multiplied by 1.67", async () => { + const moveToCheck = allMoves[Moves.TACKLE]; + + vi.spyOn(moveToCheck, "calculateBattleAccuracy"); + + // Setup Gravity on first turn + await game.startBattle([Species.PIKACHU]); + game.doAttack(getMovePosition(game.scene, 0, Moves.GRAVITY)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); + + // Use non-OHKO move on second turn + await game.toNextTurn(); + game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 1.67); + }); + + it("OHKO move accuracy is not affected", async () => { + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(5); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(5); + + /** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */ + const moveToCheck = allMoves[Moves.FISSURE]; + + vi.spyOn(moveToCheck, "calculateBattleAccuracy"); + + // Setup Gravity on first turn + await game.startBattle([Species.PIKACHU]); + game.doAttack(getMovePosition(game.scene, 0, Moves.GRAVITY)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); + + // Use OHKO move on second turn + await game.toNextTurn(); + game.doAttack(getMovePosition(game.scene, 0, Moves.FISSURE)); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30); + }); +}); diff --git a/src/test/arena/weather_fog.test.ts b/src/test/arena/weather_fog.test.ts new file mode 100644 index 00000000000..ef11d5a8c72 --- /dev/null +++ b/src/test/arena/weather_fog.test.ts @@ -0,0 +1,51 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { + MoveEffectPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { allMoves } from "#app/data/move.js"; +import { WeatherType } from "#app/data/weather.js"; +import { Abilities } from "#app/enums/abilities.js"; + +describe("Weather - Fog", () => { + 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, "WEATHER_OVERRIDE", "get").mockReturnValue(WeatherType.FOG); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(new Array(4).fill(Moves.SPLASH)); + }); + + it("move accuracy is multiplied by 90%", async () => { + const moveToCheck = allMoves[Moves.TACKLE]; + + vi.spyOn(moveToCheck, "calculateBattleAccuracy"); + + await game.startBattle([Species.MAGIKARP]); + game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 0.9); + }); +}); diff --git a/src/test/moves/double_team.test.ts b/src/test/moves/double_team.test.ts new file mode 100644 index 00000000000..ffe918f7232 --- /dev/null +++ b/src/test/moves/double_team.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import 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 { BattleStat } from "#app/data/battle-stat.js"; +import { Abilities } from "#app/enums/abilities.js"; + +describe("Moves - Double Team", () => { + 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, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DOUBLE_TEAM]); + vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + it("increases the user's evasion by one stage.", async () => { + await game.startBattle([Species.MAGIKARP]); + + const ally = game.scene.getPlayerPokemon(); + const enemy = game.scene.getEnemyPokemon(); + + vi.spyOn(enemy, "getAccuracyMultiplier"); + expect(ally.summonData.battleStats[BattleStat.EVA]).toBe(0); + + game.doAttack(getMovePosition(game.scene, 0, Moves.DOUBLE_TEAM)); + await game.phaseInterceptor.to(TurnEndPhase); + await game.toNextTurn(); + + expect(ally.summonData.battleStats[BattleStat.EVA]).toBe(1); + expect(enemy.getAccuracyMultiplier).toHaveReturnedWith(.75); + }); +});