From 06b6143931888b4580ad2f5e615f46cfa2cf355b Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 10 Jun 2025 16:51:11 -0400 Subject: [PATCH] Revamp telekinesis tests; fix up stuff --- src/data/arena-tag.ts | 10 +-- src/data/battler-tags.ts | 4 + src/data/moves/move.ts | 50 ++++++----- src/field/pokemon.ts | 2 +- test/arena/terrain.test.ts | 31 ++++--- test/moves/telekinesis.test.ts | 139 ++++++++++++++++------------- test/moves/thousand_arrows.test.ts | 70 ++++++++------- 7 files changed, 169 insertions(+), 137 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index e18ee5ac556..325ce0b7500 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1096,12 +1096,10 @@ export class GravityTag extends ArenaTag { onAdd(_arena: Arena): void { globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd")); globalScene.getField(true).forEach(pokemon => { - if (pokemon !== null) { - pokemon.removeTag(BattlerTagType.FLOATING); - pokemon.removeTag(BattlerTagType.TELEKINESIS); - if (pokemon.getTag(BattlerTagType.FLYING)) { - pokemon.addTag(BattlerTagType.INTERRUPTED); - } + pokemon.removeTag(BattlerTagType.FLOATING); + pokemon.removeTag(BattlerTagType.TELEKINESIS); + if (pokemon.getTag(BattlerTagType.FLYING)) { + pokemon.addTag(BattlerTagType.INTERRUPTED); } }); } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ffa179c6aab..d90964492e8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -660,6 +660,10 @@ export class FlinchedTag extends BattlerTag { } } +/** + * Tag to cancel the target's action when knocked out of a flying move by Smack Down or Gravity. + */ +// TODO: This is not a very good way to cancel a semi invulnerable turn export class InterruptedTag extends BattlerTag { constructor(sourceMove: MoveId) { super(BattlerTagType.INTERRUPTED, BattlerTagLapseType.PRE_MOVE, 0, sourceMove); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 573d3a96aab..ff92f5c34d8 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -5299,13 +5299,11 @@ export class VariableMoveTypeMultiplierAttr extends MoveAttr { } export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!target.getTag(BattlerTagType.IGNORE_FLYING)) { - const multiplier = args[0] as NumberHolder; - //When a flying type is hit, the first hit is always 1x multiplier. - if (target.isOfType(PokemonType.FLYING)) { - multiplier.value = 1; - } + apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean { + if (!target.isGrounded(true) && target.isOfType(PokemonType.FLYING)) { + const multiplier = args[0]; + // When a flying type is hit, the first hit is always 1x multiplier. + multiplier.value = 1; return true; } @@ -5634,8 +5632,8 @@ export class LeechSeedAttr extends AddBattlerTagAttr { } /** - * Adds the appropriate battler tag for Smack Down and Thousand arrows - * @extends AddBattlerTagAttr + * Attribute to add the {@linkcode BattlerTagType.IGNORE_FLYING | IGNORE_FLYING} battler tag to the target + * and remove any prior sources of ungroundedness. */ export class FallDownAttr extends AddBattlerTagAttr { constructor() { @@ -5644,19 +5642,29 @@ export class FallDownAttr extends AddBattlerTagAttr { /** * Adds Grounded Tag to the target and checks if fallDown message should be displayed - * @param user the {@linkcode Pokemon} using the move - * @param target the {@linkcode Pokemon} targeted by the move - * @param move the {@linkcode Move} invoking this effect + * @param user - The {@linkcode Pokemon} using the move + * @param target - The {@linkcode Pokemon} targeted by the move + * @param move - The {@linkcode Move} invoking this effect * @param args n/a - * @returns `true` if the effect successfully applies; `false` otherwise + * @returns Whether the target was successfully brought down to earth. */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - // Smack down and similar only add their tag if the pokemon is already ungrounded without considering semi-invulnerability - if (!target.isGrounded(true)) { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) })); - return super.apply(user, target, move, args); + // Smack down and similar only add their tag if the pokemon is already ungrounded + // TODO: Does this work if the target is semi-invulnerable? + if (target.isGrounded(true)) { + return false; } - return false; + + // Remove the target's flying/floating state and telekinesis, interrupting fly/bounce's activation. + // TODO: This is not a good way to remove flying... + target.removeTag(BattlerTagType.FLOATING); + target.removeTag(BattlerTagType.TELEKINESIS); + if (target.getTag(BattlerTagType.FLYING)) { + target.addTag(BattlerTagType.INTERRUPTED); + } + + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) })); + return super.apply(user, target, move, args); } } @@ -7975,6 +7983,7 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean return phase.isForcedLast() && slower; }; +// TODO: This needs to become unselectable, not merely fail const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); @@ -9280,6 +9289,7 @@ export function initMoves() { .attr(RandomMovesetMoveAttr, invalidAssistMoves, true), new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true) + // NB: We add the tag directly to avoid removing Telekinesis' accuracy boost .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true) .attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true), new AttackMove(MoveId.SUPERPOWER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3) @@ -9907,8 +9917,6 @@ export function initMoves() { .unimplemented(), new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5) .attr(FallDownAttr) - .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) - .attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ]) .attr(HitsTagAttr, BattlerTagType.FLYING) .makesContact(false) // TODO: Confirm if Smack Down & Thousand Arrows will ground semi-invulnerable Pokemon with No Guard, etc. @@ -10364,8 +10372,6 @@ export function initMoves() { .attr(FallDownAttr) .attr(HitsTagAttr, BattlerTagType.FLYING) .attr(HitsTagAttr, BattlerTagType.FLOATING) - .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) - .attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ]) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES) // TODO: Confirm if Smack Down & Thousand Arrows will ground semi-invulnerable Pokemon with No Guard, etc. diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 7c72a469156..6d522ebcf6b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2464,7 +2464,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Handle flying v ground type immunity without removing flying type so effective types are still effective // Related to https://github.com/pagefaultgames/pokerogue/issues/524 // - if (moveType === PokemonType.GROUND && this.isGrounded(true)) { + if (moveType === PokemonType.GROUND && this.isGrounded()) { const flyingIndex = types.indexOf(PokemonType.FLYING); if (flyingIndex > -1) { types.splice(flyingIndex, 1); diff --git a/test/arena/terrain.test.ts b/test/arena/terrain.test.ts index d0229b2baf2..f90e903976b 100644 --- a/test/arena/terrain.test.ts +++ b/test/arena/terrain.test.ts @@ -42,8 +42,8 @@ describe("Terrain -", () => { .disableCrits() .enemySpecies(SpeciesId.SNOM) .enemyAbility(AbilityId.STURDY) - .ability(AbilityId.NO_GUARD) - .passiveAbility(ability) + .ability(ability) + .passiveAbility(AbilityId.NO_GUARD) .enemyPassiveAbility(AbilityId.LEVITATE); }); @@ -55,27 +55,31 @@ describe("Terrain -", () => { const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower"); game.move.use(move); await game.move.forceEnemyMove(move); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toEndOfTurn(); - // Player grounded attack got boost while enemy ungrounded attack didn't - expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power); - expect(powerSpy).toHaveNthReturnedWith(2, allMoves[move].power * 1.3); + // Player grounded attack got boosted while enemy ungrounded attack didn't + expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 1.3); + expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power); }); it(`should change Terrain Pulse into a ${typeStr}-type move and double its base power`, async () => { await game.classicMode.startBattle([SpeciesId.BLISSEY]); - const blissey = game.field.getPlayerPokemon(); const powerSpy = vi.spyOn(allMoves[MoveId.TERRAIN_PULSE], "calculateBattlePower"); - const typeSpy = vi.spyOn(blissey, "getMoveType"); + const playerTypeSpy = vi.spyOn(game.field.getPlayerPokemon(), "getMoveType"); + const enemyTypeSpy = vi.spyOn(game.field.getEnemyPokemon(), "getMoveType"); - game.move.use(move); - await game.move.forceEnemyMove(MoveId.SPLASH); + game.move.use(MoveId.TERRAIN_PULSE); + await game.move.forceEnemyMove(MoveId.TERRAIN_PULSE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toEndOfTurn(); + // player grounded terrain pulse was boosted & type converted; enemy ungrounded one wasn't expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2.6); // 2 * 1.3 - expect(typeSpy).toHaveLastReturnedWith(type); + expect(playerTypeSpy).toHaveLastReturnedWith(type); + expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power); + expect(enemyTypeSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].type); }); }); @@ -169,7 +173,7 @@ describe("Terrain -", () => { ); }); - // TODO: Enable tests after terrain-msg branch is merged + // TODO: Enable suites after terrain-fail-msg branch is merged describe.skip("Electric Terrain", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); @@ -234,8 +238,7 @@ describe("Terrain -", () => { }); }); - // TODO: Enable tests after terrain-msg branch is merged - describe("Misty Terrain", () => { + describe.skip("Misty Terrain", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); }); diff --git a/test/moves/telekinesis.test.ts b/test/moves/telekinesis.test.ts index 5c9f1e22395..9aef84f86bf 100644 --- a/test/moves/telekinesis.test.ts +++ b/test/moves/telekinesis.test.ts @@ -1,5 +1,4 @@ import { BattlerTagType } from "#enums/battler-tag-type"; -import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -8,6 +7,9 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; import { BattlerIndex } from "#enums/battler-index"; +import type { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { HitCheckResult } from "#enums/hit-check-result"; +import { StatusEffect } from "#enums/status-effect"; describe("Moves - Telekinesis", () => { let phaserGame: Phaser.Game; @@ -26,114 +28,129 @@ describe("Moves - Telekinesis", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.TELEKINESIS, MoveId.TACKLE, MoveId.MUD_SHOT, MoveId.SMACK_DOWN]) .battleStyle("single") .enemySpecies(SpeciesId.SNORLAX) .enemyLevel(60) .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset([MoveId.SPLASH]); + .enemyMoveset(MoveId.SPLASH); }); - it("Telekinesis makes the affected vulnerable to most attacking moves regardless of accuracy", async () => { + it("should cause opposing non-OHKO moves to always hit the target", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const enemyOpponent = game.scene.getEnemyPokemon()!; - - game.move.select(MoveId.TELEKINESIS); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined(); + const player = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + game.move.use(MoveId.TELEKINESIS); await game.toNextTurn(); - vi.spyOn(allMoves[MoveId.TACKLE], "accuracy", "get").mockReturnValue(0); - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.isFullHp()).toBe(false); + + expect(enemy.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); + expect(enemy.getTag(BattlerTagType.FLOATING)).toBeDefined(); + + game.move.use(MoveId.TACKLE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.move.forceMiss(); + await game.toEndOfTurn(); + + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(player.getLastXMoves()[0].result).toBe(MoveResult.MISS); }); - it("Telekinesis makes the affected airborne and immune to most Ground-moves", async () => { + it("should render the target immune to Ground-moves and terrain", async () => { + game.override.ability(AbilityId.ELECTRIC_SURGE); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const enemyOpponent = game.scene.getEnemyPokemon()!; + const enemy = game.field.getEnemyPokemon(); - game.move.select(MoveId.TELEKINESIS); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined(); + game.move.use(MoveId.TELEKINESIS); + await game.toNextTurn(); + + game.move.use(MoveId.EARTHQUAKE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("MoveEffectPhase"); + const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck"); await game.toNextTurn(); - vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(100); - game.move.select(MoveId.MUD_SHOT); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.isFullHp()).toBe(true); + + expect(enemy.hp).toBe(enemy.getMaxHp()); + expect(hitSpy).toHaveLastReturnedWith([HitCheckResult.NO_EFFECT, 0]); + + game.move.use(MoveId.SPORE); + await game.toNextTurn(); + + expect(enemy.status?.effect).toBe(StatusEffect.SLEEP); }); - it("Telekinesis can still affect Pokemon that have been transformed into invalid Pokemon", async () => { - game.override.enemyMoveset(MoveId.TRANSFORM); + it("should still affect enemies transformed into invalid Pokemon", async () => { + game.override.enemyAbility(AbilityId.IMPOSTER); await game.classicMode.startBattle([SpeciesId.DIGLETT]); - const enemyOpponent = game.scene.getEnemyPokemon()!; + const enemyOpponent = game.field.getEnemyPokemon(); + + game.move.use(MoveId.TELEKINESIS); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toNextTurn(); - game.move.select(MoveId.TELEKINESIS); - await game.phaseInterceptor.to("TurnEndPhase"); expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined(); expect(enemyOpponent.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT); }); - it("Moves like Smack Down and 1000 Arrows remove all effects of Telekinesis from the target Pokemon", async () => { + it.each<{ name: string; move: MoveId }>([ + { name: "Smack Down", move: MoveId.SMACK_DOWN }, + { name: "Thousand Arrows", move: MoveId.THOUSAND_ARROWS }, + ])("should be removed when hit by $name", async ({ move }) => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const enemyOpponent = game.scene.getEnemyPokemon()!; + const enemy = game.field.getEnemyPokemon(); game.move.select(MoveId.TELEKINESIS); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined(); - await game.toNextTurn(); - game.move.select(MoveId.SMACK_DOWN); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined(); - expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined(); + + game.move.use(move); + await game.toNextTurn(); + expect(enemy.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined(); + expect(enemy.getTag(BattlerTagType.FLOATING)).toBeUndefined(); }); - it("Ingrain will remove the floating effect of Telekinesis, but not the 100% hit", async () => { - game.override.enemyMoveset([MoveId.SPLASH, MoveId.INGRAIN]); + it("should become grounded when Ingrain is used, but not remove the guaranteed hit effect", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const enemyOpponent = game.scene.getEnemyPokemon()!; - - game.move.select(MoveId.TELEKINESIS); - await game.move.selectEnemyMove(MoveId.SPLASH); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined(); + const playerPokemon = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + game.move.use(MoveId.TELEKINESIS); await game.toNextTurn(); - vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(0); - game.move.select(MoveId.MUD_SHOT); - await game.move.selectEnemyMove(MoveId.INGRAIN); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.INGRAIN)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); - expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined(); + + game.move.use(MoveId.MUD_SHOT); + await game.move.forceEnemyMove(MoveId.INGRAIN); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEndPhase"); + await game.move.forceMiss(); + await game.toNextTurn(); + + expect(enemy.getTag(BattlerTagType.TELEKINESIS)).toBeDefined(); + expect(enemy.getTag(BattlerTagType.INGRAIN)).toBeDefined(); + expect(enemy.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); + expect(enemy.getTag(BattlerTagType.FLOATING)).toBeUndefined(); + expect(enemy.isGrounded()).toBe(false); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); }); - it("should not be baton passed onto a mega gengar", async () => { + it("should not be baton passable onto a mega gengar", async () => { game.override .moveset([MoveId.BATON_PASS]) .enemyMoveset([MoveId.TELEKINESIS]) .starterForms({ [SpeciesId.GENGAR]: 1 }); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]); - game.move.select(MoveId.BATON_PASS); + + game.move.use(MoveId.BATON_PASS); game.doSelectPartyPokemon(1); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined(); + await game.toEndOfTurn(); + + expect(game.field.getPlayerPokemon().getTag(BattlerTagType.TELEKINESIS)).toBeUndefined(); }); }); diff --git a/test/moves/thousand_arrows.test.ts b/test/moves/thousand_arrows.test.ts index 5d21c7ec190..20139ddad69 100644 --- a/test/moves/thousand_arrows.test.ts +++ b/test/moves/thousand_arrows.test.ts @@ -26,11 +26,11 @@ describe("Moves - Thousand Arrows", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .enemySpecies(SpeciesId.SHUCKLE) + .enemySpecies(SpeciesId.EELEKTROSS) .startingLevel(100) - .enemyLevel(100) + .enemyLevel(50) .ability(AbilityId.COMPOUND_EYES) - .enemyAbility(AbilityId.BALL_FETCH) + .enemyAbility(AbilityId.STURDY) .moveset(MoveId.THOUSAND_ARROWS) .enemyMoveset(MoveId.SPLASH); }); @@ -39,32 +39,35 @@ describe("Moves - Thousand Arrows", () => { game.override.enemySpecies(SpeciesId.ARCHEOPS); await game.classicMode.startBattle([SpeciesId.ILLUMISE]); - const shuckle = game.scene.getEnemyPokemon()!; - expect(shuckle.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); + const archeops = game.scene.getEnemyPokemon()!; + expect(archeops.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); game.move.select(MoveId.THOUSAND_ARROWS); await game.phaseInterceptor.to("MoveEffectPhase", false); const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck"); - await game.toEndOfTurn(); expect(hitSpy).toHaveReturnedWith([expect.anything(), 1]); - expect(shuckle.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); - expect(shuckle.hp).toBeLessThan(shuckle.getMaxHp()); + expect(archeops.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); + expect(archeops.isGrounded()).toBe(true); + expect(archeops.hp).toBeLessThan(archeops.getMaxHp()); }); - it("should hit and ground targets with Levitate", async () => { - game.override.enemyAbility(AbilityId.LEVITATE); + it("should hit and ground targets with Levitate without affecting effectiveness", async () => { + game.override.enemyPassiveAbility(AbilityId.LEVITATE); await game.classicMode.startBattle([SpeciesId.ILLUMISE]); - const enemyPokemon = game.scene.getEnemyPokemon()!; - expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); + const eelektross = game.scene.getEnemyPokemon()!; + expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); game.move.select(MoveId.THOUSAND_ARROWS); + await game.phaseInterceptor.to("MoveEffectPhase", false); + const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck"); await game.toEndOfTurn(); - expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); + expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp()); + expect(hitSpy).toHaveReturnedWith([expect.anything(), 2]); }); it("should hit and ground targets under the effects of Magnet Rise", async () => { @@ -76,19 +79,19 @@ describe("Moves - Thousand Arrows", () => { await game.phaseInterceptor.to("MoveEndPhase"); // ensure magnet rise suceeeded before getting knocked down - const enemyPokemon = game.field.getEnemyPokemon(); - expect(enemyPokemon.getTag(BattlerTagType.FLOATING)).toBeDefined(); + const eelektross = game.field.getEnemyPokemon(); + expect(eelektross.getTag(BattlerTagType.FLOATING)).toBeDefined(); await game.toEndOfTurn(); - expect(enemyPokemon.getTag(BattlerTagType.FLOATING)).toBeUndefined(); - expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(eelektross.getTag(BattlerTagType.FLOATING)).toBeUndefined(); + expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); + expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp()); }); it.each<{ name: string; move: MoveId }>([ { name: "Fly", move: MoveId.FLY }, { name: "Bounce", move: MoveId.BOUNCE }, - ])("should cancel the target's Fly", async ({ move }) => { + ])("should hit through and cancel the target's $name", async ({ move }) => { await game.classicMode.startBattle([SpeciesId.ILLUMISE]); game.move.select(MoveId.THOUSAND_ARROWS); @@ -96,18 +99,19 @@ describe("Moves - Thousand Arrows", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("MoveEndPhase"); - // Fly should've worked... until we smacked them - const shuckle = game.field.getEnemyPokemon(); - expect(shuckle.getTag(BattlerTagType.FLYING)).toBeDefined(); - expect(shuckle.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); + // Fly should've worked until hit + const eelektross = game.field.getEnemyPokemon(); + expect(eelektross.getTag(BattlerTagType.FLYING)).toBeDefined(); + expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); - await game.phaseInterceptor.to("MoveEndPhase"); - expect(shuckle.getTag(BattlerTagType.FLYING)).toBeUndefined(); - expect(shuckle.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); - expect(shuckle.hp).toBeLessThan(shuckle.getMaxHp()); + await game.toEndOfTurn(); + expect(eelektross.getTag(BattlerTagType.FLYING)).toBeUndefined(); + expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); + expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp()); }); - it("should NOT ground semi-invulnerable targets unless already ungrounded", async () => { + // TODO: verify behavior + it.todo("should not ground semi-invulnerable targets unless already ungrounded", async () => { await game.classicMode.startBattle([SpeciesId.ILLUMISE]); game.move.select(MoveId.THOUSAND_ARROWS); @@ -115,10 +119,10 @@ describe("Moves - Thousand Arrows", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toEndOfTurn(); - const shuckle = game.field.getEnemyPokemon(); - expect(shuckle.isGrounded()).toBe(false); - expect(shuckle.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); - expect(shuckle.hp).toBeLessThan(shuckle.getMaxHp()); + const eelektross = game.field.getEnemyPokemon(); + expect(eelektross.isGrounded()).toBe(false); + expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); + expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp()); }); // TODO: Sky drop is currently unimplemented