diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 0880d6948f8..1a4a590e2a7 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1258,7 +1258,7 @@ export class TerrainHighestStatBoostTag extends HighestStatBoostTag implements T } } -export class HideSpriteTag extends BattlerTag { +export class SemiInvulnerableTag extends BattlerTag { constructor(tagType: BattlerTagType, turnCount: integer, sourceMove: Moves) { super(tagType, BattlerTagLapseType.MOVE_EFFECT, turnCount, sourceMove); } @@ -1615,7 +1615,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc case BattlerTagType.UNDERGROUND: case BattlerTagType.UNDERWATER: case BattlerTagType.HIDDEN: - return new HideSpriteTag(tagType, turnCount, sourceMove); + return new SemiInvulnerableTag(tagType, turnCount, sourceMove); case BattlerTagType.FIRE_BOOST: return new TypeBoostTag(tagType, sourceMove, Type.FIRE, 1.5, false); case BattlerTagType.CRIT_BOOST: diff --git a/src/data/move.ts b/src/data/move.ts index 36f27282574..6a48e588827 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,7 +1,7 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; import { BattleEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases"; import { BattleStat, getBattleStatName } from "./battle-stat"; -import { EncoreTag } from "./battler-tags"; +import { EncoreTag, SemiInvulnerableTag } from "./battler-tags"; import { getPokemonMessage, getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect"; @@ -7238,7 +7238,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) - .unimplemented(), + .attr(StatChangeAttr, BattleStat.DEF, 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), diff --git a/src/phases.ts b/src/phases.ts index a1aba34bcff..fcc1051622f 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -19,7 +19,7 @@ import { biomeLinks, getBiomeName } from "./data/biomes"; import { ModifierTier } from "./modifier/modifier-tier"; import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, HideSpriteTag as HiddenTag, ProtectedTag, TrappedTag } from "./data/battler-tags"; +import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags"; import { getPokemonMessage, getPokemonNameWithAffix } from "./messages"; import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; @@ -3033,7 +3033,7 @@ export class MoveEffectPhase extends PokemonPhase { return true; } - const hiddenTag = target.getTag(HiddenTag); + const hiddenTag = target.getTag(SemiInvulnerableTag); if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === hiddenTag.tagType)) { return false; } diff --git a/src/test/moves/flower_shield.test.ts b/src/test/moves/flower_shield.test.ts new file mode 100644 index 00000000000..f94af93fc66 --- /dev/null +++ b/src/test/moves/flower_shield.test.ts @@ -0,0 +1,120 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { + TurnEndPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; +import { BattleStat } from "#app/data/battle-stat.js"; +import { Biome } from "#app/enums/biome.js"; +import { Type } from "#app/data/type.js"; +import { SemiInvulnerableTag } from "#app/data/battler-tags.js"; + +describe("Moves - Flower Shield", () => { + 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, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.FLOWER_SHIELD, Moves.SPLASH]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + }); + + it("increases defense of all Grass-type Pokemon on the field by one stage - single battle", async () => { + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.CHERRIM); + + await game.startBattle([Species.MAGIKARP]); + const cherrim = game.scene.getEnemyPokemon(); + const magikarp = game.scene.getPlayerPokemon(); + + expect(magikarp.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(0); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLOWER_SHIELD)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(magikarp.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(1); + }); + + it("increases defense of all Grass-type Pokemon on the field by one stage - double battle", async () => { + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "STARTING_BIOME_OVERRIDE", "get").mockReturnValue(Biome.GRASS); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + + await game.startBattle([Species.CHERRIM, Species.MAGIKARP]); + const field = game.scene.getField(true); + + const grassPokemons = field.filter(p => p.getTypes().includes(Type.GRASS)); + const nonGrassPokemons = field.filter(pokemon => !grassPokemons.includes(pokemon)); + + grassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0)); + nonGrassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0)); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLOWER_SHIELD)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + await game.phaseInterceptor.to(TurnEndPhase); + + grassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(1)); + nonGrassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0)); + }); + + /** + * See semi-vulnerable state tags. {@linkcode SemiInvulnerableTag} + */ + it("does not increase defense of a pokemon in semi-vulnerable state", async () => { + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.PARAS); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DIG, Moves.DIG, Moves.DIG, Moves.DIG]); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(50); + + await game.startBattle([Species.CHERRIM]); + const paras = game.scene.getEnemyPokemon(); + const cherrim = game.scene.getPlayerPokemon(); + + expect(paras.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(paras.getTag(SemiInvulnerableTag)).toBeUndefined; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLOWER_SHIELD)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(paras.getTag(SemiInvulnerableTag)).toBeDefined(); + expect(paras.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(1); + }); + + it("does nothing if there are no Grass-type pokemon on the field", async () => { + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + + await game.startBattle([Species.MAGIKARP]); + const enemy = game.scene.getEnemyPokemon(); + const ally = game.scene.getPlayerPokemon(); + + expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(ally.summonData.battleStats[BattleStat.DEF]).toBe(0); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLOWER_SHIELD)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(ally.summonData.battleStats[BattleStat.DEF]).toBe(0); + }); +});