diff --git a/src/data/ability.ts b/src/data/ability.ts index 019c78a91f0..0286d087093 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -6,7 +6,7 @@ import { BattleStat, getBattleStatName } from "./battle-stat"; import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { getPokemonMessage, getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag } from "./battler-tags"; +import { BattlerTag, GroundedTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr } from "./move"; @@ -4110,7 +4110,7 @@ export function initAbilities() { .attr(UnswappableAbilityAbAttr) .ignorable(), new Ability(Abilities.LEVITATE, 3) - .attr(TypeImmunityAbAttr, Type.GROUND, (pokemon: Pokemon) => !pokemon.getTag(BattlerTagType.IGNORE_FLYING) && !pokemon.scene.arena.getTag(ArenaTagType.GRAVITY) && !pokemon.getTag(BattlerTagType.GROUNDED)) + .attr(TypeImmunityAbAttr, Type.GROUND, (pokemon: Pokemon) => !pokemon.getTag(GroundedTag) && !pokemon.scene.arena.getTag(ArenaTagType.GRAVITY)) .ignorable(), new Ability(Abilities.EFFECT_SPORE, 3) .attr(EffectSporeAbAttr), diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4e2a4e06d3c..0880d6948f8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1467,6 +1467,17 @@ export class CursedTag extends BattlerTag { } } +/** + * Battler tag for effects that ground the source, allowing Ground-type moves to hit them. Encompasses two tag types: + * @item IGNORE_FLYING: Persistent grounding effects (i.e. from Smack Down and Thousand Waves) + * @item ROOSTED: One-turn grounding effects (i.e. from Roost) + */ +export class GroundedTag extends BattlerTag { + constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) { + super(tagType, lapseType, 1, sourceMove); + } +} + /** * Provides the Ice Face ability's effects. */ @@ -1618,9 +1629,9 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc case BattlerTagType.BYPASS_SLEEP: return new BattlerTag(BattlerTagType.BYPASS_SLEEP, BattlerTagLapseType.TURN_END, turnCount, sourceMove); case BattlerTagType.IGNORE_FLYING: - return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, turnCount, sourceMove); - case BattlerTagType.GROUNDED: - return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, turnCount - 1, sourceMove); + return new GroundedTag(tagType, BattlerTagLapseType.CUSTOM, sourceMove); + case BattlerTagType.ROOSTED: + return new GroundedTag(tagType, BattlerTagLapseType.TURN_END, sourceMove); case BattlerTagType.SALT_CURED: return new SaltCuredTag(sourceId); case BattlerTagType.CURSED: diff --git a/src/data/move.ts b/src/data/move.ts index 3b7ed896ea0..151021f7f9d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6606,7 +6606,7 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new SelfStatusMove(Moves.ROOST, Type.FLYING, -1, 5, -1, 0, 4) .attr(HealAttr, 0.5) - .attr(AddBattlerTagAttr, BattlerTagType.GROUNDED, true, false, 1) + .attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false) .triageMove(), new StatusMove(Moves.GRAVITY, Type.PSYCHIC, -1, 5, -1, 0, 4) .attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5) @@ -6947,7 +6947,7 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES) .unimplemented(), new AttackMove(Moves.SMACK_DOWN, Type.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, 100, 0, 5) - .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 5) + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(HitsTagAttr, BattlerTagType.FLYING, false) @@ -7331,7 +7331,7 @@ export function initMoves() { .triageMove(), new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) - .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 20) // TODO: remove this turn count + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false) .attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 4d212a2da12..5cdabfe78c2 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -54,7 +54,7 @@ export enum BattlerTagType { SALT_CURED = "SALT_CURED", CURSED = "CURSED", CHARGED = "CHARGED", - GROUNDED = "GROUNDED", + ROOSTED = "ROOSTED", MAGNET_RISEN = "MAGNET_RISEN", MINIMIZED = "MINIMIZED", DESTINY_BOND = "DESTINY_BOND", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 45ea72ef500..05ed21e732c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -19,7 +19,7 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases"; import { BattleStat } from "../data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, TypeImmuneTag, getBattlerTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, TypeImmuneTag, getBattlerTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag"; @@ -884,7 +884,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (forDefend && (this.getTag(BattlerTagType.IGNORE_FLYING) || this.scene.arena.getTag(ArenaTagType.GRAVITY) || this.getTag(BattlerTagType.GROUNDED))) { + if (forDefend && (this.getTag(GroundedTag) || this.scene.arena.getTag(ArenaTagType.GRAVITY))) { const flyingIndex = types.indexOf(Type.FLYING); if (flyingIndex > -1) { types.splice(flyingIndex, 1); @@ -1097,7 +1097,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } isGrounded(): boolean { - return !this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN); + return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN)); } /** diff --git a/src/test/moves/roost.test.ts b/src/test/moves/roost.test.ts new file mode 100644 index 00000000000..db9363313a1 --- /dev/null +++ b/src/test/moves/roost.test.ts @@ -0,0 +1,64 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Species } from "#app/enums/species.js"; +import { Moves } from "#app/enums/moves.js"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { MoveEffectPhase, TurnEndPhase } from "#app/phases.js"; +import { BattlerTagType } from "#app/enums/battler-tag-type.js"; +import { Abilities } from "#app/enums/abilities.js"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Roost", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.STARAPTOR); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.STOMPING_TANTRUM ]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ROOST,Moves.ROOST,Moves.ROOST,Moves.ROOST]); + }); + + test( + "move should ground the user until the end of turn", + async () => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon).toBeDefined(); + + const enemyStartingHp = enemyPokemon.hp; + + game.doAttack(getMovePosition(game.scene, 0, Moves.STOMPING_TANTRUM)); + + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(enemyPokemon.getTag(BattlerTagType.ROOSTED)).toBeDefined(); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); + expect(enemyPokemon.getTag(BattlerTagType.ROOSTED)).toBeUndefined(); + }, TIMEOUT + ); +}); diff --git a/src/test/moves/thousand_arrows.test.ts b/src/test/moves/thousand_arrows.test.ts index 08b4edfd313..c641643593e 100644 --- a/src/test/moves/thousand_arrows.test.ts +++ b/src/test/moves/thousand_arrows.test.ts @@ -57,7 +57,7 @@ describe("Moves - Thousand Arrows", () => { // Enemy should not be grounded before move effect is applied expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); - await game.phaseInterceptor.to(TurnEndPhase, false); + await game.phaseInterceptor.to(TurnEndPhase); expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);