Initial changes for Synchronize ability

This commit is contained in:
Yiling Kang 2024-07-01 02:29:39 -07:00
parent f4c8f0080a
commit 7067187532
3 changed files with 283 additions and 2 deletions

View File

@ -1623,6 +1623,68 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
}
}
/**
* Base class for defining all {@linkcode Ability} Attributes after a status effect has been set.
* @see {@linkcode applyPostSetStatus()}.
*/
export class PostSetStatusAbAttr extends AbAttr {
/**
* Does nothing after a status condition is set.
* @param pokemon {@linkcode Pokemon} that status condition was set on.
* @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon.
* @param passive Whether this ability is a passive.
* @param effect {@linkcode StatusEffect} that was set.
* @param args Set of unique arguments needed by this attribute.
* @returns true if application of the ability succeeds.
*/
applyPostSetStatus(
pokemon: Pokemon,
sourcePokemon: Pokemon = null,
passive: boolean,
effect: StatusEffect,
args: any[]) : boolean | Promise<boolean> {
return false;
}
}
/**
* If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon,
* that Pokemon receives the same non-volatile status condition as part of this
* ability attribute. For Synchronize ability.
*/
export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
/**
* If the StatusEffect that was set is Burn, Paralysis, Poison, or Toxic, and the status
* was set by a source Pokemon, set the source Pokemon's status to the same StatusEffect.
* @param pokemon {@linkcode Pokemon} that status condition was set on.
* @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon.
* @param passive Whether this ability is a passive.
* @param effect {@linkcode StatusEffect} that was set.
* @param args Set of unique arguments needed by this attribute.
* @returns true if application of the ability succeeds.
*/
applyPostSetStatus(
pokemon: Pokemon,
sourcePokemon: Pokemon = null,
passive: boolean,
effect: StatusEffect,
args: any[]): boolean {
// Synchronizable statuses
const syncStatuses = new Set<StatusEffect>([
StatusEffect.BURN,
StatusEffect.PARALYSIS,
StatusEffect.POISON,
StatusEffect.TOXIC
]);
if (sourcePokemon && syncStatuses.has(effect)) {
return sourcePokemon.trySetStatus(effect, true);
}
return false;
}
}
export class PostVictoryAbAttr extends AbAttr {
applyPostVictory(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
return false;
@ -4012,6 +4074,11 @@ export function applyPostMoveUsedAbAttrs(attrType: Constructor<PostMoveUsedAbAtt
return applyAbAttrsInternal<PostMoveUsedAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, args), args);
}
export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbAttr>,
pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, args), args);
}
export function applyBattleStatMultiplierAbAttrs(attrType: Constructor<BattleStatMultiplierAbAttr>,
pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<BattleStatMultiplierAbAttr>(attrType, pokemon, (attr, passive) => attr.applyBattleStat(pokemon, passive, battleStat, statValue, args), args);
@ -4239,7 +4306,8 @@ export function initAbilities() {
.attr(EffectSporeAbAttr),
new Ability(Abilities.SYNCHRONIZE, 3)
.attr(SyncEncounterNatureAbAttr)
.unimplemented(),
.attr(SynchronizeStatusAbAttr)
.partial(), // interaction with psycho shift needs work
new Ability(Abilities.CLEAR_BODY, 3)
.attr(ProtectStatAbAttr)
.ignorable(),

View File

@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag
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, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr } 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, AddSecondStrikeAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability";
import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui";
@ -2586,6 +2586,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (effect !== StatusEffect.FAINT) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
applyPostSetStatusAbAttrs(PostSetStatusAbAttr, this, effect, sourcePokemon);
}
return true;

View File

@ -0,0 +1,212 @@
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 {
MoveEffectPhase,
TurnEndPhase,
} from "#app/phases";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#app/data/status-effect.js";
describe("Abilities - Synchronize", () => {
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, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
vi.spyOn(overrides, "STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.NONE);
vi.spyOn(overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.NONE);
// Opponent mocks
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ALAKAZAM);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.PSYCHIC, Moves.CALM_MIND, Moves.FOCUS_BLAST, Moves.RECOVER]);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SYNCHRONIZE);
}, 20000);
it("does not trigger when no status is applied by opponent Pokemon", async () => {
// Arrange
const moveToUse = Moves.HEADBUTT;
// Starter mocks
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ZIGZAGOON);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.PICKUP);
// Act
await game.startBattle();
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status).toBe(undefined);
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}, 20000);
it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => {
// Arrange
const moveToUse = Moves.THUNDER_WAVE;
// Starter mocks
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.TOGEKISS);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SERENE_GRACE);
// Act
await game.startBattle();
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
it("sets the status of the source pokemon to Burned when burn is applied by it", async () => {
// Arrange
const moveToUse = Moves.WILL_O_WISP;
// Starter mocks
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.EEVEE);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.GUTS);
// Act
await game.startBattle();
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.BURN);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.BURN);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
it("sets the status of the source pokemon to Poisoned when poison is applied by it", async () => {
// Arrange
const moveToUse = Moves.POISON_POWDER;
// Starter mocks
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TECHNICIAN);
// Act
await game.startBattle();
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.POISON);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
it("sets the status of the source pokemon to Toxic when toxic is applied by it", async () => {
// Arrange
const moveToUse = Moves.TOXIC;
// Starter mocks
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.QUAGSIRE);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DAMP);
// Act
await game.startBattle();
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.TOXIC);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.TOXIC);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
it("does not trigger when Pokemon is statused to non Burn, Paralysis, Poison, or Toxic", async () => {
// Arrange
const moveToUse = Moves.SPORE;
// Starter mocks
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.POISON_HEAL);
// Act
await game.startBattle();
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(undefined);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP);
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}, 20000);
it("does not trigger when Pokemon is statused by Toxic Spikes", async () => {
// Arrange
const moveToUse = Moves.SPLASH;
// Starter mocks
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SYNCHRONIZE);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TOXIC_SPIKES]);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TECHNICIAN);
// Act
// Turn 1 - Opponent uses spikes, trainer uses splash
// Turn 2 - Opponent uses splash, trainer sends out Alakazam. Alakazam is toxic-ed but Synchronize should not proc
await game.startBattle([Species.MAGIKARP, Species.ALAKAZAM]);
const leadPokemon = game.scene.getPlayerPokemon();
expect(leadPokemon).not.toBe(undefined);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); // use splash
await game.toNextTurn();
game.doSwitchPokemon(1);
game.doAttack(0);
await game.phaseInterceptor.to(TurnEndPhase);
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(undefined);
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}, 20000);
});