From 8d5bfa51e8033e94a87928ace555ed5266324805 Mon Sep 17 00:00:00 2001 From: Adrian T <68144167+torranx@users.noreply.github.com> Date: Sun, 14 Jul 2024 12:28:39 +0800 Subject: [PATCH] [Enhancement] Decouple move power calculation from Pokemon.apply(), Fixes Power Spot & Battery not boosting ally's move (#2984) * refactor power calc, fix battery & power spot * fix hard press unit test * fix hard press * refactor tests * use sypOn hp instead * rename method * cleanup tests * improve tests * use slow vs fast pokemon * fix steely spirit test * fix steely spirit for real this time * remove unnecessary test * address pr feedback * add removed code --- src/data/move.ts | 72 +++++++++++- src/field/pokemon.ts | 62 ++-------- src/test/abilities/aura_break.test.ts | 63 +++-------- src/test/abilities/battery.test.ts | 103 ++++++----------- src/test/abilities/power_spot.test.ts | 100 +++++------------ src/test/abilities/steely_spirit.test.ts | 91 +++++++-------- src/test/moves/hard_press.test.ts | 137 +++++++---------------- 7 files changed, 233 insertions(+), 395 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index ff3054ab7b4..54f35129063 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,7 +1,7 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; import { BattleEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases"; import { BattleStat, getBattleStatName } from "./battle-stat"; -import { EncoreTag, SemiInvulnerableTag } from "./battler-tags"; +import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, TypeBoostTag } from "./battler-tags"; import { getPokemonMessage, getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect"; @@ -9,10 +9,10 @@ import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; -import { ArenaTagSide, ArenaTrapTag } 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 } from "./ability"; +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 { allAbilities } from "./ability"; -import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier"; +import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; import { Stat } from "./pokemon-stat"; import { TerrainType } from "./terrain"; @@ -655,6 +655,70 @@ export default class Move implements Localizable { return score; } + + /** + * Calculates the power of a move in battle based on various conditions and attributes. + * + * @param source {@linkcode Pokemon} The Pokémon using the move. + * @param target {@linkcode Pokemon} The Pokémon being targeted by the move. + * @returns The calculated power of the move. + */ + calculateBattlePower(source: Pokemon, target: Pokemon): number { + const power = new Utils.NumberHolder(this.power); + + const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1); + applyPreAttackAbAttrs(MoveTypeChangeAttr, source, target, this, typeChangeMovePowerMultiplier); + + const sourceTeraType = source.getTeraType(); + if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !source.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { + power.value = 60; + } + + applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, power); + + if (source.getAlly()) { + applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source.getAlly(), target, this, power); + } + + const fieldAuras = new Set( + source.scene.getField(true) + .map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[]) + .flat(), + ); + for (const aura of fieldAuras) { + // The only relevant values are `move` and the `power` holder + aura.applyPreAttack(null, null, null, this, [power]); + } + + const alliedField: Pokemon[] = source instanceof PlayerPokemon ? source.scene.getPlayerField() : source.scene.getEnemyField(); + alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, target, this, power)); + + power.value *= typeChangeMovePowerMultiplier.value; + + const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === this.type) as TypeBoostTag; + if (typeBoost) { + power.value *= typeBoost.boostValue; + } + + if (source.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() && this.type === Type.GROUND && this.moveTarget === MoveTarget.ALL_NEAR_OTHERS) { + power.value /= 2; + } + + applyMoveAttrs(VariablePowerAttr, source, target, this, power); + + source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); + + if (!this.hasAttr(TypelessAttr)) { + source.scene.arena.applyTags(WeakenMoveTypeTag, this.type, power); + source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power); + } + + if (source.getTag(HelpingHandTag)) { + power.value *= 1.5; + } + + return power.value; + } } export class AttackMove extends Move { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 18a10f8b5d5..6cf3c2ece3f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,14 +3,14 @@ import BattleScene, { AnySound } from "../battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; import { Stat } from "../data/pokemon-stat"; -import { AttackTypeBoosterModifier, DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonMultiHitModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; import { PokeballType } from "../data/pokeball"; import { Gender } from "../data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; @@ -19,11 +19,11 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases"; import { BattleStat } from "../data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; -import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr, UserFieldMoveTypePowerBoostAbAttr } from "../data/ability"; +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 PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1747,9 +1747,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory); const moveCategory = variableCategory.value as MoveCategory; - const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1); applyMoveAttrs(VariableMoveTypeAttr, source, this, move); - applyPreAttackAbAttrs(MoveTypeChangeAttr, source, this, move, typeChangeMovePowerMultiplier); const types = this.getTypes(true, true); const cancelled = new Utils.BooleanHolder(false); @@ -1778,31 +1776,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { case MoveCategory.PHYSICAL: case MoveCategory.SPECIAL: const isPhysical = moveCategory === MoveCategory.PHYSICAL; - const power = new Utils.NumberHolder(move.power); + const power = move.calculateBattlePower(source, this); const sourceTeraType = source.getTeraType(); - if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type && power.value < 60 && move.priority <= 0 && !move.hasAttr(MultiHitAttr) && !this.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { - power.value = 60; - } - applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, this, move, power); - - if (source.getAlly()?.hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) { - applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source, this, move, power); - } - - const fieldAuras = new Set( - this.scene.getField(true) - .map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[]) - .flat(), - ); - for (const aura of fieldAuras) { - // The only relevant values are `move` and the `power` holder - aura.applyPreAttack(null, null, null, move, [power]); - } - - const alliedField: Pokemon[] = source instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); - alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, power)); - - power.value *= typeChangeMovePowerMultiplier.value; if (!typeless) { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier); @@ -1818,28 +1793,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { result = HitResult.NO_EFFECT; } else { const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag; - if (typeBoost) { - power.value *= typeBoost.boostValue; - if (typeBoost.oneUse) { - source.removeTag(typeBoost.tagType); - } + if (typeBoost?.oneUse) { + source.removeTag(typeBoost.tagType); } + const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(move.type, source.isGrounded())); applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); - if (this.scene.arena.getTerrainType() === TerrainType.GRASSY && this.isGrounded() && move.type === Type.GROUND && move.moveTarget === MoveTarget.ALL_NEAR_OTHERS) { - power.value /= 2; - } - applyMoveAttrs(VariablePowerAttr, source, this, move, power); - - this.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); - if (!typeless) { - this.scene.arena.applyTags(WeakenMoveTypeTag, move.type, power); - this.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, move.type, power); - } - if (source.getTag(HelpingHandTag)) { - power.value *= 1.5; - } let isCritical: boolean; const critOnly = new Utils.BooleanHolder(false); const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); @@ -1910,7 +1870,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!isTypeImmune) { damage.value = Math.ceil( - ((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) + ((((2 * source.level / 5 + 2) * power * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * ((this.scene.randBattleSeedInt(16) + 85) / 100) * criticalMultiplier.value); if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { if (!move.hasAttr(BypassBurnDamageReductionAttr)) { @@ -1981,7 +1941,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, damage); - console.log("damage", damage.value, move.name, power.value, sourceAtk, targetDef); + console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); // In case of fatal damage, this tag would have gotten cleared before we could lapse it. const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); diff --git a/src/test/abilities/aura_break.test.ts b/src/test/abilities/aura_break.test.ts index 77718f6d514..fa7f34edb2f 100644 --- a/src/test/abilities/aura_break.test.ts +++ b/src/test/abilities/aura_break.test.ts @@ -7,15 +7,13 @@ 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 Pokemon from "#app/field/pokemon.js"; -import { FieldMoveTypePowerBoostAbAttr } from "#app/data/ability.js"; -import { NumberHolder } from "#app/utils.js"; +import { allMoves } from "#app/data/move.js"; describe("Abilities - Aura Break", () => { let phaserGame: Phaser.Game; let game: GameManager; - const multiplier = 9 / 16; + + const auraBreakMultiplier = 9/16 * 4/3; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -33,63 +31,34 @@ describe("Abilities - Aura Break", () => { vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.MOONBLAST, Moves.DARK_PULSE, Moves.MOONBLAST, Moves.DARK_PULSE]); vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.AURA_BREAK); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); }); it("reverses the effect of fairy aura", async () => { + const moveToCheck = allMoves[Moves.MOONBLAST]; + const basePower = moveToCheck.power; + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.FAIRY_AURA); - const basePower = allMoves[Moves.MOONBLAST].power; - await game.startBattle([Species.MAGIKARP]); + vi.spyOn(moveToCheck, "calculateBattlePower"); + await game.startBattle([Species.PIKACHU]); game.doAttack(getMovePosition(game.scene, 0, Moves.MOONBLAST)); - - const appliedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[Moves.MOONBLAST]); - await game.phaseInterceptor.to(MoveEffectPhase); - expect(appliedPower).not.toBe(undefined); - expect(appliedPower).not.toBe(basePower); - expect(appliedPower).toBe(basePower * multiplier); - + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(expect.closeTo(basePower * auraBreakMultiplier)); }); it("reverses the effect of dark aura", async () => { + const moveToCheck = allMoves[Moves.DARK_PULSE]; + const basePower = moveToCheck.power; + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DARK_AURA); - const basePower = allMoves[Moves.DARK_PULSE].power; - await game.startBattle([Species.MAGIKARP]); + vi.spyOn(moveToCheck, "calculateBattlePower"); + await game.startBattle([Species.PIKACHU]); game.doAttack(getMovePosition(game.scene, 0, Moves.DARK_PULSE)); - - const appliedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[Moves.DARK_PULSE]); - await game.phaseInterceptor.to(MoveEffectPhase); - expect(appliedPower).not.toBe(undefined); - expect(appliedPower).not.toBe(basePower); - expect(appliedPower).toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(expect.closeTo(basePower * auraBreakMultiplier)); }); }); - -/** - * Calculates the mocked power of a move in a Pokémon battle, taking into account certain abilities. - * - * @param defender - The defending Pokémon. - * @param attacker - The attacking Pokémon. - * @param move - The move being used in the attack. - * @returns The calculated power of the move after applying any relevant ability effects. - * - * @remarks - * This function creates a NumberHolder with the initial power of the move. - * It then checks if the defender has an ability with the FieldMoveTypePowerBoostAbAttr. - * If so, it applies a power modification of 9/16 using an instance of FieldMoveTypePowerBoostAbAttr. - * The final calculated power is then returned. - */ -const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move): number => { - const powerHolder = new NumberHolder(move.power); - - if (defender.hasAbilityWithAttr(FieldMoveTypePowerBoostAbAttr)) { - const auraBreakInstance = new FieldMoveTypePowerBoostAbAttr(move.type, 9 / 16); - auraBreakInstance.applyPreAttack(attacker, false, defender, move, [powerHolder]); - } - - return powerHolder.value; -}; diff --git a/src/test/abilities/battery.test.ts b/src/test/abilities/battery.test.ts index 1077394ee48..b600772f3c4 100644 --- a/src/test/abilities/battery.test.ts +++ b/src/test/abilities/battery.test.ts @@ -5,15 +5,16 @@ import overrides from "#app/overrides"; import { Species } from "#enums/species"; import { Moves } from "#enums/moves"; import { getMovePosition } from "#app/test/utils/gameManagerUtils"; -import Move, { allMoves, MoveCategory } from "#app/data/move.js"; -import { AllyMoveCategoryPowerBoostAbAttr } 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 { Abilities } from "#app/enums/abilities.js"; +import { MoveEffectPhase, TurnEndPhase } from "#app/phases.js"; describe("Abilities - Battery", () => { let phaserGame: Phaser.Game; let game: GameManager; + const batteryMultiplier = 1.3; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,94 +28,54 @@ describe("Abilities - Battery", () => { beforeEach(() => { game = new GameManager(phaserGame); vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.ROCK_SLIDE, Moves.SPLASH, Moves.HEAT_WAVE]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.BREAKING_SWIPE, Moves.SPLASH, Moves.DAZZLING_GLEAM]); vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); }); it("raises the power of allies' special moves by 30%", async () => { - const moveToBeUsed = Moves.HEAT_WAVE; - const basePower = allMoves[moveToBeUsed].power; + const moveToCheck = allMoves[Moves.DAZZLING_GLEAM]; + const basePower = moveToCheck.power; - await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]); + vi.spyOn(moveToCheck, "calculateBattlePower"); - game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + await game.startBattle([Species.PIKACHU, Species.CHARJABUG]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.DAZZLING_GLEAM)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(MoveEffectPhase); - const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); - const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); - - expect(mockedPower).not.toBe(undefined); - expect(mockedPower).not.toBe(basePower); - expect(mockedPower).toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower * batteryMultiplier); }); it("does not raise the power of allies' non-special moves", async () => { - const moveToBeUsed = Moves.ROCK_SLIDE; - const basePower = allMoves[moveToBeUsed].power; + const moveToCheck = allMoves[Moves.BREAKING_SWIPE]; + const basePower = moveToCheck.power; - await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]); + vi.spyOn(moveToCheck, "calculateBattlePower"); - game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + await game.startBattle([Species.PIKACHU, Species.CHARJABUG]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.BREAKING_SWIPE)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(MoveEffectPhase); - const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); - const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); - - expect(mockedPower).not.toBe(undefined); - expect(mockedPower).toBe(basePower); - expect(mockedPower).not.toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower); }); it("does not raise the power of the ability owner's special moves", async () => { - const moveToBeUsed = Moves.HEAT_WAVE; - const basePower = allMoves[moveToBeUsed].power; + const moveToCheck = allMoves[Moves.DAZZLING_GLEAM]; + const basePower = moveToCheck.power; - await game.startBattle([Species.CHARJABUG, Species.MAGIKARP]); + vi.spyOn(moveToCheck, "calculateBattlePower"); - game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + await game.startBattle([Species.CHARJABUG, Species.PIKACHU]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.DAZZLING_GLEAM)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(TurnEndPhase); - const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]); - const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); - - expect(mockedPower).not.toBe(undefined); - expect(mockedPower).toBe(basePower); - expect(mockedPower).not.toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower); }); }); - -/** - * Calculates the mocked power of a move. - * Note this does not consider other damage calculations - * except the power multiplier from Battery. - * - * @param defender - The defending Pokémon. - * @param attacker - The attacking Pokémon. - * @param move - The move being used by the attacker. - * @returns The adjusted power of the move. - */ -const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => { - const powerHolder = new NumberHolder(move.power); - - /** - * @see AllyMoveCategoryPowerBoostAbAttr - */ - if (attacker.getAlly().hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) { - const batteryInstance = new AllyMoveCategoryPowerBoostAbAttr([MoveCategory.SPECIAL], 1.3); - batteryInstance.applyPreAttack(attacker, false, defender, move, [ powerHolder ]); - } - - return powerHolder.value; -}; - -/** - * Retrieves the power multiplier from a Pokémon's ability attribute. - * - * @param pokemon - The Pokémon whose ability attributes are being queried. - * @returns The power multiplier of the `AllyMoveCategoryPowerBoostAbAttr` attribute. - */ -const getAttrPowerMultiplier = (pokemon: Pokemon) => { - const attr = pokemon.getAbilityAttrs(AllyMoveCategoryPowerBoostAbAttr); - - return (attr[0] as AllyMoveCategoryPowerBoostAbAttr)["powerMultiplier"]; -}; diff --git a/src/test/abilities/power_spot.test.ts b/src/test/abilities/power_spot.test.ts index 129a6cc49a2..577c9f87ec5 100644 --- a/src/test/abilities/power_spot.test.ts +++ b/src/test/abilities/power_spot.test.ts @@ -5,15 +5,16 @@ import overrides from "#app/overrides"; import { Species } from "#enums/species"; import { Moves } from "#enums/moves"; import { getMovePosition } from "#app/test/utils/gameManagerUtils"; -import Move, { allMoves, MoveCategory } from "#app/data/move.js"; -import { AllyMoveCategoryPowerBoostAbAttr } 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 { MoveEffectPhase, TurnEndPhase } from "#app/phases.js"; +import { Abilities } from "#app/enums/abilities.js"; describe("Abilities - Power Spot", () => { let phaserGame: Phaser.Game; let game: GameManager; + const powerSpotMultiplier = 1.3; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,94 +28,51 @@ describe("Abilities - Power Spot", () => { beforeEach(() => { game = new GameManager(phaserGame); vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.ROCK_SLIDE, Moves.SPLASH, Moves.HEAT_WAVE]); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.BREAKING_SWIPE, Moves.SPLASH, Moves.DAZZLING_GLEAM]); vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); }); it("raises the power of allies' special moves by 30%", async () => { - const moveToBeUsed = Moves.HEAT_WAVE; - const basePower = allMoves[moveToBeUsed].power; + const moveToCheck = allMoves[Moves.DAZZLING_GLEAM]; + const basePower = moveToCheck.power; - await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]); + vi.spyOn(moveToCheck, "calculateBattlePower"); - game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + await game.startBattle([Species.PIKACHU, Species.STONJOURNER]); + game.doAttack(getMovePosition(game.scene, 0, Moves.DAZZLING_GLEAM)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(MoveEffectPhase); - const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); - const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); - - expect(mockedPower).not.toBe(undefined); - expect(mockedPower).not.toBe(basePower); - expect(mockedPower).toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower * powerSpotMultiplier); }); it("raises the power of allies' physical moves by 30%", async () => { - const moveToBeUsed = Moves.ROCK_SLIDE; - const basePower = allMoves[moveToBeUsed].power; + const moveToCheck = allMoves[Moves.BREAKING_SWIPE]; + const basePower = moveToCheck.power; - await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]); + vi.spyOn(moveToCheck, "calculateBattlePower"); - game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + await game.startBattle([Species.PIKACHU, Species.STONJOURNER]); + game.doAttack(getMovePosition(game.scene, 0, Moves.BREAKING_SWIPE)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(MoveEffectPhase); - const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); - const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); - - expect(mockedPower).not.toBe(undefined); - expect(mockedPower).not.toBe(basePower); - expect(mockedPower).toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower * powerSpotMultiplier); }); it("does not raise the power of the ability owner's moves", async () => { - const moveToBeUsed = Moves.HEAT_WAVE; - const basePower = allMoves[moveToBeUsed].power; + const moveToCheck = allMoves[Moves.BREAKING_SWIPE]; + const basePower = moveToCheck.power; - await game.startBattle([Species.STONJOURNER, Species.MAGIKARP]); + vi.spyOn(moveToCheck, "calculateBattlePower"); - game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + await game.startBattle([Species.STONJOURNER, Species.PIKACHU]); + game.doAttack(getMovePosition(game.scene, 0, Moves.BREAKING_SWIPE)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(TurnEndPhase); - const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]); - const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); - - expect(mockedPower).not.toBe(undefined); - expect(mockedPower).toBe(basePower); - expect(mockedPower).not.toBe(basePower * multiplier); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower); }); }); - -/** - * Calculates the mocked power of a move. - * Note this does not consider other damage calculations - * except the power multiplier from Power Spot. - * - * @param defender - The defending Pokémon. - * @param attacker - The attacking Pokémon. - * @param move - The move being used by the attacker. - * @returns The adjusted power of the move. - */ -const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => { - const powerHolder = new NumberHolder(move.power); - - /** - * @see AllyMoveCategoryPowerBoostAbAttr - */ - if (attacker.getAlly().hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) { - const powerSpotInstance = new AllyMoveCategoryPowerBoostAbAttr([MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3); - powerSpotInstance.applyPreAttack(attacker, false, defender, move, [ powerHolder ]); - } - - return powerHolder.value; -}; - -/** - * Retrieves the power multiplier from a Pokémon's ability attribute. - * - * @param pokemon - The Pokémon whose ability attributes are being queried. - * @returns The power multiplier of the `AllyMoveCategoryPowerBoostAbAttr` attribute. - */ -const getAttrPowerMultiplier = (pokemon: Pokemon) => { - const attr = pokemon.getAbilityAttrs(AllyMoveCategoryPowerBoostAbAttr); - - return (attr[0] as AllyMoveCategoryPowerBoostAbAttr)["powerMultiplier"]; -}; diff --git a/src/test/abilities/steely_spirit.test.ts b/src/test/abilities/steely_spirit.test.ts index 7eac51bf5f8..403024c7277 100644 --- a/src/test/abilities/steely_spirit.test.ts +++ b/src/test/abilities/steely_spirit.test.ts @@ -5,17 +5,17 @@ import overrides from "#app/overrides"; import { Species } from "#enums/species"; import { Moves } from "#enums/moves"; import { getMovePosition } from "#app/test/utils/gameManagerUtils"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon.js"; -import Move, { allMoves } from "#app/data/move.js"; -import { NumberHolder } from "#app/utils.js"; -import { allAbilities, applyPreAttackAbAttrs, UserFieldMoveTypePowerBoostAbAttr } from "#app/data/ability.js"; +import { allMoves } from "#app/data/move.js"; +import { allAbilities } from "#app/data/ability.js"; import { Abilities } from "#app/enums/abilities.js"; +import { MoveEffectPhase, SelectTargetPhase } from "#app/phases.js"; describe("Abilities - Steely Spirit", () => { let phaserGame: Phaser.Game; let game: GameManager; const steelySpiritMultiplier = 1.5; const moveToCheck = Moves.IRON_HEAD; + const ironHeadPower = allMoves[moveToCheck].power; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -30,29 +30,34 @@ describe("Abilities - Steely Spirit", () => { beforeEach(() => { game = new GameManager(phaserGame); vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.IRON_HEAD, Moves.SPLASH]); vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + vi.spyOn(allMoves[moveToCheck], "calculateBattlePower"); }); - it("increases Steel-type moves used by the user and its allies", async () => { - await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]); - const perserrker = game.scene.getPlayerField()[1]; + it("increases Steel-type moves' power used by the user and its allies by 50%", async () => { + await game.startBattle([Species.PIKACHU, Species.SHUCKLE]); + const boostSource = game.scene.getPlayerField()[1]; + const enemyToCheck = game.scene.getEnemyPokemon(); - vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + vi.spyOn(boostSource, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); - expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); + expect(boostSource.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + await game.phaseInterceptor.to(SelectTargetPhase, false); + game.doSelectTarget(enemyToCheck.getBattlerIndex()); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(MoveEffectPhase); - const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]); - - expect(mockedMovePower).toBe(allMoves[moveToCheck].power * steelySpiritMultiplier); + expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower * steelySpiritMultiplier); }); it("stacks if multiple users with this ability are on the field.", async () => { - await game.startBattle([Species.PERRSERKER, Species.PERRSERKER]); + await game.startBattle([Species.PIKACHU, Species.PIKACHU]); + const enemyToCheck = game.scene.getEnemyPokemon(); game.scene.getPlayerField().forEach(p => { vi.spyOn(p, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); @@ -61,55 +66,35 @@ describe("Abilities - Steely Spirit", () => { expect(game.scene.getPlayerField().every(p => p.hasAbility(Abilities.STEELY_SPIRIT))).toBe(true); game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); - game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(SelectTargetPhase, false); + game.doSelectTarget(enemyToCheck.getBattlerIndex()); + game.doAttack(getMovePosition(game.scene, 1, moveToCheck)); + await game.phaseInterceptor.to(SelectTargetPhase, false); + game.doSelectTarget(enemyToCheck.getBattlerIndex()); + await game.phaseInterceptor.to(MoveEffectPhase); - const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToCheck]); - - expect(mockedMovePower).toBe(allMoves[moveToCheck].power * Math.pow(steelySpiritMultiplier, 2)); + expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower * Math.pow(steelySpiritMultiplier, 2)); }); it("does not take effect when suppressed", async () => { - await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]); - const perserrker = game.scene.getPlayerField()[1]; + await game.startBattle([Species.PIKACHU, Species.SHUCKLE]); + const boostSource = game.scene.getPlayerField()[1]; + const enemyToCheck = game.scene.getEnemyPokemon(); - vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); - expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); + vi.spyOn(boostSource, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + expect(boostSource.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); - perserrker.summonData.abilitySuppressed = true; + boostSource.summonData.abilitySuppressed = true; - expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(false); - expect(perserrker.summonData.abilitySuppressed).toBe(true); + expect(boostSource.hasAbility(Abilities.STEELY_SPIRIT)).toBe(false); + expect(boostSource.summonData.abilitySuppressed).toBe(true); game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + await game.phaseInterceptor.to(SelectTargetPhase, false); + game.doSelectTarget(enemyToCheck.getBattlerIndex()); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(MoveEffectPhase); - const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]); - - expect(mockedMovePower).toBe(allMoves[moveToCheck].power); + expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower); }); }); - -/** - * Calculates the mocked power of a move. - * Note this does not consider other damage calculations - * except the power multiplier from Steely Spirit. - * - * @param defender - The defending Pokémon. - * @param attacker - The attacking Pokémon. - * @param move - The move being used by the attacker. - * @returns The adjusted power of the move. - */ -const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => { - const powerHolder = new NumberHolder(move.power); - - /** - * Check if pokemon has the specified ability and is in effect. - * See Pokemon.hasAbility {@linkcode Pokemon.hasAbility} - */ - if (attacker.hasAbility(Abilities.STEELY_SPIRIT)) { - const alliedField: Pokemon[] = attacker instanceof PlayerPokemon ? attacker.scene.getPlayerField() : attacker.scene.getEnemyField(); - alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, powerHolder)); - } - - return powerHolder.value; -}; diff --git a/src/test/moves/hard_press.test.ts b/src/test/moves/hard_press.test.ts index 1ec8cf3df1d..87d61ecae90 100644 --- a/src/test/moves/hard_press.test.ts +++ b/src/test/moves/hard_press.test.ts @@ -4,39 +4,19 @@ import GameManager from "#app/test/utils/gameManager"; import overrides from "#app/overrides"; import { Species } from "#enums/species"; import { - MoveEffectPhase, + MoveEffectPhase } from "#app/phases"; import { Moves } from "#enums/moves"; import { getMovePosition } from "#app/test/utils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; -import { applyMoveAttrs, VariablePowerAttr } from "#app/data/move"; -import * as Utils from "#app/utils"; -import { Stat } from "#enums/stat"; - -/** - * Checks the base power of the {@linkcode intendedMove} before and after any - * {@linkcode VariablePowerAttr}s have been applied. - * @param phase current {@linkcode MoveEffectPhase} - * @param intendedMove Expected move during this {@linkcode phase} - * @param before Expected base power before any base power changes - * @param after Expected base power after any base power changes - */ -const checkBasePowerChanges = (phase: MoveEffectPhase, intendedMove: Moves, before: number, after: number) => { - // Double check if the intended move was used and verify its initial base power - const move = phase.move.getMove(); - expect(move.id).toBe(intendedMove); - expect(move.power).toBe(before); - - /** Mocking application of {@linkcode VariablePowerAttr} */ - const power = new Utils.IntegerHolder(move.power); - applyMoveAttrs(VariablePowerAttr, phase.getUserPokemon(), phase.getTarget(), move, power); - expect(power.value).toBe(after); -}; +import { allMoves } from "#app/data/move.js"; describe("Moves - Hard Press", () => { let phaserGame: Phaser.Game; let game: GameManager; + const moveToCheck = allMoves[Moves.HARD_PRESS]; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -50,98 +30,59 @@ describe("Moves - Hard Press", () => { beforeEach(() => { game = new GameManager(phaserGame); vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX); - vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.HARD_PRESS ]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MUNCHLAX); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.HARD_PRESS]); + vi.spyOn(moveToCheck, "calculateBattlePower"); }); - it("HARD_PRESS varies based on target health ratio (100%)", async () => { - await game.startBattle([ Species.GRAVELER ]); - const moveToUse = Moves.HARD_PRESS; + it("should return 100 power if target HP ratio is at 100%", async () => { + await game.startBattle([Species.PIKACHU]); - // Force party to go first - game.scene.getParty()[0].stats[Stat.SPD] = 100; - game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS)); + await game.phaseInterceptor.to(MoveEffectPhase); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 100); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(100); }); - it("HARD_PRESS varies based on target health ratio (50%)", async () => { - await game.startBattle([ Species.GRAVELER ]); - const moveToUse = Moves.HARD_PRESS; - const enemy = game.scene.getEnemyParty()[0]; + it("should return 50 power if target HP ratio is at 50%", async () => { + await game.startBattle([Species.PIKACHU]); + const targetHpRatio = .5; + const enemy = game.scene.getEnemyPokemon(); - // Force party to go first - game.scene.getParty()[0].stats[Stat.SPD] = 100; - enemy.stats[Stat.SPD] = 1; + vi.spyOn(enemy, "getHpRatio").mockReturnValue(targetHpRatio); - // Halve the enemy's HP - enemy.hp /= 2; + game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS)); + await game.phaseInterceptor.to(MoveEffectPhase); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 50); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(50); }); - it("HARD_PRESS varies based on target health ratio (1%)", async () => { - await game.startBattle([ Species.GRAVELER ]); - const moveToUse = Moves.HARD_PRESS; - const enemy = game.scene.getEnemyParty()[0]; + it("should return 1 power if target HP ratio is at 1%", async () => { + await game.startBattle([Species.PIKACHU]); + const targetHpRatio = .01; + const enemy = game.scene.getEnemyPokemon(); - // Force party to go first - game.scene.getParty()[0].stats[Stat.SPD] = 100; - enemy.stats[Stat.SPD] = 1; + vi.spyOn(enemy, "getHpRatio").mockReturnValue(targetHpRatio); - // Force enemy to have 1% of full health - enemy.stats[Stat.HP] = 100; - enemy.hp = 1; + game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS)); + await game.phaseInterceptor.to(MoveEffectPhase); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 1); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(1); }); - it("HARD_PRESS varies based on target health ratio, (<1%)", async () => { - await game.startBattle([ Species.GRAVELER ]); - const moveToUse = Moves.HARD_PRESS; - const enemy = game.scene.getEnemyParty()[0]; + it("should return 1 power if target HP ratio is less than 1%", async () => { + await game.startBattle([Species.PIKACHU]); + const targetHpRatio = .005; + const enemy = game.scene.getEnemyPokemon(); - // Force party to go first - game.scene.getParty()[0].stats[Stat.SPD] = 100; - enemy.stats[Stat.SPD] = 1; + vi.spyOn(enemy, "getHpRatio").mockReturnValue(targetHpRatio); - // Force enemy to have less than 1% of full health - enemy.stats[Stat.HP] = 1000; - enemy.hp = 1; + game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS)); + await game.phaseInterceptor.to(MoveEffectPhase); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 1); - }); - - it("HARD_PRESS varies based on target health ratio, random", async () => { - await game.startBattle([ Species.GRAVELER ]); - const moveToUse = Moves.HARD_PRESS; - const enemy = game.scene.getEnemyParty()[0]; - - // Force party to go first - game.scene.getParty()[0].stats[Stat.SPD] = 100; - enemy.stats[Stat.SPD] = 1; - - // Force a random n / 100 ratio with the enemy's HP - enemy.stats[Stat.HP] = 100; - enemy.hp = Utils.randInt(99, 1); - - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - // Because the ratio is n / 100 and the max base power of the move is 100, the resultant base power should just be n - checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, enemy.hp); + expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(1); }); });