From 6b7efb444b4b774fdb0dc892caa3f9e4b22967ae Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:29:24 -0800 Subject: [PATCH] [Ability] Fully implement Synchronize (#4785) Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 5 ++- src/data/battler-tags.ts | 26 +++++++++++++- src/data/move.ts | 20 ++++++----- src/enums/battler-tag-type.ts | 3 +- src/test/abilities/corrosion.test.ts | 46 ++++++++++++++++++++++++ src/test/abilities/synchronize.test.ts | 12 ------- src/test/moves/psycho_shift.test.ts | 49 ++++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 src/test/abilities/corrosion.test.ts create mode 100644 src/test/moves/psycho_shift.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 4752217da19..2b65ff79d56 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5433,8 +5433,7 @@ export function initAbilities() { .attr(EffectSporeAbAttr), new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .attr(SynchronizeStatusAbAttr) - .partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now + .attr(SynchronizeStatusAbAttr), new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), @@ -6036,7 +6035,7 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.CORROSION, 7) .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) - .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet) + .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented) + fling with toxic orb (not implemented yet) new Ability(Abilities.COMATOSE, 7) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 473974048c9..ebbab7bfa4a 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -18,7 +18,7 @@ import Move, { StatusCategoryOnAllyAttr } from "#app/data/move"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; -import { StatusEffect } from "#app/data/status-effect"; +import { getStatusEffectHealText, StatusEffect } from "#app/data/status-effect"; import { TerrainType } from "#app/data/terrain"; import { Type } from "#app/data/type"; import { WeatherType } from "#app/data/weather"; @@ -2866,6 +2866,28 @@ export class GrudgeTag extends BattlerTag { } } +/** + * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon + */ +export class PsychoShiftTag extends BattlerTag { + constructor() { + super(BattlerTagType.PSYCHO_SHIFT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.PSYCHO_SHIFT); + } + + /** + * Heals Psycho Shift's user of its status effect after it uses a move + * @returns `false` to expire the tag immediately + */ + override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + if (pokemon.status && pokemon.isActive(true)) { + pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); + pokemon.resetStatus(); + pokemon.updateInfo(); + } + return false; + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -3049,6 +3071,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new PowerTrickTag(sourceMove, sourceId); case BattlerTagType.GRUDGE: return new GrudgeTag(); + case BattlerTagType.PSYCHO_SHIFT: + return new PsychoShiftTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index fb09d822a1d..9964409df7a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2270,24 +2270,26 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { super(false, { trigger: MoveEffectTrigger.HIT }); } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + /** + * Applies the effect of Psycho Shift to its target + * Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed. + * @returns `true` if Psycho Shift's effect is able to be applied to the target + */ + apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined); if (target.status) { return false; } else { const canSetStatus = target.canSetStatus(statusToApply, true, false, user); + const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false; - if (canSetStatus) { - if (user.status) { - user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); - } - user.resetStatus(); - user.updateInfo(); - target.trySetStatus(statusToApply, true, user); + if (trySetStatus && user.status) { + // PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move + user.addTag(BattlerTagType.PSYCHO_SHIFT); } - return canSetStatus; + return trySetStatus; } } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 660a47e0d68..b2bbc1e6189 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -90,5 +90,6 @@ export enum BattlerTagType { ELECTRIFIED = "ELECTRIFIED", TELEKINESIS = "TELEKINESIS", COMMANDED = "COMMANDED", - GRUDGE = "GRUDGE" + GRUDGE = "GRUDGE", + PSYCHO_SHIFT = "PSYCHO_SHIFT", } diff --git a/src/test/abilities/corrosion.test.ts b/src/test/abilities/corrosion.test.ts new file mode 100644 index 00000000000..e607e85defb --- /dev/null +++ b/src/test/abilities/corrosion.test.ts @@ -0,0 +1,46 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Corrosion", () => { + 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 + .moveset([ Moves.SPLASH ]) + .battleType("single") + .disableCrits() + .enemySpecies(Species.GRIMER) + .enemyAbility(Abilities.CORROSION) + .enemyMoveset(Moves.TOXIC); + }); + + it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => { + game.override.ability(Abilities.SYNCHRONIZE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(playerPokemon!.status).toBeUndefined(); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(playerPokemon!.status).toBeDefined(); + expect(enemyPokemon!.status).toBeUndefined(); + }); +}); diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index d34b5631271..f30ab9d3db4 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -94,16 +94,4 @@ describe("Abilities - Synchronize", () => { expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); }); - - it("should activate with Psycho Shift after the move clears the status", async () => { - game.override.statusEffect(StatusEffect.PARALYSIS); - await game.classicMode.startBattle(); - - game.move.select(Moves.PSYCHO_SHIFT); - await game.phaseInterceptor.to("BerryPhase"); - - expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise - expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }); }); diff --git a/src/test/moves/psycho_shift.test.ts b/src/test/moves/psycho_shift.test.ts new file mode 100644 index 00000000000..448a8c99ef0 --- /dev/null +++ b/src/test/moves/psycho_shift.test.ts @@ -0,0 +1,49 @@ +import { StatusEffect } from "#app/enums/status-effect"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Psycho Shift", () => { + 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 + .moveset([ Moves.PSYCHO_SHIFT ]) + .ability(Abilities.BALL_FETCH) + .statusEffect(StatusEffect.POISON) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyLevel(20) + .enemyAbility(Abilities.SYNCHRONIZE) + .enemyMoveset(Moves.SPLASH); + }); + + it("If Psycho Shift is used on a Pokémon with Synchronize, the user of Psycho Shift will already be afflicted with a status condition when Synchronize activates", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon?.status).toBeUndefined(); + + game.move.select(Moves.PSYCHO_SHIFT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon?.status).toBeNull(); + expect(enemyPokemon?.status).toBeDefined(); + }); +});