Fixed groundedness checks in code

This commit is contained in:
Bertie690 2025-06-10 09:42:47 -04:00
parent 4e9655e129
commit e59bf5254f
6 changed files with 127 additions and 109 deletions

View File

@ -9910,7 +9910,9 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ]) .attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
.attr(HitsTagAttr, BattlerTagType.FLYING) .attr(HitsTagAttr, BattlerTagType.FLYING)
.makesContact(false), .makesContact(false)
// TODO: Confirm if Smack Down & Thousand Arrows will ground semi-invulnerable Pokemon with No Guard, etc.
.edgeCase(),
new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
new AttackMove(MoveId.FLAME_BURST, PokemonType.FIRE, MoveCategory.SPECIAL, 70, 100, 15, -1, 0, 5) new AttackMove(MoveId.FLAME_BURST, PokemonType.FIRE, MoveCategory.SPECIAL, 70, 100, 15, -1, 0, 5)
@ -10365,7 +10367,9 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ]) .attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
// TODO: Confirm if Smack Down & Thousand Arrows will ground semi-invulnerable Pokemon with No Guard, etc.
.edgeCase(),
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
.makesContact(false) .makesContact(false)

View File

@ -2268,6 +2268,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* To be considered grounded, a Pokemon must either: * To be considered grounded, a Pokemon must either:
* * Be {@linkcode GroundedTag | forcibly grounded} from an effect like Smack Down or Ingrain * * Be {@linkcode GroundedTag | forcibly grounded} from an effect like Smack Down or Ingrain
* * Be under the effects of {@linkcode ArenaTagType.GRAVITY | harsh gravity}
* * **Not** be any of the following things: * * **Not** be any of the following things:
* * {@linkcode PokemonType.FLYING | Flying-type} * * {@linkcode PokemonType.FLYING | Flying-type}
* * {@linkcode Abilities.LEVITATE | Levitating} * * {@linkcode Abilities.LEVITATE | Levitating}
@ -2280,11 +2281,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public isGrounded(ignoreSemiInvulnerable = false): boolean { public isGrounded(ignoreSemiInvulnerable = false): boolean {
return ( return (
!!this.getTag(GroundedTag) || !!this.getTag(GroundedTag) ||
!!globalScene.arena.hasTag(ArenaTagType.GRAVITY) ||
(!this.isOfType(PokemonType.FLYING, true, true) && (!this.isOfType(PokemonType.FLYING, true, true) &&
!this.hasAbility(AbilityId.LEVITATE) && !this.hasAbility(AbilityId.LEVITATE) &&
!this.getTag(BattlerTagType.FLOATING) && !this.getTag(BattlerTagType.FLOATING) &&
ignoreSemiInvulnerable || !this.getTag(SemiInvulnerableTag) (ignoreSemiInvulnerable || !this.getTag(SemiInvulnerableTag)))
)
); );
} }
@ -2462,7 +2463,8 @@ 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 // 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 // Related to https://github.com/pagefaultgames/pokerogue/issues/524
if (moveType === PokemonType.GROUND && (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY))) { //
if (moveType === PokemonType.GROUND && this.isGrounded(true)) {
const flyingIndex = types.indexOf(PokemonType.FLYING); const flyingIndex = types.indexOf(PokemonType.FLYING);
if (flyingIndex > -1) { if (flyingIndex > -1) {
types.splice(flyingIndex, 1); types.splice(flyingIndex, 1);

View File

@ -1,7 +1,7 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { MoveResult } from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";

View File

@ -8,6 +8,8 @@ import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { StatusEffect } from "#enums/status-effect";
import { MoveResult } from "#enums/move-result";
describe("Arena - Gravity", () => { describe("Arena - Gravity", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -27,130 +29,129 @@ describe("Arena - Gravity", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleStyle("single") .battleStyle("single")
.moveset([MoveId.TACKLE, MoveId.GRAVITY, MoveId.FISSURE])
.ability(AbilityId.UNNERVE) .ability(AbilityId.UNNERVE)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemySpecies(SpeciesId.SHUCKLE) .enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH)
.enemyLevel(5); .enemyLevel(5);
}); });
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) // Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
it("non-OHKO move accuracy is multiplied by 1.67", async () => { it("should multiply all non-OHKO move accuracy by 1.67x", async () => {
const moveToCheck = allMoves[MoveId.TACKLE]; const accSpy = vi.spyOn(allMoves[MoveId.TACKLE], "calculateBattleAccuracy");
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn
await game.classicMode.startBattle([SpeciesId.PIKACHU]); await game.classicMode.startBattle([SpeciesId.PIKACHU]);
game.move.select(MoveId.GRAVITY);
await game.phaseInterceptor.to("TurnEndPhase"); game.move.use(MoveId.GRAVITY);
await game.move.forceEnemyMove(MoveId.TACKLE);
await game.toEndOfTurn();
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67);
// Use non-OHKO move on second turn
await game.toNextTurn();
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
}); });
it("OHKO move accuracy is not affected", async () => { it("should not affect OHKO move accuracy", async () => {
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */ const accSpy = vi.spyOn(allMoves[MoveId.FISSURE], "calculateBattleAccuracy");
const moveToCheck = allMoves[MoveId.FISSURE];
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn
await game.classicMode.startBattle([SpeciesId.PIKACHU]); await game.classicMode.startBattle([SpeciesId.PIKACHU]);
game.move.select(MoveId.GRAVITY);
await game.phaseInterceptor.to("TurnEndPhase"); game.move.use(MoveId.GRAVITY);
await game.move.forceEnemyMove(MoveId.FISSURE);
await game.toEndOfTurn();
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.FISSURE].accuracy);
// Use OHKO move on second turn
await game.toNextTurn();
game.move.select(MoveId.FISSURE);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
}); });
describe("Against flying types", () => { describe.each<{ name: string; overrides: () => unknown }>([
it("can be hit by ground-type moves now", async () => { { name: "Flying-type", overrides: () => game.override.enemySpecies(SpeciesId.MOLTRES) },
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.EARTHQUAKE]); { name: "Levitating", overrides: () => game.override.enemyAbility(AbilityId.LEVITATE) },
])("should ground $name Pokemon", ({ overrides }) => {
beforeEach(overrides);
it("should remove immunity to Ground-type moves", async () => {
await game.classicMode.startBattle([SpeciesId.PIKACHU]); await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const pidgeot = game.scene.getEnemyPokemon()!; const enemy = game.field.getEnemyPokemon();
vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); const effectivenessSpy = vi.spyOn(enemy, "getAttackTypeEffectiveness");
// Try earthquake on 1st turn (fails!); game.move.use(MoveId.EARTHQUAKE);
game.move.select(MoveId.EARTHQUAKE); await game.move.forceEnemyMove(MoveId.GRAVITY);
await game.phaseInterceptor.to("TurnEndPhase"); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
// Setup Gravity on 2nd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.GRAVITY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
expect(effectivenessSpy).toHaveLastReturnedWith(2);
// Use ground move on 3rd turn expect(enemy.isGrounded()).toBe(true);
await game.toNextTurn();
game.move.select(MoveId.EARTHQUAKE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
}); });
it("keeps super-effective moves super-effective after using gravity", async () => { it("should preserve normal move effectiveness for secondary type", async () => {
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.THUNDERBOLT]);
await game.classicMode.startBattle([SpeciesId.PIKACHU]); await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const pidgeot = game.scene.getEnemyPokemon()!; const enemy = game.field.getEnemyPokemon();
vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); const effectivenessSpy = vi.spyOn(enemy, "getAttackTypeEffectiveness");
// Setup Gravity on 1st turn game.move.use(MoveId.THUNDERBOLT);
game.move.select(MoveId.GRAVITY); await game.move.forceEnemyMove(MoveId.GRAVITY);
await game.phaseInterceptor.to("TurnEndPhase"); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toEndOfTurn();
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(effectivenessSpy).toHaveLastReturnedWith(2);
});
// Use electric move on 2nd turn it("causes terrain to come into effect", async () => {
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const enemy = game.field.getEnemyPokemon();
enemy.hp = 1;
const statusSpy = vi.spyOn(enemy, "canSetStatus");
// Turn 1: set up electric terrain; spore works due to being ungrounded
game.move.use(MoveId.SPORE);
await game.move.forceEnemyMove(MoveId.ELECTRIC_TERRAIN);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.THUNDERBOLT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2); expect(statusSpy).toHaveLastReturnedWith(true);
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
enemy.resetStatus();
// Turn 2: gravity grounds enemy; makes spore fail
game.move.use(MoveId.SPORE);
await game.move.forceEnemyMove(MoveId.GRAVITY);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
expect(statusSpy).toHaveLastReturnedWith(true);
expect(enemy.status?.effect).toBeUndefined();
}); });
}); });
it("cancels Fly if its user is semi-invulnerable", async () => { it.each<{ name: string; move: MoveId }>([
game.override.enemySpecies(SpeciesId.SNORLAX).enemyMoveset(MoveId.FLY).moveset([MoveId.GRAVITY, MoveId.SPLASH]); { name: "Fly", move: MoveId.FLY },
{ name: "Bounce", move: MoveId.BOUNCE },
{ name: "Sky Drop", move: MoveId.SKY_DROP },
])("cancels $name if its user is semi-invulnerable", async ({ move }) => {
await game.classicMode.startBattle([SpeciesId.CHARIZARD]); await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
const charizard = game.scene.getPlayerPokemon()!; const charizard = game.field.getPlayerPokemon();
const snorlax = game.scene.getEnemyPokemon()!; const snorlax = game.field.getEnemyPokemon();
game.move.select(MoveId.SPLASH);
game.move.use(MoveId.SPLASH);
await game.move.forceEnemyMove(move);
await game.toNextTurn(); await game.toNextTurn();
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined(); expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
game.move.select(MoveId.GRAVITY); game.move.select(MoveId.GRAVITY);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase"); await game.phaseInterceptor.to("MoveEffectPhase");
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined(); expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
await game.phaseInterceptor.to("TurnEndPhase"); await game.toEndOfTurn();
expect(charizard.hp).toBe(charizard.getMaxHp()); expect(charizard.hp).toBe(charizard.getMaxHp());
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeUndefined();
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
}); });
}); });

