[Ability] Fully implement Synchronize (#4785)

Co-authored-by: frutescens <info@laptop>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Mumble 2024-11-06 08:29:24 -08:00 committed by GitHub
parent 4f733796c5
commit 6b7efb444b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 135 additions and 26 deletions

View File

@ -5433,8 +5433,7 @@ export function initAbilities() {
.attr(EffectSporeAbAttr), .attr(EffectSporeAbAttr),
new Ability(Abilities.SYNCHRONIZE, 3) new Ability(Abilities.SYNCHRONIZE, 3)
.attr(SyncEncounterNatureAbAttr) .attr(SyncEncounterNatureAbAttr)
.attr(SynchronizeStatusAbAttr) .attr(SynchronizeStatusAbAttr),
.partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now
new Ability(Abilities.CLEAR_BODY, 3) new Ability(Abilities.CLEAR_BODY, 3)
.attr(ProtectStatAbAttr) .attr(ProtectStatAbAttr)
.ignorable(), .ignorable(),
@ -6036,7 +6035,7 @@ export function initAbilities() {
.bypassFaint(), .bypassFaint(),
new Ability(Abilities.CORROSION, 7) new Ability(Abilities.CORROSION, 7)
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) .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) new Ability(Abilities.COMATOSE, 7)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)

View File

@ -18,7 +18,7 @@ import Move, {
StatusCategoryOnAllyAttr StatusCategoryOnAllyAttr
} from "#app/data/move"; } from "#app/data/move";
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; 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 { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { WeatherType } from "#app/data/weather"; 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. * 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 * @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); return new PowerTrickTag(sourceMove, sourceId);
case BattlerTagType.GRUDGE: case BattlerTagType.GRUDGE:
return new GrudgeTag(); return new GrudgeTag();
case BattlerTagType.PSYCHO_SHIFT:
return new PsychoShiftTag();
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -2270,24 +2270,26 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
super(false, { trigger: MoveEffectTrigger.HIT }); 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); const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
if (target.status) { if (target.status) {
return false; return false;
} else { } else {
const canSetStatus = target.canSetStatus(statusToApply, true, false, user); const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
if (canSetStatus) { if (trySetStatus && user.status) {
if (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.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); user.addTag(BattlerTagType.PSYCHO_SHIFT);
}
user.resetStatus();
user.updateInfo();
target.trySetStatus(statusToApply, true, user);
} }
return canSetStatus; return trySetStatus;
} }
} }

View File

@ -90,5 +90,6 @@ export enum BattlerTagType {
ELECTRIFIED = "ELECTRIFIED", ELECTRIFIED = "ELECTRIFIED",
TELEKINESIS = "TELEKINESIS", TELEKINESIS = "TELEKINESIS",
COMMANDED = "COMMANDED", COMMANDED = "COMMANDED",
GRUDGE = "GRUDGE" GRUDGE = "GRUDGE",
PSYCHO_SHIFT = "PSYCHO_SHIFT",
} }

View File

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

View File

@ -94,16 +94,4 @@ describe("Abilities - Synchronize", () => {
expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); 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");
});
}); });

View File

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