diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 26ffa0c0656..db69e61ef53 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1966,11 +1966,8 @@ export class HealAttr extends MoveEffectAttr { } // TODO: Change post move failure rework - override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { - if (!super.canApply(user, target, _move, _args)) { - return false; - } - + override getCondition(): MoveConditionFunc { + return (user, target) => { const healedPokemon = this.selfTarget ? user : target; if (healedPokemon.isFullHp()) { globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", { @@ -2019,8 +2016,8 @@ export class HealOnAllyAttr extends HealAttr { return false; } - override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { - return user.getAlly() !== target || super.canApply(user, target, _move, _args); + override getCondition(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { + return user.getAlly() !== target || super.getCondition()(user, target, _move); } } diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 0a6daaa209c..9fb21fc5507 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -2,6 +2,7 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { getEnumValues, toReadableString } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; @@ -58,7 +59,7 @@ describe("Moves - Recovery Moves - ", () => { expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); }); - it("should display message without adding phase if used at full HP", async () => { + it("should fail if used at full HP", async () => { await game.classicMode.startBattle([SpeciesId.BLISSEY]); game.move.use(move); @@ -72,6 +73,7 @@ describe("Moves - Recovery Moves - ", () => { }), ); expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); }); diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index 0b65d36f82a..7fa52b8f2f7 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -1,13 +1,11 @@ -import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { AbilityId } from "#enums/ability-id"; describe("Moves - Roost", () => { let phaserGame: Phaser.Game; @@ -28,221 +26,109 @@ describe("Moves - Roost", () => { game.override .battleStyle("single") .enemySpecies(SpeciesId.RELICANTH) + .ability(AbilityId.TRIAGE) .startingLevel(100) .enemyLevel(100) - .enemyMoveset(MoveId.EARTHQUAKE) - .moveset([MoveId.ROOST, MoveId.BURN_UP, MoveId.DOUBLE_SHOCK]); + .enemyMoveset(MoveId.SPLASH) }); - /** - * Roost's behavior should be defined as: - * The pokemon loses its flying type for a turn. If the pokemon was ungroundd solely due to being a flying type, it will be grounded until end of turn. - * 1. Pure Flying type pokemon -> become normal type until end of turn - * 2. Dual Flying/X type pokemon -> become type X until end of turn - * 3. Pokemon that use burn up into roost (ex. Moltres) -> become flying due to burn up, then typeless until end of turn after using roost - * 4. If a pokemon is afflicted with Forest's Curse or Trick or treat, dual type pokemon will become 3 type pokemon after the flying type is regained - * Pure flying types become (Grass or Ghost) and then back to flying/ (Grass or Ghost), - * and pokemon post Burn up become () - * 5. If a pokemon is also ungrounded due to other reasons (such as levitate), it will stay ungrounded post roost, despite not being flying type. - * 6. Non flying types using roost (such as dunsparce) are already grounded, so this move will only heal and have no other effects. - */ - - // TODO: This test currently is meaningless since we don't fail ineffective moves - it.todo("should still ground the user without failing when used at full HP"); - - test("Non flying type uses roost -> no type change, took damage", async () => { - await game.classicMode.startBattle([SpeciesId.DUNSPARCE]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be normal type, and NOT flying type - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Lose HP, still normal type - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - }); - - test("Pure flying type -> becomes normal after roost and takes damage from ground moves -> regains flying", async () => { - await game.classicMode.startBattle([SpeciesId.TORNADUS]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be normal type, and NOT flying type - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeTruthy(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeFalsy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Should have lost HP and is now back to being pure flying - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeFalsy(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); - }); - - test("Dual X/flying type -> becomes type X after roost and takes damage from ground moves -> regains flying", async () => { + it("should remove the user's Flying type until end of turn", async () => { await game.classicMode.startBattle([SpeciesId.HAWLUCHA]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - // Should only be pure fighting type and grounded - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FIGHTING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); + const hawlucha = game.field.getPlayerPokemon(); + hawlucha.hp = 1; - await game.phaseInterceptor.to(TurnEndPhase); + game.move.use(MoveId.ROOST); + await game.phaseInterceptor.to("MoveEffectPhase"); - // Should have lost HP and is now back to being fighting/flying - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.FIGHTING).toBeTruthy(); - expect(playerPokemonTypes[1] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + + // Should lose flying type temporarily + expect(hawlucha.getTag(BattlerTagType.ROOSTED)).toBeDefined(); + expect(hawlucha.getTypes()).toEqual([PokemonType.FIGHTING]) + expect(hawlucha.isGrounded()).toBe(true); + + await game.toEndOfTurn(); + + // Should have changed back to fighting/flying + expect(hawlucha.getTypes()).toEqual([PokemonType.FIGHTING, PokemonType.FLYING]) + expect(hawlucha.isGrounded()).toBe(false); }); - test("Pokemon with levitate after using roost should lose flying type but still be unaffected by ground moves", async () => { - game.override.starterForms({ [SpeciesId.ROTOM]: 4 }); - await game.classicMode.startBattle([SpeciesId.ROTOM]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); + it("should preserve types of non-Flying type Pokemon", async () => { + await game.classicMode.startBattle([SpeciesId.MEW]); - // Should only be pure eletric type and grounded - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.ELECTRIC).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + const mew = game.field.getPlayerPokemon(); + mew.hp = 1; - await game.phaseInterceptor.to(TurnEndPhase); + game.move.use(MoveId.ROOST); + await game.toEndOfTurn(false); - // Should have lost HP and is now back to being electric/flying - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBe(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.ELECTRIC).toBeTruthy(); - expect(playerPokemonTypes[1] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + // Should remain psychic type + expect(mew.getTypes()).toEqual([PokemonType.PSYCHIC]); + expect(mew.isGrounded()).toBe(true); }); - test("A fire/flying type that uses burn up, then roost should be typeless until end of turn", async () => { + it("should convert pure Flying types into normal types", async () => { + await game.classicMode.startBattle([SpeciesId.TORNADUS]); + + const tornadus = game.field.getPlayerPokemon(); + tornadus.hp = 1; + + game.move.use(MoveId.ROOST); + await game.toEndOfTurn(true); + + // Should only be normal type, and NOT flying type + expect(tornadus.getTypes()).toEqual([PokemonType.NORMAL]); + expect(tornadus.isGrounded()).toBe(true); + }); + + it.each<{ name: string, move: MoveId, species: SpeciesId }>([ + { name: "Burn Up", move: MoveId.BURN_UP, species: SpeciesId.MOLTRES }, + { name: "Double Shock", move: MoveId.DOUBLE_SHOCK, species: SpeciesId.ZAPDOS } + ])("should render user typeless when roosting after using $name", async ({ move, species }) => { + await game.classicMode.startBattle([species]); + + const player = game.field.getPlayerPokemon()!; + player.hp = 1; + + game.move.use(move); + await game.toNextTurn(); + + // Should be pure flying type + expect(player.getTypes()).toEqual([PokemonType.FLYING]); + expect(player.isGrounded()).toBe(false); + + game.move.use(move); + await game.phaseInterceptor.to("MoveEffectPhase"); + + // Should be typeless + expect(player.getTag(BattlerTagType.ROOSTED)).toBeDefined(); + expect(player.getTypes()).toEqual([PokemonType.UNKNOWN]); + expect(player.isGrounded()).toBe(true); + + await game.toEndOfTurn(); + + // Should go back to being pure flying + expect(player.getTypes()).toEqual([PokemonType.FLYING]); + expect(player.isGrounded()).toBe(false); + }); + + it("should revert to 3 types when affected by Forest's Curse or Trick-or-Treat", async () => { await game.classicMode.startBattle([SpeciesId.MOLTRES]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.BURN_UP); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - // Should only be pure flying type after burn up - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); + const moltres = game.field.getPlayerPokemon(); + moltres.hp = 1; - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); + game.move.use(MoveId.ROOST); + await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT) + await game.toEndOfTurn(false); - // Should only be typeless type after roost and is grounded - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.getTag(BattlerTagType.ROOSTED)).toBeDefined(); - expect(playerPokemonTypes[0] === PokemonType.UNKNOWN).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); + expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.GHOST]); + expect(moltres.isGrounded()).toBe(true); - await game.phaseInterceptor.to(TurnEndPhase); + await game.toEndOfTurn(); - // Should go back to being pure flying and have taken damage from earthquake, and is ungrounded again - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); - }); - - test("An electric/flying type that uses double shock, then roost should be typeless until end of turn", async () => { - game.override.enemySpecies(SpeciesId.ZEKROM); - await game.classicMode.startBattle([SpeciesId.ZAPDOS]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.DOUBLE_SHOCK); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be pure flying type after burn up - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be typeless type after roost and is grounded - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.getTag(BattlerTagType.ROOSTED)).toBeDefined(); - expect(playerPokemonTypes[0] === PokemonType.UNKNOWN).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Should go back to being pure flying and have taken damage from earthquake, and is ungrounded again - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); - }); - - test("Dual Type Pokemon afflicted with Forests Curse/Trick or Treat and post roost will become dual type and then become 3 type at end of turn", async () => { - game.override.enemyMoveset([ - MoveId.TRICK_OR_TREAT, - MoveId.TRICK_OR_TREAT, - MoveId.TRICK_OR_TREAT, - MoveId.TRICK_OR_TREAT, - ]); - await game.classicMode.startBattle([SpeciesId.MOLTRES]); - const playerPokemon = game.scene.getPlayerPokemon()!; - game.move.select(MoveId.ROOST); - await game.phaseInterceptor.to(MoveEffectPhase); - - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FIRE).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Should be fire/flying/ghost - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes.filter(type => type === PokemonType.FLYING)).toHaveLength(1); - expect(playerPokemonTypes.filter(type => type === PokemonType.FIRE)).toHaveLength(1); - expect(playerPokemonTypes.filter(type => type === PokemonType.GHOST)).toHaveLength(1); - expect(playerPokemonTypes.length === 3).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); + expect(moltres.isGrounded()).toBe(false); }); }); diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index b9f499d4e0c..334266d3bfa 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -363,9 +363,12 @@ export default class GameManager { console.log("==================[New Turn]=================="); } - /** Transition to the {@linkcode TurnEndPhase | end of the current turn}. */ - async toEndOfTurn() { - await this.phaseInterceptor.to("TurnEndPhase"); + /** + * Transition to the {@linkcode TurnEndPhase | end of the current turn}. + * @param endTurn - Whether to run the TurnEndPhase or not; default `true` + */ + async toEndOfTurn(endTurn = true) { + await this.phaseInterceptor.to("TurnEndPhase", endTurn); console.log("==================[End of Turn]=================="); }