From 5b4a24824fe0fdc2c5c9447259e59da76bb0767b Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:06:31 +0800 Subject: [PATCH] [Ability] Fully implement Pastel Veil, update Sweet Veil (after beta fix) (#3208) * ful implement pastel veil, update sweet veil * improve docs * update docs * cleanup attrs --- src/data/ability.ts | 101 +++++++++++++++++++++-- src/field/pokemon.ts | 17 +++- src/test/abilities/pastel_veil.test.ts | 79 ++++++++++++++++++ src/test/abilities/sweet_veil.test.ts | 108 +++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 src/test/abilities/pastel_veil.test.ts create mode 100644 src/test/abilities/sweet_veil.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index fe3a0e9478f..8f01e98e2be 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,4 +1,4 @@ -import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; +import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; @@ -2147,6 +2147,49 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { } } +/** + * Removes supplied status effects from the user's field. + */ +export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAttr { + private statusEffect: StatusEffect[]; + + /** + * @param statusEffect - The status effects to be removed from the user's field. + */ + constructor(...statusEffect: StatusEffect[]) { + super(false); + + this.statusEffect = statusEffect; + } + + /** + * Removes supplied status effect from the user's field when user of the ability is summoned. + * + * @param pokemon - The Pokémon that triggered the ability. + * @param passive - n/a + * @param args - n/a + * @returns A boolean or a promise that resolves to a boolean indicating the result of the ability application. + */ + applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { + const party = pokemon instanceof PlayerPokemon ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField(); + const allowedParty = party.filter(p => p.isAllowedInBattle()); + + if (allowedParty.length < 1) { + return false; + } + + for (const pokemon of allowedParty) { + if (this.statusEffect.includes(pokemon.status?.effect)) { + pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); + pokemon.resetStatus(false); + pokemon.updateInfo(); + } + } + + return true; + } +} + /** Attempt to copy the stat changes on an ally pokemon */ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { @@ -2398,17 +2441,33 @@ export class PreSetStatusAbAttr extends AbAttr { } } -export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr { +/** + * Provides immunity to status effects to specified targets. + */ +export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { private immuneEffects: StatusEffect[]; + /** + * @param immuneEffects - The status effects to which the Pokémon is immune. + */ constructor(...immuneEffects: StatusEffect[]) { super(); this.immuneEffects = immuneEffects; } + /** + * Applies immunity to supplied status effects. + * + * @param pokemon - The Pokémon to which the status is being applied. + * @param passive - n/a + * @param effect - The status effect being applied. + * @param cancelled - A holder for a boolean value indicating if the status application was cancelled. + * @param args - n/a + * @returns A boolean indicating the result of the status application. + */ applyPreSetStatus(pokemon: Pokemon, passive: boolean, effect: StatusEffect, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (!this.immuneEffects.length || this.immuneEffects.indexOf(effect) > -1) { + if (this.immuneEffects.length < 1 || this.immuneEffects.includes(effect)) { cancelled.value = true; return true; } @@ -2430,13 +2489,28 @@ export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr { } } +/** + * Provides immunity to status effects to the user. + * @extends PreSetStatusEffectImmunityAbAttr + */ +export class StatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { } + +/** + * Provides immunity to status effects to the user's field. + * @extends PreSetStatusEffectImmunityAbAttr + */ +export class UserFieldStatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { } + export class PreApplyBattlerTagAbAttr extends AbAttr { applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { return false; } } -export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { +/** + * Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets. + */ +export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { private immuneTagType: BattlerTagType; constructor(immuneTagType: BattlerTagType) { @@ -2463,6 +2537,18 @@ export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { } } +/** + * Provides immunity to BattlerTags {@linkcode BattlerTag} to the user. + * @extends PreApplyBattlerTagImmunityAbAttr + */ +export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { } + +/** + * Provides immunity to BattlerTags {@linkcode BattlerTag} to the user's field. + * @extends PreApplyBattlerTagImmunityAbAttr + */ +export class UserFieldBattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { } + export class BlockCritAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { (args[0] as Utils.BooleanHolder).value = true; @@ -4722,10 +4808,10 @@ export function initAbilities() { new Ability(Abilities.REFRIGERATE, 6) .attr(MoveTypeChangeAttr, Type.ICE, 1.2, (user, target, move) => move.type === Type.NORMAL), new Ability(Abilities.SWEET_VEIL, 6) - .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) + .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .ignorable() - .partial(), + .partial(), // Mold Breaker ally should not be affected by Sweet Veil new Ability(Abilities.STANCE_CHANGE, 6) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) @@ -5020,7 +5106,8 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonNeutralizingGas", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .partial(), new Ability(Abilities.PASTEL_VEIL, 8) - .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) + .attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) + .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .ignorable(), new Ability(Abilities.HUNGER_SWITCH, 8) .attr(PostTurnFormChangeAbAttr, p => p.getFormKey ? 0 : 1) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9b586d235c6..b4c6598efb5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr } from "../data/ability"; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1779,6 +1779,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1]; } + /** + * Gets the Pokémon on the allied field. + * + * @returns An array of Pokémon on the allied field. + */ + getAlliedField(): Pokemon[] { + return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); + } + /** * Calculates the accuracy multiplier of the user against a target. * @@ -2236,6 +2245,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const cancelled = new Utils.BooleanHolder(false); applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled); + const userField = this.getAlliedField(); + userField.forEach(pokemon => applyPreApplyBattlerTagAbAttrs(UserFieldBattlerTagImmunityAbAttr, pokemon, newTag, cancelled)); + if (!cancelled.value && newTag.canAdd(this)) { this.summonData.tags.push(newTag); newTag.onAdd(this); @@ -2644,6 +2656,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const cancelled = new Utils.BooleanHolder(false); applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet); + const userField = this.getAlliedField(); + userField.forEach(pokemon => applyPreSetStatusAbAttrs(UserFieldStatusEffectImmunityAbAttr, pokemon, effect, cancelled, quiet)); + if (cancelled.value) { return false; } diff --git a/src/test/abilities/pastel_veil.test.ts b/src/test/abilities/pastel_veil.test.ts new file mode 100644 index 00000000000..f19b395677f --- /dev/null +++ b/src/test/abilities/pastel_veil.test.ts @@ -0,0 +1,79 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { + CommandPhase, + TurnEndPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { StatusEffect } from "#app/data/status-effect.js"; +import { allAbilities } from "#app/data/ability.js"; +import { Abilities } from "#app/enums/abilities.js"; +import { BattlerIndex } from "#app/battle.js"; + +describe("Abilities - Pastel Veil", () => { + 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, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("double"); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TOXIC_THREAD, Moves.TOXIC_THREAD, Moves.TOXIC_THREAD, Moves.TOXIC_THREAD]); + }); + + it("prevents the user and its allies from being afflicted by poison", async () => { + await game.startBattle([Species.GALAR_PONYTA, Species.MAGIKARP]); + const ponyta = game.scene.getPlayerField()[0]; + + vi.spyOn(ponyta, "getAbility").mockReturnValue(allAbilities[Abilities.PASTEL_VEIL]); + + expect(ponyta.hasAbility(Abilities.PASTEL_VEIL)).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false); + }); + + it("it heals the poisoned status condition of allies if user is sent out into battle", async () => { + await game.startBattle([Species.MAGIKARP, Species.MAGIKARP, Species.GALAR_PONYTA]); + const ponyta = game.scene.getParty().find(p => p.species.speciesId === Species.GALAR_PONYTA); + + vi.spyOn(ponyta, "getAbility").mockReturnValue(allAbilities[Abilities.PASTEL_VEIL]); + + expect(ponyta.hasAbility(Abilities.PASTEL_VEIL)).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + expect(game.scene.getPlayerField().some(p => p.status?.effect === StatusEffect.POISON)).toBe(true); + + const poisonedMon = game.scene.getPlayerField().find(p => p.status?.effect === StatusEffect.POISON); + + await game.phaseInterceptor.to(CommandPhase); + game.doAttack(getMovePosition(game.scene, (poisonedMon.getBattlerIndex() as BattlerIndex.PLAYER | BattlerIndex.PLAYER_2), Moves.SPLASH)); + game.doSwitchPokemon(2); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false); + }); +}); diff --git a/src/test/abilities/sweet_veil.test.ts b/src/test/abilities/sweet_veil.test.ts new file mode 100644 index 00000000000..5a8022958ad --- /dev/null +++ b/src/test/abilities/sweet_veil.test.ts @@ -0,0 +1,108 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { + CommandPhase, + MoveEffectPhase, + MovePhase, + TurnEndPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { BattlerTagType } from "#app/enums/battler-tag-type.js"; +import { Abilities } from "#app/enums/abilities.js"; +import { BattlerIndex } from "#app/battle.js"; + +describe("Abilities - Sweet Veil", () => { + 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, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("double"); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.REST]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.POWDER, Moves.POWDER, Moves.POWDER, Moves.POWDER]); + }); + + it("prevents the user and its allies from falling asleep", async () => { + await game.startBattle([Species.SWIRLIX, Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false); + }); + + it("causes Rest to fail when used by the user or its allies", async () => { + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + await game.startBattle([Species.SWIRLIX, Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.REST)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false); + }); + + it("causes Yawn to fail if used on the user or its allies", async () => { + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.YAWN, Moves.YAWN, Moves.YAWN, Moves.YAWN]); + await game.startBattle([Species.SWIRLIX, Species.MAGIKARP]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.scene.getPlayerField().every(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(false); + }); + + it("prevents the user and its allies already drowsy due to Yawn from falling asleep.", async () => { + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.PIKACHU); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(5); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(5); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.YAWN, Moves.YAWN, Moves.YAWN, Moves.YAWN]); + + await game.startBattle([Species.SHUCKLE, Species.SHUCKLE, Species.SWIRLIX]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + // First pokemon move + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValueOnce(true); + + // Second pokemon move + await game.phaseInterceptor.to(MovePhase, false); + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValueOnce(true); + + expect(game.scene.getPlayerField().some(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + const drowsyMon = game.scene.getPlayerField().find(p => !!p.getTag(BattlerTagType.DROWSY)); + + await game.phaseInterceptor.to(CommandPhase); + game.doAttack(getMovePosition(game.scene, (drowsyMon.getBattlerIndex() as BattlerIndex.PLAYER | BattlerIndex.PLAYER_2), Moves.SPLASH)); + game.doSwitchPokemon(2); + + expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false); + }); +});