From fd1baef244d949879b3b588498cd4bd48e586711 Mon Sep 17 00:00:00 2001 From: Adrian T <68144167+torranx@users.noreply.github.com> Date: Tue, 18 Jun 2024 04:12:11 +0800 Subject: [PATCH] [Ability] Implement Power Spot & Battery (#2268) * add battery * add power spot * refactor * remove FieldVariableMovePowerAbAttr * remove showing ability bar * document + cleanup * add unit tests * update test name * update variable names * update multiplier --- src/data/ability.ts | 43 ++++++--- src/field/pokemon.ts | 6 +- src/test/abilities/battery.test.ts | 125 ++++++++++++++++++++++++++ src/test/abilities/power_spot.test.ts | 125 ++++++++++++++++++++++++++ 4 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 src/test/abilities/battery.test.ts create mode 100644 src/test/abilities/power_spot.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index e11c891db4b..b96d1e846b7 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1282,17 +1282,18 @@ export class VariableMovePowerBoostAbAttr extends VariableMovePowerAbAttr { } } -export class FieldVariableMovePowerAbAttr extends AbAttr { - applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean { - //const power = args[0] as Utils.NumberHolder; - return false; - } -} - -export class FieldMovePowerBoostAbAttr extends FieldVariableMovePowerAbAttr { +/** + * Boosts the power of a Pokémon's move under certain conditions. + * @extends AbAttr + */ +export class FieldMovePowerBoostAbAttr extends AbAttr { private condition: PokemonAttackCondition; private powerMultiplier: number; + /** + * @param condition - A function that determines whether the power boost condition is met. + * @param powerMultiplier - The multiplier to apply to the move's power when the condition is met. + */ constructor(condition: PokemonAttackCondition, powerMultiplier: number) { super(false); this.condition = condition; @@ -1310,12 +1311,34 @@ export class FieldMovePowerBoostAbAttr extends FieldVariableMovePowerAbAttr { } } +/** + * Boosts the power of a specific type of move. + * @extends FieldMovePowerBoostAbAttr + */ export class FieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr { + /** + * @param boostedType - The type of move that will receive the power boost. + * @param powerMultiplier - The multiplier to apply to the move's power, defaults to 1.5 if not provided. + */ constructor(boostedType: Type, powerMultiplier?: number) { super((pokemon, defender, move) => move.type === boostedType, powerMultiplier || 1.5); } } +/** + * Boosts the power of moves in specified categories. + * @extends FieldMovePowerBoostAbAttr + */ +export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr { + /** + * @param boostedCategories - The categories of moves that will receive the power boost. + * @param powerMultiplier - The multiplier to apply to the move's power. + */ + constructor(boostedCategories: MoveCategory[], powerMultiplier: number) { + super((pokemon, defender, move) => boostedCategories.includes(move.category), powerMultiplier); + } +} + export class BattleStatMultiplierAbAttr extends AbAttr { private battleStat: BattleStat; private multiplier: number; @@ -4548,7 +4571,7 @@ export function initAbilities() { new Ability(Abilities.DANCER, 7) .attr(PostDancingMoveAbAttr), new Ability(Abilities.BATTERY, 7) - .unimplemented(), + .attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL], 1.3), new Ability(Abilities.FLUFFY, 7) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 0.5) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.type === Type.FIRE, 2) @@ -4660,7 +4683,7 @@ export function initAbilities() { .attr(IceFaceMoveImmunityAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE)) .ignorable(), new Ability(Abilities.POWER_SPOT, 8) - .unimplemented(), + .attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3), new Ability(Abilities.MIMICRY, 8) .unimplemented(), new Ability(Abilities.SCREEN_CLEANER, 8) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 715b03be947..7dca91c90fb 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStat 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, FieldMoveTypePowerBoostAbAttr } from "../data/ability"; +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 } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1755,6 +1755,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } 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[]) diff --git a/src/test/abilities/battery.test.ts b/src/test/abilities/battery.test.ts new file mode 100644 index 00000000000..93bac836f61 --- /dev/null +++ b/src/test/abilities/battery.test.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { TurnEndPhase, } from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import 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"; + +describe("Abilities - Battery", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "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_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; + + await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]); + + game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); + const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedPower).not.toBe(undefined); + expect(appliedPower).not.toBe(basePower); + expect(appliedPower).toBe(basePower * multiplier); + }); + + it("does not raise the power of allies' non-special moves", async () => { + const moveToBeUsed = Moves.ROCK_SLIDE; + const basePower = allMoves[moveToBeUsed].power; + + await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]); + + game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); + const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedPower).not.toBe(undefined); + expect(appliedPower).toBe(basePower); + expect(appliedPower).not.toBe(basePower * multiplier); + }); + + it("does not raise the power of the ability owner's special moves", async () => { + const moveToBeUsed = Moves.HEAT_WAVE; + const basePower = allMoves[moveToBeUsed].power; + + await game.startBattle([Species.CHARJABUG, Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]); + const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedPower).not.toBe(undefined); + expect(appliedPower).toBe(basePower); + expect(appliedPower).not.toBe(basePower * multiplier); + }); +}); + +/** + * Calculates the adjusted applied power of a move. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used by the attacker. + * @returns The adjusted power of the move. + */ +const getAppliedMovePower = (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 new file mode 100644 index 00000000000..5450aee9742 --- /dev/null +++ b/src/test/abilities/power_spot.test.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { TurnEndPhase, } from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import 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"; + +describe("Abilities - Power Spot", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "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_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; + + await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]); + + game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); + const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedPower).not.toBe(undefined); + expect(appliedPower).not.toBe(basePower); + expect(appliedPower).toBe(basePower * multiplier); + }); + + it("raises the power of allies' physical moves by 30%", async () => { + const moveToBeUsed = Moves.ROCK_SLIDE; + const basePower = allMoves[moveToBeUsed].power; + + await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]); + + game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]); + const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedPower).not.toBe(undefined); + expect(appliedPower).not.toBe(basePower); + expect(appliedPower).toBe(basePower * multiplier); + }); + + it("does not raise the power of the ability owner's moves", async () => { + const moveToBeUsed = Moves.HEAT_WAVE; + const basePower = allMoves[moveToBeUsed].power; + + await game.startBattle([Species.STONJOURNER, Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]); + const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(appliedPower).not.toBe(undefined); + expect(appliedPower).toBe(basePower); + expect(appliedPower).not.toBe(basePower * multiplier); + }); +}); + +/** + * Calculates the adjusted applied power of a move. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used by the attacker. + * @returns The adjusted power of the move. + */ +const getAppliedMovePower = (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"]; +};