[Ability] (Partially) Implement synchronize ability with old psycho shift interaction (#2746)
* Initial changes for Synchronize ability * Fix psycho shift interaction causing buggy behaviour * Update to show ability even if opponent pokemon does not get statused * Fix some spacing * Update tests * Formatting change * Remove impossible `if` statement * Add `simulated` support * Apply suggestions from code review Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Don't need those comments --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>
This commit is contained in:
parent
107a7497a9
commit
b9b2491f95
|
@ -1798,6 +1798,61 @@ 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 = null, passive: boolean, effect: StatusEffect, simulated: boolean, 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.
|
||||
*/
|
||||
override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]): boolean {
|
||||
/** Synchronizable statuses */
|
||||
const syncStatuses = new Set<StatusEffect>([
|
||||
StatusEffect.BURN,
|
||||
StatusEffect.PARALYSIS,
|
||||
StatusEffect.POISON,
|
||||
StatusEffect.TOXIC
|
||||
]);
|
||||
|
||||
if (sourcePokemon && syncStatuses.has(effect)) {
|
||||
if (!simulated) {
|
||||
sourcePokemon.trySetStatus(effect, true, pokemon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PostVictoryAbAttr extends AbAttr {
|
||||
applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
|
||||
return false;
|
||||
|
@ -4677,6 +4732,10 @@ export function applyStatMultiplierAbAttrs(attrType: Constructor<StatMultiplierA
|
|||
pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise<void> {
|
||||
return applyAbAttrsInternal<StatMultiplierAbAttr>(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args);
|
||||
}
|
||||
export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbAttr>,
|
||||
pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, simulated: boolean = false, ...args: any[]): Promise<void> {
|
||||
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a field Stat multiplier attribute
|
||||
|
@ -4907,7 +4966,8 @@ export function initAbilities() {
|
|||
.attr(EffectSporeAbAttr),
|
||||
new Ability(Abilities.SYNCHRONIZE, 3)
|
||||
.attr(SyncEncounterNatureAbAttr)
|
||||
.unimplemented(),
|
||||
.attr(SynchronizeStatusAbAttr)
|
||||
.partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now
|
||||
new Ability(Abilities.CLEAR_BODY, 3)
|
||||
.attr(ProtectStatAbAttr)
|
||||
.ignorable(),
|
||||
|
|
|
@ -2091,21 +2091,20 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
|||
|
||||
if (target.status) {
|
||||
return false;
|
||||
}
|
||||
//@ts-ignore - how can target.status.effect be checked when we return `false` before when it's defined?
|
||||
if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { // TODO: resolve ts-ignore
|
||||
const statusAfflictResult = target.trySetStatus(statusToApply, true, user);
|
||||
if (statusAfflictResult) {
|
||||
} else {
|
||||
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
|
||||
|
||||
if (canSetStatus) {
|
||||
if (user.status) {
|
||||
user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user)));
|
||||
}
|
||||
user.resetStatus();
|
||||
user.updateInfo();
|
||||
}
|
||||
return statusAfflictResult;
|
||||
target.trySetStatus(statusToApply, true, user);
|
||||
}
|
||||
|
||||
return false;
|
||||
return canSetStatus;
|
||||
}
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
|||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
|
||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability";
|
||||
import PokemonData from "../system/pokemon-data";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
|
@ -3365,7 +3365,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
if (asPhase) {
|
||||
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText!, sourcePokemon!)); // TODO: are these bangs correct?
|
||||
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -3399,6 +3399,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;
|
||||
|
|
|
@ -9,24 +9,24 @@ import { PokemonPhase } from "./pokemon-phase";
|
|||
import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase";
|
||||
|
||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
private statusEffect: StatusEffect | undefined;
|
||||
private cureTurn: integer | null;
|
||||
private sourceText: string | null;
|
||||
private sourcePokemon: Pokemon | null;
|
||||
private statusEffect?: StatusEffect | undefined;
|
||||
private cureTurn?: integer | null;
|
||||
private sourceText?: string | null;
|
||||
private sourcePokemon?: Pokemon | null;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string, sourcePokemon?: Pokemon) {
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
|
||||
super(scene, battlerIndex);
|
||||
|
||||
this.statusEffect = statusEffect;
|
||||
this.cureTurn = cureTurn!; // TODO: is this bang correct?
|
||||
this.sourceText = sourceText!; // TODO: is this bang correct?
|
||||
this.sourcePokemon = sourcePokemon!; // For tracking which Pokemon caused the status effect // TODO: is this bang correct?
|
||||
this.cureTurn = cureTurn;
|
||||
this.sourceText = sourceText;
|
||||
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect
|
||||
}
|
||||
|
||||
start() {
|
||||
const pokemon = this.getPokemon();
|
||||
if (!pokemon?.status) {
|
||||
if (pokemon?.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
||||
if (pokemon && !pokemon.status) {
|
||||
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
||||
if (this.cureTurn) {
|
||||
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
|||
});
|
||||
return;
|
||||
}
|
||||
} else if (pokemon.status.effect === this.statusEffect) {
|
||||
this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon)));
|
||||
} else if (pokemon.status?.effect === this.statusEffect) {
|
||||
this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)));
|
||||
}
|
||||
this.end();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import { StatusEffect } from "#app/data/status-effect";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
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);
|
||||
|
||||
game.override
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.SYNCHRONIZE)
|
||||
.moveset([Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT])
|
||||
.ability(Abilities.NO_GUARD);
|
||||
}, 20000);
|
||||
|
||||
it("does not trigger when no status is applied by opponent Pokemon", async () => {
|
||||
await game.classicMode.startBattle([Species.FEEBAS]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getParty()[0].status).toBeUndefined();
|
||||
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
|
||||
}, 20000);
|
||||
|
||||
it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => {
|
||||
await game.classicMode.startBattle([Species.FEEBAS]);
|
||||
|
||||
game.move.select(Moves.THUNDER_WAVE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
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("does not trigger on Sleep", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.SPORE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getParty()[0].status?.effect).toBeUndefined();
|
||||
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 () => {
|
||||
game.override
|
||||
.ability(Abilities.SYNCHRONIZE)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Array(4).fill(Moves.TOXIC_SPIKES));
|
||||
|
||||
await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON);
|
||||
expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined();
|
||||
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
|
||||
}, 20000);
|
||||
|
||||
it("shows ability even if it fails to set the status of the opponent Pokemon", async () => {
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
game.move.select(Moves.THUNDER_WAVE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getParty()[0].status?.effect).toBeUndefined();
|
||||
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
||||
}, 20000);
|
||||
|
||||
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.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise
|
||||
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
||||
}, 20000);
|
||||
});
|
Loading…
Reference in New Issue