View File

@ -1,7 +1,7 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#enums/battler-index";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { getTerrainName, TerrainType } from "#app/data/terrain"; import { getTerrainName, TerrainType } from "#app/data/terrain";
import { MoveResult } from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { capitalizeFirstLetter, randSeedInt, toDmgValue } from "#app/utils/common"; import { capitalizeFirstLetter, randSeedInt, toDmgValue } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
@ -99,6 +99,7 @@ describe("Terrain -", () => {
it("should heal all grounded, non-semi-invulnerable pokemon for 1/16th max HP at end of turn", async () => { it("should heal all grounded, non-semi-invulnerable pokemon for 1/16th max HP at end of turn", async () => {
await game.classicMode.startBattle([SpeciesId.BLISSEY]); await game.classicMode.startBattle([SpeciesId.BLISSEY]);
// shuckle is grounded, pidgeot isn't
const blissey = game.field.getPlayerPokemon(); const blissey = game.field.getPlayerPokemon();
blissey.hp = toDmgValue(blissey.getMaxHp() / 2); blissey.hp = toDmgValue(blissey.getMaxHp() / 2);
expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1); expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1);
@ -114,7 +115,6 @@ describe("Terrain -", () => {
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1); expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1); expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
game.phaseInterceptor.clearLogs();
game.move.use(MoveId.DIG); game.move.use(MoveId.DIG);
await game.toNextTurn(); await game.toNextTurn();
@ -169,7 +169,7 @@ describe("Terrain -", () => {
); );
}); });
// TODO: Enable after terrain block PR is added // TODO: Enable tests after terrain-msg branch is merged
describe.skip("Electric Terrain", () => { describe.skip("Electric Terrain", () => {
afterEach(() => { afterEach(() => {
game.phaseInterceptor.restoreOg(); game.phaseInterceptor.restoreOg();
@ -183,24 +183,23 @@ describe("Terrain -", () => {
.enemyLevel(100) .enemyLevel(100)
.enemySpecies(SpeciesId.SHUCKLE) .enemySpecies(SpeciesId.SHUCKLE)
.enemyAbility(AbilityId.ELECTRIC_SURGE) .enemyAbility(AbilityId.ELECTRIC_SURGE)
.enemyPassiveAbility(AbilityId.LEVITATE)
.ability(AbilityId.NO_GUARD); .ability(AbilityId.NO_GUARD);
}); });
it("should prevent all grounded pokemon from being put to sleep", async () => { it("should prevent all grounded pokemon from being put to sleep", async () => {
await game.classicMode.startBattle([SpeciesId.BLISSEY]); await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
game.move.use(MoveId.SPORE); game.move.use(MoveId.SPORE);
await game.move.forceEnemyMove(MoveId.SPORE); await game.move.forceEnemyMove(MoveId.SPORE);
await game.toEndOfTurn(); await game.toEndOfTurn();
const blissey = game.field.getPlayerPokemon(); const pidgeot = game.field.getPlayerPokemon();
const shuckle = game.field.getEnemyPokemon(); const shuckle = game.field.getEnemyPokemon();
expect(blissey.status?.effect).toBeUndefined(); expect(pidgeot.status?.effect).toBeUndefined();
expect(shuckle.status?.effect).toBe(StatusEffect.SLEEP); expect(shuckle.status?.effect).toBe(StatusEffect.SLEEP);
// TODO: These don't work due to how move failures are propagated // TODO: These don't work due to how move failures are propagated
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(game.textInterceptor.logs).not.toContain( expect(game.textInterceptor.logs).not.toContain(
i18next.t("terrain:defaultBlockMessage", { i18next.t("terrain:defaultBlockMessage", {
@ -211,16 +210,19 @@ describe("Terrain -", () => {
}); });
it("should prevent attack moves from applying sleep without showing text/failing move", async () => { it("should prevent attack moves from applying sleep without showing text/failing move", async () => {
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
vi.spyOn(allMoves[MoveId.RELIC_SONG], "chance", "get").mockReturnValue(100); vi.spyOn(allMoves[MoveId.RELIC_SONG], "chance", "get").mockReturnValue(100);
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
const shuckle = game.field.getEnemyPokemon();
const statusSpy = vi.spyOn(shuckle, "canSetStatus");
game.move.use(MoveId.RELIC_SONG); game.move.use(MoveId.RELIC_SONG);
await game.move.forceEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toEndOfTurn(); await game.toEndOfTurn();
const blissey = game.field.getPlayerPokemon(); const blissey = game.field.getPlayerPokemon();
const shuckle = game.field.getEnemyPokemon();
expect(shuckle.status?.effect).toBeUndefined(); expect(shuckle.status?.effect).toBeUndefined();
expect(statusSpy).toHaveLastReturnedWith(false);
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(game.textInterceptor.logs).not.toContain( expect(game.textInterceptor.logs).not.toContain(
@ -232,6 +234,7 @@ describe("Terrain -", () => {
}); });
}); });
// TODO: Enable tests after terrain-msg branch is merged
describe("Misty Terrain", () => { describe("Misty Terrain", () => {
afterEach(() => { afterEach(() => {
game.phaseInterceptor.restoreOg(); game.phaseInterceptor.restoreOg();
@ -245,26 +248,25 @@ describe("Terrain -", () => {
.enemyLevel(100) .enemyLevel(100)
.enemySpecies(SpeciesId.SHUCKLE) .enemySpecies(SpeciesId.SHUCKLE)
.enemyAbility(AbilityId.MISTY_SURGE) .enemyAbility(AbilityId.MISTY_SURGE)
.enemyPassiveAbility(AbilityId.LEVITATE)
.ability(AbilityId.NO_GUARD); .ability(AbilityId.NO_GUARD);
}); });
it("should prevent all grounded pokemon from being statused or confused", async () => { it("should prevent all grounded pokemon from being statused or confused", async () => {
game.override.confusionActivation(false); // prevent self hits from ruining things game.override.confusionActivation(false); // prevent self hits from ruining things
await game.classicMode.startBattle([SpeciesId.BLISSEY]); await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
// blissey is grounded, shuckle isn't // shuckle is grounded, pidgeot isn't
game.move.use(MoveId.TOXIC); game.move.use(MoveId.TOXIC);
await game.move.forceEnemyMove(MoveId.TOXIC); await game.move.forceEnemyMove(MoveId.TOXIC);
await game.toNextTurn(); await game.toNextTurn();
const blissey = game.field.getPlayerPokemon(); const pidgeot = game.field.getPlayerPokemon();
const shuckle = game.field.getEnemyPokemon(); const shuckle = game.field.getEnemyPokemon();
expect(blissey.status?.effect).toBeUndefined(); expect(pidgeot.status?.effect).toBeUndefined();
expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC); expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC);
// TODO: These don't work due to how move failures are propagated // TODO: These don't work due to how move failures are propagated
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(game.textInterceptor.logs).toContain( expect(game.textInterceptor.logs).toContain(
i18next.t("terrain:mistyBlockMessage", { i18next.t("terrain:mistyBlockMessage", {
@ -277,21 +279,30 @@ describe("Terrain -", () => {
await game.move.forceEnemyMove(MoveId.CONFUSE_RAY); await game.move.forceEnemyMove(MoveId.CONFUSE_RAY);
await game.toNextTurn(); await game.toNextTurn();
expect(blissey.getTag(BattlerTagType.CONFUSED)).toBeUndefined(); expect(pidgeot.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeUndefined(); expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeDefined();
}); });
it("should prevent attack moves from applying status without showing text/failing move", async () => { it.each<{ status: string; move: MoveId }>([
{ status: "Sleep", move: MoveId.RELIC_SONG },
{ status: "Burn", move: MoveId.SACRED_FIRE },
{ status: "Freeze", move: MoveId.ICE_BEAM },
{ status: "Paralysis", move: MoveId.NUZZLE },
{ status: "Poison", move: MoveId.SLUDGE_BOMB },
{ status: "Toxic", move: MoveId.MALIGNANT_CHAIN },
{ status: "Confusion", move: MoveId.MAGICAL_TORQUE },
])("should prevent attack moves from applying $name without showing text/failing move", async ({ move }) => {
await game.classicMode.startBattle([SpeciesId.BLISSEY]); await game.classicMode.startBattle([SpeciesId.BLISSEY]);
vi.spyOn(allMoves[MoveId.SACRED_FIRE], "chance", "get").mockReturnValue(100); vi.spyOn(allMoves[move], "chance", "get").mockReturnValue(100);
game.move.use(MoveId.SACRED_FIRE); game.move.use(move);
await game.move.forceEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toEndOfTurn(); await game.toEndOfTurn();
const blissey = game.field.getPlayerPokemon(); const blissey = game.field.getPlayerPokemon();
const shuckle = game.field.getEnemyPokemon(); const shuckle = game.field.getEnemyPokemon();
expect(shuckle.status?.effect).toBeUndefined(); expect(shuckle.status?.effect).toBeUndefined();
expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(game.textInterceptor.logs).not.toContain( expect(game.textInterceptor.logs).not.toContain(

View File

@ -1,4 +1,4 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#enums/battler-index";
import { AbilityId } from "#app/enums/ability-id"; import { AbilityId } from "#app/enums/ability-id";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import type { MoveEffectPhase } from "#app/phases/move-effect-phase"; import type { MoveEffectPhase } from "#app/phases/move-effect-phase";
@ -44,7 +44,7 @@ describe("Moves - Thousand Arrows", () => {
game.move.select(MoveId.THOUSAND_ARROWS); game.move.select(MoveId.THOUSAND_ARROWS);
await game.phaseInterceptor.to("MoveEffectPhase", false); await game.phaseInterceptor.to("MoveEffectPhase", false);
const hitSpy = vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck"); const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
await game.toEndOfTurn(); await game.toEndOfTurn();