diff --git a/src/data/ability.ts b/src/data/ability.ts index 6ffdc1f5403..dc120ee9184 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3282,6 +3282,32 @@ export class StatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr */ export class UserFieldStatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { } +/** + * Conditionally provides immunity to status effects to the user's field. + * + * Used by {@linkcode Abilities.FLOWER_VEIL | Flower Veil}. + * @extends UserFieldStatusEffectImmunityAbAttr + * + */ +export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldStatusEffectImmunityAbAttr { + protected condition: (target: Pokemon, source: Pokemon | null) => boolean; + + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { + if (cancelled.value || !this.condition(pokemon, args[1] as Pokemon)) { + return false; + } + + return super.apply(pokemon, passive, simulated, cancelled, args); + } + + constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) { + super(...immuneEffects); + + this.condition = condition; + } +} + + export class PreApplyBattlerTagAbAttr extends AbAttr { canApplyPreApplyBattlerTag( pokemon: Pokemon, @@ -3348,6 +3374,24 @@ export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { */ export class UserFieldBattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { } +export class ConditionalUserFieldBattlerTagImmunityAbAttr extends UserFieldBattlerTagImmunityAbAttr { + private condition: (target: Pokemon, source: Pokemon | null) => boolean; + + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { + if (cancelled.value || !this.condition(pokemon, args[1] as Pokemon)) { + return false; + } + + return super.apply(pokemon, passive, simulated, cancelled, args); + } + + constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, immuneTagTypes: BattlerTagType | BattlerTagType[]) { + super(immuneTagTypes); + + this.condition = condition; + } +} + export class BlockCritAbAttr extends AbAttr { constructor() { super(false); @@ -6670,6 +6714,16 @@ export function initAbilities() { .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]) .ignorable(), new Ability(Abilities.FLOWER_VEIL, 6) + .attr(ConditionalUserFieldStatusEffectImmunityAbAttr, (target: Pokemon, source: Pokemon | null) => { + return source ? target.getTypes().includes(Type.GRASS) && target.id !== source.id : false; + }) + .attr(ConditionalUserFieldBattlerTagImmunityAbAttr, + (target: Pokemon, source: Pokemon | null) => { + return source ? target.getTypes().includes(Type.GRASS) && target.id !== source.id : false; + }, + [ BattlerTagType.DROWSY ], + + ) .ignorable() .unimplemented(), new Ability(Abilities.CHEEK_POUCH, 6) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index aeab8a6490b..6148c72bf86 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8692,7 +8692,7 @@ export function initMoves() { new SelfStatusMove(Moves.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true) .attr(HealAttr, 1, true) - .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true)) + .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user)) .triageMove(), new AttackMove(Moves.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1) .attr(FlinchAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index aba13b2e51b..05ea2ef4889 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5456,7 +5456,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { pokemon, effect, cancelled, - quiet, + quiet, this, sourcePokemon, ), ); diff --git a/test/abilities/flower_veil.test.ts b/test/abilities/flower_veil.test.ts new file mode 100644 index 00000000000..aa0b0e35e14 --- /dev/null +++ b/test/abilities/flower_veil.test.ts @@ -0,0 +1,86 @@ +import { BattlerIndex } from "#app/battle"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Flower 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); + game.override + .moveset([ Moves.SPLASH ]) + .enemySpecies(Species.BULBASAUR) + .ability(Abilities.FLOWER_VEIL) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should not prevent any source of self-inflicted status conditions", async () => { + game.override.enemyMoveset([ Moves.TACKLE, Moves.SPLASH ]) + .ability(Abilities.FLOWER_VEIL) + .moveset([ Moves.REST, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const user = game.scene.getPlayerPokemon()!; + game.move.select(Moves.REST); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(user.status?.effect).toBe(StatusEffect.SLEEP); + + game.scene.addModifier(modifierTypes.FLAME_ORB().newModifier(user)); + game.scene.updateModifiers(true); + // remove sleep status + user.resetStatus(); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(user.status?.effect).toBe(StatusEffect.BURN); + + game.scene.addModifier(modifierTypes.TOXIC_ORB().newModifier(user)); + game.scene.updateModifiers(true); + user.resetStatus(); + + }); + + it("should prevent the drops while retaining the boosts from spicy extract", async () => { + game.override.enemyMoveset([ Moves.SPICY_EXTRACT ]) + .moveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const user = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(user.getStatStage(Stat.ATK)).toBe(2); + expect(user.getStatStage(Stat.DEF)).toBe(0); + }); + + it("should not prevent self-inflicted stat drops from moves like Close Combat", async () => { + game.override.moveset([ Moves.CLOSE_COMBAT ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const enemy = game.scene.getEnemyPokemon()!; + game.move.select(Moves.CLOSE_COMBAT); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.DEF)).toBe(-1); + }); +});