[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
This commit is contained in:
Adrian T 2024-07-14 12:28:39 +08:00 committed by GitHub
parent eedabbf17c
commit 8d5bfa51e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 233 additions and 395 deletions

View File

@ -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 {

View File

@ -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);

View File

@ -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;
};

View File

@ -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"];
};

View File

@ -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"];
};

View File

@ -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;
};

View File

@ -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);
});
});