diff --git a/src/data/move.ts b/src/data/move.ts index 71ba6cf4afd..3ff843d6b73 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5846,6 +5846,41 @@ export class TransformAttr extends MoveEffectAttr { } } +/** + * Attribute used for status moves, namely Speed Swap, + * that swaps the user's and target's corresponding stats. + * @extends MoveEffectAttr + * @see {@linkcode apply} + */ +export class SwapStatAttr extends MoveEffectAttr { + /** The stat to be swapped between the user and the target */ + private stat: EffectiveStat; + + constructor(stat: EffectiveStat) { + super(); + + this.stat = stat; + } + + /** + * Takes the average of the user's and target's corresponding current + * {@linkcode stat} values and sets that stat to the average for both + * temporarily. + * @param user the {@linkcode Pokemon} that used the move + * @param target the {@linkcode Pokemon} that the move was used on + * @param _move N/A + * @param _args N/A + * @returns true if attribute application succeeds + */ + apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { + const temp = user.getStat(this.stat, false); + user.setStat(this.stat, target.getStat(this.stat, false), false); + target.setStat(this.stat, temp, false); + + return true; + } +} + /** * Attribute used for status moves, namely Power Split and Guard Split, * that take the average of a user's and target's corresponding @@ -8163,7 +8198,7 @@ export function initMoves() { user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)})); }), new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7) - .unimplemented(), + .attr(SwapStatAttr, Stat.SPD), new AttackMove(Moves.SMART_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7), new StatusMove(Moves.PURIFY, Type.POISON, -1, 20, -1, 0, 7) .condition( diff --git a/src/test/moves/guard_split.test.ts b/src/test/moves/guard_split.test.ts index 06adb8b0870..fa905d6a251 100644 --- a/src/test/moves/guard_split.test.ts +++ b/src/test/moves/guard_split.test.ts @@ -1,7 +1,6 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import Phaser from "phaser"; import GameManager from "#app/test/utils/gameManager"; -import Overrides from "#app/overrides"; import { Species } from "#enums/species"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; @@ -34,8 +33,8 @@ describe("Moves - Guard Split", () => { game.override.ability(Abilities.NONE); }); - it("should average the user's Defense and Special Defense stats with those of the target", async () => { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(SPLASH_ONLY); + it("should average the user's DEF and SPDEF stats with those of the target", async () => { + game.override.enemyMoveset(SPLASH_ONLY); await game.startBattle([ Species.INDEEDEE ]); @@ -57,7 +56,7 @@ describe("Moves - Guard Split", () => { }, 20000); it("should be idempotent", async () => { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.GUARD_SPLIT, Moves.GUARD_SPLIT, Moves.GUARD_SPLIT, Moves.GUARD_SPLIT ]); + game.override.enemyMoveset(new Array(4).fill(Moves.GUARD_SPLIT)); await game.startBattle([ Species.INDEEDEE ]); diff --git a/src/test/moves/power_split.test.ts b/src/test/moves/power_split.test.ts index b24d333b3ef..79828ddd22d 100644 --- a/src/test/moves/power_split.test.ts +++ b/src/test/moves/power_split.test.ts @@ -1,7 +1,6 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import Phaser from "phaser"; import GameManager from "#app/test/utils/gameManager"; -import Overrides from "#app/overrides"; import { Species } from "#enums/species"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; @@ -34,8 +33,8 @@ describe("Moves - Power Split", () => { game.override.ability(Abilities.NONE); }); - it("should average the user's Attack and Special Attack stats with those of the target", async () => { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(SPLASH_ONLY); + it("should average the user's ATK and SPATK stats with those of the target", async () => { + game.override.enemyMoveset(SPLASH_ONLY); await game.startBattle([ Species.INDEEDEE ]); @@ -57,7 +56,7 @@ describe("Moves - Power Split", () => { }, 20000); it("should be idempotent", async () => { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.POWER_SPLIT, Moves.POWER_SPLIT, Moves.POWER_SPLIT, Moves.POWER_SPLIT ]); + game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT)); await game.startBattle([ Species.INDEEDEE ]); diff --git a/src/test/moves/speed_swap.test.ts b/src/test/moves/speed_swap.test.ts new file mode 100644 index 00000000000..2815e79f277 --- /dev/null +++ b/src/test/moves/speed_swap.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +describe("Moves - Speed Swap", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override.battleType("single"); + game.override.enemyAbility(Abilities.NONE); + game.override.enemySpecies(Species.MEW); + game.override.enemyLevel(200); + game.override.moveset([ Moves.SPEED_SWAP ]); + game.override.ability(Abilities.NONE); + }); + + it("should swap the user's SPD and the target's SPD stats", async () => { + game.override.enemyMoveset(SPLASH_ONLY); + await game.startBattle([ + Species.INDEEDEE + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const playerSpd = player.getStat(Stat.SPD, false); + const enemySpd = enemy.getStat(Stat.SPD, false); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPEED_SWAP)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.SPD, false)).toBe(enemySpd); + expect(enemy.getStat(Stat.SPD, false)).toBe(playerSpd); + }, 20000); +});