diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index bb54c2303f3..f258314157b 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -3627,8 +3627,6 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { export interface ConfusionOnStatusEffectAbAttrParams extends AbAttrBaseParams { /** The status effect that was applied */ effect: StatusEffect; - /** The move that applied the status effect */ - move: Move; /** The opponent that was inflicted with the status effect */ opponent: Pokemon; } @@ -3657,9 +3655,9 @@ export class ConfusionOnStatusEffectAbAttr extends AbAttr { /** * Applies confusion to the target pokemon. */ - override apply({ opponent, simulated, pokemon, move }: ConfusionOnStatusEffectAbAttrParams): void { + override apply({ opponent, simulated, pokemon }: ConfusionOnStatusEffectAbAttrParams): void { if (!simulated) { - opponent.addTag(BattlerTagType.CONFUSED, pokemon.randBattleSeedIntRange(2, 5), move.id, opponent.id); + opponent.addTag(BattlerTagType.CONFUSED, pokemon.randBattleSeedIntRange(2, 5), undefined, opponent.id); } } } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 67e07ac47f6..5d6b77797a8 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -992,7 +992,7 @@ class ToxicSpikesTag extends EntryHazardTag { // Attempt to poison the target, suppressing any status effect messages const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC; - return pokemon.trySetStatus(effect, null, 0, this.getMoveName(), false, true); + return pokemon.trySetStatus(effect, undefined, 0, this.getMoveName(), false, true); } getMatchupScoreMultiplier(pokemon: Pokemon): number { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 8b5e80792bd..1073473b48f 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2955,14 +2955,9 @@ export class StatusEffectAttr extends MoveEffectAttr { return false; } - // non-status moves don't play sound effects for failures const quiet = move.category !== MoveCategory.STATUS; - if (target.trySetStatus(this.effect, user, undefined, null, false, quiet)) { - applyAbAttrs("ConfusionOnStatusEffectAbAttr", { pokemon: user, opponent: target, move, effect: this.effect }); - return true; - } - return false; + return target.trySetStatus(this.effect, user, undefined, null, false, quiet); } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 781c5508409..075b25bd62e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4863,7 +4863,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { */ public trySetStatus( effect: StatusEffect, - sourcePokemon: Pokemon | null = null, + sourcePokemon?: Pokemon, sleepTurnsRemaining?: number, sourceText: string | null = null, overrideStatus?: boolean, diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index b9f3e266d87..8b66134cd78 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -13,6 +13,11 @@ import { PokemonPhase } from "#phases/pokemon-phase"; export class ObtainStatusEffectPhase extends PokemonPhase { public readonly phaseName = "ObtainStatusEffectPhase"; + private readonly statusEffect: StatusEffect; + private readonly sourcePokemon?: Pokemon; + private readonly sleepTurnsRemaining?: number; + private readonly statusMessage: string; + /** * @param battlerIndex - The {@linkcode BattlerIndex} of the Pokemon obtaining the status effect. * @param statusEffect - The {@linkcode StatusEffect} being applied. @@ -27,19 +32,20 @@ export class ObtainStatusEffectPhase extends PokemonPhase { */ constructor( battlerIndex: BattlerIndex, - private statusEffect: StatusEffect, - private sourcePokemon: Pokemon | null = null, - private sleepTurnsRemaining?: number, - sourceText: string | null = null, // TODO: This should take `undefined` instead of `null` - private statusMessage = "", + statusEffect: StatusEffect, + sourcePokemon?: Pokemon, + sleepTurnsRemaining?: number, + sourceText: string | null = null, // TODO: this should be `sourceText?: string`, and then remove `?? undefined` below + statusMessage?: string, ) { super(battlerIndex); - this.statusMessage ||= getStatusEffectObtainText( - statusEffect, - getPokemonNameWithAffix(this.getPokemon()), - sourceText ?? undefined, - ); + this.statusEffect = statusEffect; + this.sourcePokemon = sourcePokemon; + this.sleepTurnsRemaining = sleepTurnsRemaining; + this.statusMessage = + statusMessage + || getStatusEffectObtainText(statusEffect, getPokemonNameWithAffix(this.getPokemon()), sourceText ?? undefined); } start() { @@ -58,8 +64,15 @@ export class ObtainStatusEffectPhase extends PokemonPhase { applyAbAttrs("PostSetStatusAbAttr", { pokemon, effect: this.statusEffect, - sourcePokemon: this.sourcePokemon ?? undefined, + sourcePokemon: this.sourcePokemon, }); + if (this.sourcePokemon) { + applyAbAttrs("ConfusionOnStatusEffectAbAttr", { + pokemon: this.sourcePokemon, + opponent: pokemon, + effect: this.statusEffect, + }); + } } this.end(); }); diff --git a/test/abilities/poison-puppeteer.test.ts b/test/abilities/poison-puppeteer.test.ts new file mode 100644 index 00000000000..d17fc1873a4 --- /dev/null +++ b/test/abilities/poison-puppeteer.test.ts @@ -0,0 +1,112 @@ +import { allAbilities } from "#data/data-lists"; +import { AbilityId } from "#enums/ability-id"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Poison Puppeteer", () => { + 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 + .ability(AbilityId.POISON_PUPPETEER) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyLevel(100) + .enemyMoveset(MoveId.SPLASH); + }); + + it("should confuse the target if the user poisons the target directly", async () => { + await game.classicMode.startBattle([SpeciesId.MAREANIE]); + + game.move.use(MoveId.MORTAL_SPIN); + await game.toEndOfTurn(); + + const enemyPokemon = game.field.getEnemyPokemon(); + expect(enemyPokemon).toHaveStatusEffect(StatusEffect.POISON); + expect(enemyPokemon).toHaveBattlerTag(BattlerTagType.CONFUSED); + }); + + it("should confuse the target if the user badly poisons the target directly", async () => { + await game.classicMode.startBattle([SpeciesId.MAREANIE]); + + game.move.use(MoveId.TOXIC); + await game.toEndOfTurn(); + + const enemyPokemon = game.field.getEnemyPokemon(); + expect(enemyPokemon).toHaveStatusEffect(StatusEffect.TOXIC); + expect(enemyPokemon).toHaveBattlerTag(BattlerTagType.CONFUSED); + }); + + it("should not confuse the target if the user poisons the target via Toxic Spikes", async () => { + game.override.startingWave(5); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.TOXIC_SPIKES); + await game.toNextTurn(); + + game.move.use(MoveId.SPLASH); + game.forceEnemyToSwitch(); + await game.toEndOfTurn(); + + const enemyPokemon = game.field.getEnemyPokemon(); + expect(enemyPokemon).toHaveStatusEffect(StatusEffect.POISON); + expect(enemyPokemon).not.toHaveBattlerTag(BattlerTagType.CONFUSED); + }); + + it("should not confuse the target if the user paralyzes the target", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.NUZZLE); + await game.toEndOfTurn(); + + const enemyPokemon = game.field.getEnemyPokemon(); + expect(enemyPokemon).toHaveStatusEffect(StatusEffect.PARALYSIS); + expect(enemyPokemon).not.toHaveBattlerTag(BattlerTagType.CONFUSED); + }); + + it("should confuse the target if the target was poisoned due to Synchronize", async () => { + game.override.passiveAbility(AbilityId.SYNCHRONIZE).enemyAbility(AbilityId.NO_GUARD); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.POISON_POWDER); + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon()).toHaveStatusEffect(StatusEffect.POISON); + expect(game.field.getEnemyPokemon()).toHaveBattlerTag(BattlerTagType.CONFUSED); + }); + + it("should confuse the target if the target was poisoned due to Toxic Chain", async () => { + game.override.passiveAbility(AbilityId.TOXIC_CHAIN); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const toxicChainAttr = allAbilities[AbilityId.TOXIC_CHAIN].getAttrs("PostAttackApplyStatusEffectAbAttr")[0]; + // @ts-expect-error: `chance` is private + vi.spyOn(toxicChainAttr, "chance", "get").mockReturnValue(100); + + game.move.use(MoveId.TACKLE); + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon()).toHaveStatusEffect(StatusEffect.TOXIC); + expect(game.field.getEnemyPokemon()).toHaveBattlerTag(BattlerTagType.CONFUSED); + }); +});