From 57f39efdae0d8288591320a97072a93190cd5890 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:52:48 -0700 Subject: [PATCH] [Moves][Ability] Implement Torment / Taunt / Imprison + Aroma Veil (#4378) * Torment * Taunt and Imprison * ability immunities * Aroma Veil * Imprison * Test Files * Added exceptions for Rollout and check for active ability * adding tests so that git doesn't auto-fail * Blah * please * some documentation * Removed random newlines * Added check for ability's presence mid battle * Changed BattlerTagImmunityAbAttr to look at lists instead * Work? * Imprison and Taunt Tests * Tests * Final tests before documentation * documentation blah * Imports * Flx Change * flx - adding overrides * Update src/data/arena-tag.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * flx fixes * quick docs * privated retrieveField * Handling undefined * Update src/data/arena-tag.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * forget to remove partials for heal block * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Marked Torment as partial * Update src/test/moves/torment.test.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * tsdocs * Prevents test pokemon from being immune to torment * Update src/data/arena-tag.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * Torranx Fixes * Check for this.source * why * lighting things with my mind on fire * aRHGHSHDKSHD --------- Co-authored-by: frutescens Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> --- src/data/ability.ts | 18 ++- src/data/arena-tag.ts | 94 ++++++++++++++-- src/data/battler-tags.ts | 152 +++++++++++++++++++++++++- src/data/move.ts | 9 +- src/enums/arena-tag-type.ts | 3 +- src/enums/battler-tag-type.ts | 3 + src/locales/en/battler-tags.json | 3 + src/test/abilities/aroma_veil.test.ts | 65 +++++++++++ src/test/moves/imprison.test.ts | 98 +++++++++++++++++ src/test/moves/taunt.test.ts | 54 +++++++++ src/test/moves/torment.test.ts | 64 +++++++++++ 11 files changed, 537 insertions(+), 26 deletions(-) create mode 100644 src/test/abilities/aroma_veil.test.ts create mode 100644 src/test/moves/imprison.test.ts create mode 100644 src/test/moves/taunt.test.ts create mode 100644 src/test/moves/torment.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 9e9c423623d..3ace872de3c 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2026,6 +2026,7 @@ export class PostSummonAbAttr extends AbAttr { return false; } } + /** * Removes specified arena tags when a Pokemon is summoned. */ @@ -2852,17 +2853,17 @@ export class PreApplyBattlerTagAbAttr extends AbAttr { * Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets. */ export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { - private immuneTagType: BattlerTagType; + private immuneTagTypes: BattlerTagType[]; private battlerTag: BattlerTag; - constructor(immuneTagType: BattlerTagType) { + constructor(immuneTagTypes: BattlerTagType | BattlerTagType[]) { super(); - this.immuneTagType = immuneTagType; + this.immuneTagTypes = Array.isArray(immuneTagTypes) ? immuneTagTypes : [immuneTagTypes]; } applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (tag.tagType === this.immuneTagType) { + if (this.immuneTagTypes.includes(tag.tagType)) { cancelled.value = true; if (!simulated) { this.battlerTag = tag; @@ -4916,7 +4917,7 @@ export function initAbilities() { .attr(TypeImmunityHealAbAttr, Type.WATER) .ignorable(), new Ability(Abilities.OBLIVIOUS, 3) - .attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED) + .attr(BattlerTagImmunityAbAttr, [BattlerTagType.INFATUATED, BattlerTagType.TAUNT]) .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.CLOUD_NINE, 3) @@ -5402,8 +5403,7 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), new Ability(Abilities.AROMA_VEIL, 6) - .ignorable() - .unimplemented(), + .attr(UserFieldBattlerTagImmunityAbAttr, [BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK]), new Ability(Abilities.FLOWER_VEIL, 6) .ignorable() .unimplemented(), @@ -5885,7 +5885,6 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.EARTH_EATER, 9) .attr(TypeImmunityHealAbAttr, Type.GROUND) - .partial() // Healing not blocked by Heal Block .ignorable(), new Ability(Abilities.MYCELIUM_MIGHT, 9) .attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.category === MoveCategory.STATUS, -0.2) @@ -5900,8 +5899,7 @@ export function initAbilities() { .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1) .condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)), new Ability(Abilities.HOSPITALITY, 9) - .attr(PostSummonAllyHealAbAttr, 4, true) - .partial(), // Healing not blocked by Heal Block + .attr(PostSummonAllyHealAbAttr, 4, true), new Ability(Abilities.TOXIC_CHAIN, 9) .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC), new Ability(Abilities.EMBODY_ASPECT_TEAL, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fdc32b75c19..8ffb3038629 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,14 +1,15 @@ -import { Arena } from "../field/arena"; -import { Type } from "./type"; -import * as Utils from "../utils"; -import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move"; -import { getPokemonNameWithAffix } from "../messages"; -import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; -import { BattlerIndex } from "../battle"; -import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; +import { Arena } from "#app/field/arena"; +import BattleScene from "#app/battle-scene"; +import { Type } from "#app/data/type"; +import * as Utils from "#app/utils"; +import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; +import { getPokemonNameWithAffix } from "#app/messages"; +import Pokemon, { HitResult, PlayerPokemon, PokemonMove, EnemyPokemon } from "#app/field/pokemon"; +import { StatusEffect } from "#app/data/status-effect"; +import { BattlerIndex } from "#app/battle"; +import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { Stat } from "#enums/stat"; -import { CommonAnim, CommonBattleAnim } from "./battle-anims"; +import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import i18next from "i18next"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -919,6 +920,77 @@ class SafeguardTag extends ArenaTag { } } +/** + * This arena tag facilitates the application of the move Imprison + * Imprison remains in effect as long as the source Pokemon is active and present on the field. + * Imprison will apply to any opposing Pokemon that switch onto the field as well. + */ +class ImprisonTag extends ArenaTrapTag { + private source: Pokemon; + + constructor(sourceId: number, side: ArenaTagSide) { + super(ArenaTagType.IMPRISON, Moves.IMPRISON, sourceId, side, 1); + } + + /** + * Helper function that retrieves the Pokemon effected + * @param {BattleScene} scene medium to retrieve the involved Pokemon + * @returns list of PlayerPokemon or EnemyPokemon on the field + */ + private retrieveField(scene: BattleScene): PlayerPokemon[] | EnemyPokemon[] { + if (!this.source.isPlayer()) { + return scene.getPlayerField() ?? []; + } + return scene.getEnemyField() ?? []; + } + + /** + * This function applies the effects of Imprison to the opposing Pokemon already present on the field. + * @param arena + */ + override onAdd({ scene }: Arena) { + this.source = scene.getPokemonById(this.sourceId!)!; + if (this.source) { + const party = this.retrieveField(scene); + party?.forEach((p: PlayerPokemon | EnemyPokemon ) => { + p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); + }); + scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", {pokemonNameWithAffix: getPokemonNameWithAffix(this.source)})); + } + } + + /** + * Checks if the source Pokemon is still active on the field + * @param _arena + * @returns `true` if the source of the tag is still active on the field | `false` if not + */ + override lapse(_arena: Arena): boolean { + return this.source.isActive(true); + } + + /** + * This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active + * @param {Pokemon} pokemon the Pokemon Imprison is applied to + * @returns `true` + */ + override activateTrap(pokemon: Pokemon): boolean { + if (this.source.isActive(true)) { + pokemon.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); + } + return true; + } + + /** + * When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon + * @param arena + */ + override onRemove({ scene }: Arena): void { + const party = this.retrieveField(scene); + party?.forEach((p: PlayerPokemon | EnemyPokemon) => { + p.removeTag(BattlerTagType.IMPRISON); + }); + } +} export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { @@ -967,6 +1039,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return new HappyHourTag(turnCount, sourceId, side); case ArenaTagType.SAFEGUARD: return new SafeguardTag(turnCount, sourceId, side); + case ArenaTagType.IMPRISON: + return new ImprisonTag(sourceId, side); default: return null; } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index e92446ef5a2..65af0fb6ee8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3,7 +3,7 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import { StatusEffect } from "./status-effect"; import * as Utils from "../utils"; -import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr } from "./move"; +import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move"; import { Type } from "./type"; import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; import { TerrainType } from "./terrain"; @@ -2437,6 +2437,150 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { } } +/** + * Battle Tag that applies the move Torment to the target Pokemon + * Torment restricts the use of moves twice in a row. + * The tag is only removed if the target leaves the battle. + * Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied + */ +export class TormentTag extends MoveRestrictionBattlerTag { + private target: Pokemon; + + constructor(sourceId: number) { + super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.TORMENT, sourceId); + } + + /** + * Adds the battler tag to the target Pokemon and defines the private class variable 'target' + * 'Target' is used to track the Pokemon's current status + * @param {Pokemon} pokemon the Pokemon tormented + */ + override onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + this.target = pokemon; + pokemon.scene.queueMessage(i18next.t("battlerTags:tormentOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + } + + /** + * Torment only ends when the affected Pokemon leaves the battle field + * @param {Pokemon} pokemon the Pokemon under the effects of Torment + * @param _tagType + * @returns `true` if still present | `false` if not + */ + override lapse(pokemon: Pokemon, _tagType: BattlerTagLapseType): boolean { + return !pokemon.isActive(true); + } + + /** + * This checks if the current move used is identical to the last used move with a {@linkcode MoveResult} of `SUCCESS`/`MISS` + * @param {Moves} move the move under investigation + * @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other + */ + override isMoveRestricted(move: Moves): boolean { + const lastMove = this.target.getLastXMoves(1)[0]; + if ( !lastMove ) { + return false; + } + // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY + // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts + const moveObj = allMoves[lastMove.move]; + const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || this.target.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); + const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS); + if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { + return true; + } + return false; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } +} + +/** + * BattlerTag that applies the effects of Taunt to the target Pokemon + * Taunt restricts the use of status moves. + * The tag is removed after 4 turns. + */ +export class TauntTag extends MoveRestrictionBattlerTag { + constructor() { + super(BattlerTagType.TAUNT, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 4, Moves.TAUNT); + } + + override onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:tauntOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + } + + /** + * Checks if a move is a status move and determines its restriction status on that basis + * @param {Moves} move the move under investigation + * @returns `true` if the move is a status move + */ + override isMoveRestricted(move: Moves): boolean { + return allMoves[move].category === MoveCategory.STATUS; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } + + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); + } +} + +/** + * BattlerTag that applies the effects of Imprison to the target Pokemon + * Imprison restricts the opposing side's usage of moves shared by the source-user of Imprison. + * The tag is only removed when the source-user is removed from the field. + */ +export class ImprisonTag extends MoveRestrictionBattlerTag { + private source: Pokemon | null; + + constructor(sourceId: number) { + super(BattlerTagType.IMPRISON, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 1, Moves.IMPRISON, sourceId); + } + + override onAdd(pokemon: Pokemon) { + if (this.sourceId) { + this.source = pokemon.scene.getPokemonById(this.sourceId); + } + } + + /** + * Checks if the source of Imprison is still active + * @param _pokemon + * @param _lapseType + * @returns `true` if the source is still active + */ + override lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + return this.source?.isActive(true) ?? false; + } + + /** + * Checks if the source of the tag has the parameter move in its moveset and that the source is still active + * @param {Moves} move the move under investigation + * @returns `false` if either condition is not met + */ + override isMoveRestricted(move: Moves): boolean { + if (this.source) { + const sourceMoveset = this.source.getMoveset().map(m => m!.moveId); + return sourceMoveset?.includes(move) && this.source.isActive(true); + } + return false; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } + + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); + } +} + + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @@ -2604,6 +2748,12 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new MysteryEncounterPostSummonTag(); case BattlerTagType.HEAL_BLOCK: return new HealBlockTag(turnCount, sourceMove); + case BattlerTagType.TORMENT: + return new TormentTag(sourceId); + case BattlerTagType.TAUNT: + return new TauntTag(); + case BattlerTagType.IMPRISON: + return new ImprisonTag(sourceId); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 8866e86f708..27c27696650 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7507,7 +7507,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) .ignoresSubstitute() - .unimplemented(), + .partial() // Incomplete implementation because of Uproar's partial implementation + .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) .attr(ConfuseAttr), @@ -7538,7 +7539,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) .ignoresSubstitute() - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4), new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .ignoresSubstitute() @@ -7581,9 +7582,9 @@ export function initMoves() { new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3) .ignoresSubstitute() .attr(SwitchAbilitiesAttr), - new SelfStatusMove(Moves.IMPRISON, Type.PSYCHIC, -1, 10, -1, 0, 3) + new StatusMove(Moves.IMPRISON, Type.PSYCHIC, 100, 10, -1, 0, 3) .ignoresSubstitute() - .unimplemented(), + .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 1c79750c91a..c6f911cb493 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -23,5 +23,6 @@ export enum ArenaTagType { TAILWIND = "TAILWIND", HAPPY_HOUR = "HAPPY_HOUR", SAFEGUARD = "SAFEGUARD", - NO_CRIT = "NO_CRIT" + NO_CRIT = "NO_CRIT", + IMPRISON = "IMPRISON", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index d606ae319f7..9ed3b629746 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -82,4 +82,7 @@ export enum BattlerTagType { AUTOTOMIZED = "AUTOTOMIZED", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", HEAL_BLOCK = "HEAL_BLOCK", + TORMENT = "TORMENT", + TAUNT = "TAUNT", + IMPRISON = "IMPRISON", } diff --git a/src/locales/en/battler-tags.json b/src/locales/en/battler-tags.json index 481f69db250..6a5eeee2577 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -74,5 +74,8 @@ "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", + "tormentOnAdd": "{{pokemonNameWithAffix}} was subjected to torment!", + "tauntOnAdd": "{{pokemonNameWithAffix}} fell for the taunt!", + "imprisonOnAdd": "{{pokemonNameWithAffix}} sealed the opponents move(s)!", "autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!" } diff --git a/src/test/abilities/aroma_veil.test.ts b/src/test/abilities/aroma_veil.test.ts new file mode 100644 index 00000000000..b70308a5d60 --- /dev/null +++ b/src/test/abilities/aroma_veil.test.ts @@ -0,0 +1,65 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerIndex } from "#app/battle"; +import { PlayerPokemon } from "#app/field/pokemon"; + +describe("Moves - Aroma Veil", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("double") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.HEAL_BLOCK, Moves.IMPRISON, Moves.SPLASH]) + .enemySpecies(Species.SHUCKLE) + .ability(Abilities.AROMA_VEIL) + .moveset([Moves.GROWL]); + }); + + it("Aroma Veil protects the Pokemon's side against most Move Restriction Battler Tags", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const party = game.scene.getParty()! as PlayerPokemon[]; + + game.move.select(Moves.GROWL); + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.HEAL_BLOCK); + await game.toNextTurn(); + party.forEach(p => { + expect(p.getTag(BattlerTagType.HEAL_BLOCK)).toBeUndefined(); + }); + }); + + it("Aroma Veil does not protect against Imprison", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const party = game.scene.getParty()! as PlayerPokemon[]; + + game.move.select(Moves.GROWL); + game.move.select(Moves.GROWL, 1); + await game.forceEnemyMove(Moves.IMPRISON, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeDefined(); + party.forEach(p => { + expect(p.getTag(BattlerTagType.IMPRISON)).toBeDefined(); + }); + }); +}); diff --git a/src/test/moves/imprison.test.ts b/src/test/moves/imprison.test.ts new file mode 100644 index 00000000000..abb4b3cac6c --- /dev/null +++ b/src/test/moves/imprison.test.ts @@ -0,0 +1,98 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { ArenaTagType } from "#enums/arena-tag-type"; + +describe("Moves - Imprison", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.IMPRISON, Moves.SPLASH, Moves.GROWL]) + .enemySpecies(Species.SHUCKLE) + .moveset([Moves.TRANSFORM, Moves.SPLASH]); + }); + + it("Pokemon under Imprison cannot use shared moves", async () => { + await game.classicMode.startBattle([Species.REGIELEKI]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TRANSFORM); + await game.forceEnemyMove(Moves.IMPRISON); + await game.toNextTurn(); + const playerMoveset = playerPokemon.getMoveset().map(x => x?.moveId); + const enemyMoveset = game.scene.getEnemyPokemon()!.getMoveset().map(x => x?.moveId); + expect(enemyMoveset.includes(playerMoveset[0])).toBeTruthy(); + const imprisonArenaTag = game.scene.arena.getTag(ArenaTagType.IMPRISON); + const imprisonBattlerTag = playerPokemon.getTag(BattlerTagType.IMPRISON); + expect(imprisonArenaTag).toBeDefined(); + expect(imprisonBattlerTag).toBeDefined(); + + // Second turn, Imprison forces Struggle to occur + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const move1 = playerPokemon.getLastXMoves(1)[0]!; + expect(move1.move).toBe(Moves.STRUGGLE); + }); + + it("Imprison applies to Pokemon switched into Battle", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const playerPokemon1 = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.IMPRISON); + await game.toNextTurn(); + const imprisonArenaTag = game.scene.arena.getTag(ArenaTagType.IMPRISON); + const imprisonBattlerTag1 = playerPokemon1.getTag(BattlerTagType.IMPRISON); + expect(imprisonArenaTag).toBeDefined(); + expect(imprisonBattlerTag1).toBeDefined(); + + // Second turn, Imprison forces Struggle to occur + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const playerPokemon2 = game.scene.getPlayerPokemon()!; + const imprisonBattlerTag2 = playerPokemon2.getTag(BattlerTagType.IMPRISON); + expect(playerPokemon1).not.toEqual(playerPokemon2); + expect(imprisonBattlerTag2).toBeDefined(); + }); + + it("The effects of Imprison only end when the source is no longer active", async () => { + game.override.moveset([Moves.SPLASH, Moves.IMPRISON]); + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + game.move.select(Moves.IMPRISON); + await game.forceEnemyMove(Moves.GROWL); + await game.toNextTurn(); + expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeDefined(); + expect(enemyPokemon.getTag(BattlerTagType.IMPRISON)).toBeDefined(); + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.isActive(true)).toBeFalsy(); + expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeUndefined(); + expect(enemyPokemon.getTag(BattlerTagType.IMPRISON)).toBeUndefined(); + }); +}); diff --git a/src/test/moves/taunt.test.ts b/src/test/moves/taunt.test.ts new file mode 100644 index 00000000000..50bb2fee9df --- /dev/null +++ b/src/test/moves/taunt.test.ts @@ -0,0 +1,54 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { MoveResult } from "#app/field/pokemon"; +import { BattlerTagType } from "#enums/battler-tag-type"; + +describe("Moves - Taunt", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.TAUNT, Moves.SPLASH]) + .enemySpecies(Species.SHUCKLE) + .moveset([Moves.GROWL]); + }); + + it("Pokemon should not be able to use Status Moves", async () => { + await game.classicMode.startBattle([Species.REGIELEKI]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + // First turn, Player Pokemon succeeds using Growl without Taunt + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.TAUNT); + await game.toNextTurn(); + const move1 = playerPokemon.getLastXMoves(1)[0]!; + expect(move1.move).toBe(Moves.GROWL); + expect(move1.result).toBe(MoveResult.SUCCESS); + expect(playerPokemon?.getTag(BattlerTagType.TAUNT)).toBeDefined(); + + // Second turn, Taunt forces Struggle to occur + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const move2 = playerPokemon.getLastXMoves(1)[0]!; + expect(move2.move).toBe(Moves.STRUGGLE); + }); +}); diff --git a/src/test/moves/torment.test.ts b/src/test/moves/torment.test.ts new file mode 100644 index 00000000000..f725f2bc34a --- /dev/null +++ b/src/test/moves/torment.test.ts @@ -0,0 +1,64 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { MoveResult } from "#app/field/pokemon"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; + +describe("Moves - Torment", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.TORMENT, Moves.SPLASH]) + .enemySpecies(Species.SHUCKLE) + .enemyLevel(30) + .moveset([Moves.TACKLE]) + .ability(Abilities.BALL_FETCH); + }); + + it("Pokemon should not be able to use the same move consecutively", async () => { + await game.classicMode.startBattle([Species.CHANSEY]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + // First turn, Player Pokemon uses Tackle successfully + game.move.select(Moves.TACKLE); + await game.forceEnemyMove(Moves.TORMENT); + await game.toNextTurn(); + const move1 = playerPokemon.getLastXMoves(1)[0]!; + expect(move1.move).toBe(Moves.TACKLE); + expect(move1.result).toBe(MoveResult.SUCCESS); + expect(playerPokemon?.getTag(BattlerTagType.TORMENT)).toBeDefined(); + + // Second turn, Torment forces Struggle to occur + game.move.select(Moves.TACKLE); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const move2 = playerPokemon.getLastXMoves(1)[0]!; + expect(move2.move).toBe(Moves.STRUGGLE); + + // Third turn, Tackle can be used. + game.move.select(Moves.TACKLE); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to(TurnEndPhase); + const move3 = playerPokemon.getLastXMoves(1)[0]!; + expect(move3.move).toBe(Moves.TACKLE); + }); +});