diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 81ba4b17005..520fbad5a5e 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -86,37 +86,57 @@ export class MistTag extends ArenaTag { } } +/** + * Reduces the damage of specific move categories in the arena. + * @extends ArenaTag + */ export class WeakenMoveScreenTag extends ArenaTag { - constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide) { + protected weakenedCategories: MoveCategory[]; + + /** + * Creates a new instance of the WeakenMoveScreenTag class. + * + * @param tagType - The type of the arena tag. + * @param turnCount - The number of turns the tag is active. + * @param sourceMove - The move that created the tag. + * @param sourceId - The ID of the source of the tag. + * @param side - The side (player or enemy) the tag affects. + * @param weakenedCategories - The categories of moves that are weakened by this tag. + */ + constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, weakenedCategories: MoveCategory[]) { super(tagType, turnCount, sourceMove, sourceId, side); + + this.weakenedCategories = weakenedCategories; } + /** + * Applies the weakening effect to the move. + * + * @param arena - The arena where the move is applied. + * @param args - The arguments for the move application. + * @param args[0] - The category of the move. + * @param args[1] - A boolean indicating whether it is a double battle. + * @param args[2] - An object of type `Utils.NumberHolder` that holds the damage multiplier + * + * @returns True if the move was weakened, otherwise false. + */ apply(arena: Arena, args: any[]): boolean { - if ((args[1] as boolean)) { - (args[2] as Utils.NumberHolder).value = 2732/4096; - } else { - (args[2] as Utils.NumberHolder).value = 0.5; - } - return true; - } -} - -class ReflectTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { - super(ArenaTagType.REFLECT, turnCount, Moves.REFLECT, sourceId, side); - } - - apply(arena: Arena, args: any[]): boolean { - if ((args[0] as MoveCategory) === MoveCategory.PHYSICAL) { - if ((args[1] as boolean)) { - (args[2] as Utils.NumberHolder).value = 2732/4096; - } else { - (args[2] as Utils.NumberHolder).value = 0.5; - } + if (this.weakenedCategories.includes((args[0] as MoveCategory))) { + (args[2] as Utils.NumberHolder).value = (args[1] as boolean) ? 2732/4096 : 0.5; return true; } return false; } +} + +/** + * Reduces the damage of physical moves. + * Used by {@linkcode Moves.REFLECT} + */ +class ReflectTag extends WeakenMoveScreenTag { + constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.REFLECT, turnCount, Moves.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]); + } onAdd(arena: Arena, quiet: boolean = false): void { if (!quiet) { @@ -125,21 +145,13 @@ class ReflectTag extends WeakenMoveScreenTag { } } +/** + * Reduces the damage of special moves. + * Used by {@linkcode Moves.LIGHT_SCREEN} + */ class LightScreenTag extends WeakenMoveScreenTag { constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { - super(ArenaTagType.LIGHT_SCREEN, turnCount, Moves.LIGHT_SCREEN, sourceId, side); - } - - apply(arena: Arena, args: any[]): boolean { - if ((args[0] as MoveCategory) === MoveCategory.SPECIAL) { - if ((args[1] as boolean)) { - (args[2] as Utils.NumberHolder).value = 2732/4096; - } else { - (args[2] as Utils.NumberHolder).value = 0.5; - } - return true; - } - return false; + super(ArenaTagType.LIGHT_SCREEN, turnCount, Moves.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]); } onAdd(arena: Arena, quiet: boolean = false): void { @@ -149,9 +161,13 @@ class LightScreenTag extends WeakenMoveScreenTag { } } +/** + * Reduces the damage of physical and special moves. + * Used by {@linkcode Moves.AURORA_VEIL} + */ class AuroraVeilTag extends WeakenMoveScreenTag { constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { - super(ArenaTagType.AURORA_VEIL, turnCount, Moves.AURORA_VEIL, sourceId, side); + super(ArenaTagType.AURORA_VEIL, turnCount, Moves.AURORA_VEIL, sourceId, side, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL]); } onAdd(arena: Arena, quiet: boolean = false): void { diff --git a/src/test/moves/aurora_veil.test.ts b/src/test/moves/aurora_veil.test.ts new file mode 100644 index 00000000000..e9c3d920717 --- /dev/null +++ b/src/test/moves/aurora_veil.test.ts @@ -0,0 +1,124 @@ +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 { + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#app/enums/abilities.js"; +import Pokemon from "#app/field/pokemon.js"; +import Move, { allMoves } from "#app/data/move.js"; +import { NumberHolder } from "#app/utils.js"; +import { ArenaTagSide } from "#app/data/arena-tag.js"; +import { WeatherType } from "#app/data/weather.js"; +import { ArenaTagType } from "#app/enums/arena-tag-type.js"; + + +describe("Moves - Aurora Veil", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const singleBattleMultiplier = 0.5; + const doubleBattleMultiplier = 2732/4096; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ABSORB, Moves.ROCK_SLIDE, Moves.TACKLE]); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.AURORA_VEIL, Moves.AURORA_VEIL, Moves.AURORA_VEIL, Moves.AURORA_VEIL]); + vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(WeatherType.HAIL); + }); + + it("reduces damage of physical attacks by half in a single battle", async() => { + const moveToUse = Moves.TACKLE; + await game.startBattle([Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * singleBattleMultiplier); + }); + + it("reduces damage of physical attacks by a third in a double battle", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + + const moveToUse = Moves.ROCK_SLIDE; + await game.startBattle([Species.SHUCKLE, Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + game.doAttack(getMovePosition(game.scene, 1, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * doubleBattleMultiplier); + }); + + it("reduces damage of special attacks by half in a single battle", async() => { + const moveToUse = Moves.ABSORB; + await game.startBattle([Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * singleBattleMultiplier); + }); + + it("reduces damage of special attacks by a third in a double battle", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + + const moveToUse = Moves.DAZZLING_GLEAM; + await game.startBattle([Species.SHUCKLE, Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + game.doAttack(getMovePosition(game.scene, 1, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * doubleBattleMultiplier); + }); +}); + +/** + * Calculates the damage of a move multiplied by screen's multiplier, Auroa Veil in this case {@linkcode Moves.AURORA_VEIL}. + * Please note this does not consider other damage calculations except the screen multiplier. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used. + * @returns The calculated move damage considering any weakening effects. + */ +const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) => { + const multiplierHolder = new NumberHolder(1); + const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + + if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) { + defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, move.category, defender.scene.currentBattle.double, multiplierHolder); + } + + return move.power * multiplierHolder.value; +}; diff --git a/src/test/moves/light_screen.test.ts b/src/test/moves/light_screen.test.ts new file mode 100644 index 00000000000..30a27ce4412 --- /dev/null +++ b/src/test/moves/light_screen.test.ts @@ -0,0 +1,106 @@ +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 { + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#app/enums/abilities.js"; +import Pokemon from "#app/field/pokemon.js"; +import Move, { allMoves } from "#app/data/move.js"; +import { NumberHolder } from "#app/utils.js"; +import { ArenaTagSide } from "#app/data/arena-tag.js"; +import { ArenaTagType } from "#app/enums/arena-tag-type.js"; + + +describe("Moves - Light Screen", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const singleBattleMultiplier = 0.5; + const doubleBattleMultiplier = 2732/4096; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ABSORB, Moves.DAZZLING_GLEAM, Moves.TACKLE]); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.LIGHT_SCREEN, Moves.LIGHT_SCREEN, Moves.LIGHT_SCREEN, Moves.LIGHT_SCREEN]); + vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); + }); + + it("reduces damage of special attacks by half in a single battle", async() => { + const moveToUse = Moves.ABSORB; + await game.startBattle([Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * singleBattleMultiplier); + }); + + it("reduces damage of special attacks by a third in a double battle", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + + const moveToUse = Moves.DAZZLING_GLEAM; + await game.startBattle([Species.SHUCKLE, Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + game.doAttack(getMovePosition(game.scene, 1, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * doubleBattleMultiplier); + }); + + it("does not affect physical attacks", async() => { + const moveToUse = Moves.TACKLE; + await game.startBattle([Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power); + }); +}); + +/** + * Calculates the damage of a move multiplied by screen's multiplier, Light Screen in this case {@linkcode Moves.LIGHT_SCREEN}. + * Please note this does not consider other damage calculations except the screen multiplier. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used. + * @returns The calculated move damage considering any weakening effects. + */ +const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) => { + const multiplierHolder = new NumberHolder(1); + const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + + if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) { + defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, move.category, defender.scene.currentBattle.double, multiplierHolder); + } + + return move.power * multiplierHolder.value; +}; diff --git a/src/test/moves/reflect.test.ts b/src/test/moves/reflect.test.ts new file mode 100644 index 00000000000..00fb9a69f2f --- /dev/null +++ b/src/test/moves/reflect.test.ts @@ -0,0 +1,106 @@ +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 { + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#app/enums/abilities.js"; +import Pokemon from "#app/field/pokemon.js"; +import Move, { allMoves } from "#app/data/move.js"; +import { NumberHolder } from "#app/utils.js"; +import { ArenaTagSide } from "#app/data/arena-tag.js"; +import { ArenaTagType } from "#app/enums/arena-tag-type.js"; + + +describe("Moves - Reflect", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const singleBattleMultiplier = 0.5; + const doubleBattleMultiplier = 2732/4096; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ABSORB, Moves.ROCK_SLIDE, Moves.TACKLE]); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.REFLECT, Moves.REFLECT, Moves.REFLECT, Moves.REFLECT]); + vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); + }); + + it("reduces damage of physical attacks by half in a single battle", async() => { + const moveToUse = Moves.TACKLE; + await game.startBattle([Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * singleBattleMultiplier); + }); + + it("reduces damage of physical attacks by a third in a double battle", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + + const moveToUse = Moves.ROCK_SLIDE; + await game.startBattle([Species.SHUCKLE, Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + game.doAttack(getMovePosition(game.scene, 1, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power * doubleBattleMultiplier); + }); + + it("does not affect special attacks", async() => { + const moveToUse = Moves.ABSORB; + await game.startBattle([Species.SHUCKLE]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const mockedDmg = getMockedMoveDamage(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToUse]); + + expect(mockedDmg).toBe(allMoves[moveToUse].power); + }); +}); + +/** + * Calculates the damage of a move multiplied by screen's multiplier, Reflect in this case {@linkcode Moves.REFLECT}. + * Please note this does not consider other damage calculations except the screen multiplier. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used. + * @returns The calculated move damage considering any weakening effects. + */ +const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) => { + const multiplierHolder = new NumberHolder(1); + const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + + if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) { + defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, move.category, defender.scene.currentBattle.double, multiplierHolder); + } + + return move.power * multiplierHolder.value; +};