From e82a410d60f013333a42c8b715595f82fd93cc5a Mon Sep 17 00:00:00 2001 From: torranx Date: Mon, 9 Sep 2024 04:21:48 +0800 Subject: [PATCH] fully implement throat chop --- src/data/battler-tags.ts | 51 ++++++++++++++++++++++++++-- src/data/move.ts | 2 +- src/enums/battler-tag-type.ts | 1 + src/locales/en/battle.json | 2 ++ src/test/moves/throat_chop.test.ts | 54 ++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/test/moves/throat_chop.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ddb85600c18..8436ebb4339 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -107,8 +107,8 @@ export interface TerrainBattlerTag { * to select restricted moves. */ export abstract class MoveRestrictionBattlerTag extends BattlerTag { - constructor(tagType: BattlerTagType, turnCount: integer, sourceMove?: Moves, sourceId?: integer) { - super(tagType, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], turnCount, sourceMove, sourceId); + constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType | BattlerTagLapseType[], turnCount: integer, sourceMove?: Moves, sourceId?: integer) { + super(tagType, lapseType, turnCount, sourceMove, sourceId); } /** @override */ @@ -158,6 +158,49 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { abstract interruptedText(pokemon: Pokemon, move: Moves): string; } +/** + * Tag representing the "Throat Chop" effect. Pokemon with this tag cannot use sound-based moves. + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Throat_Chop_(move) | Throat Chop} + * @extends MoveRestrictionBattlerTag + */ +export class ThroatChoppedTag extends MoveRestrictionBattlerTag { + constructor() { + super(BattlerTagType.THROAT_CHOPPED, [ BattlerTagLapseType.TURN_END, BattlerTagLapseType.PRE_MOVE ], 2, Moves.THROAT_CHOP); + } + + /** + * Checks if a move is restricted by Throat Chop. + * @override + * @param {Moves} move the move to check for sound-based restriction + * @returns true if the move is sound-based + */ + override isMoveRestricted(move: Moves): boolean { + return allMoves[move].hasFlag(MoveFlags.SOUND_BASED); + } + + /** + * Shows a message when the player attempts to select a move that is restricted by Throat Chop. + * @override + * @param {Pokemon} pokemon the pokemon that is attempting to select the restricted move + * @param {Moves} move the move that is being restricted + * @returns the message to display when the player attempts to select the restricted move + */ + override selectionDeniedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } + + /** + * Shows a message when a move is interrupted by Throat Chop. + * @override + * @param {Pokemon} pokemon the interrupted pokemon + * @param {Moves} move the move that was interrupted + * @returns the message to display when the move is interrupted + */ + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:throatChopInterruptedMove", { pokemonName: getPokemonNameWithAffix(pokemon) }); + } +} + /** * Tag representing the "disabling" effect performed by {@linkcode Moves.DISABLE} and {@linkcode Abilities.CURSED_BODY}. * When the tag is added, the last-used move of the tag holder is set as the disabled move. @@ -167,7 +210,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag { private moveId: Moves = Moves.NONE; constructor(sourceId: number) { - super(BattlerTagType.DISABLED, 4, Moves.DISABLE, sourceId); + super(BattlerTagType.DISABLED, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 4, Moves.DISABLE, sourceId); } /** @override */ @@ -2125,6 +2168,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.GULP_MISSILE_ARROKUDA: case BattlerTagType.GULP_MISSILE_PIKACHU: return new GulpMissileTag(tagType, sourceMove); + case BattlerTagType.THROAT_CHOPPED: + return new ThroatChoppedTag(); 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 19014c0eb30..ba38c800bb1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8404,7 +8404,7 @@ export function initMoves() { .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))), new AttackMove(Moves.THROAT_CHOP, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7) - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.THROAT_CHOPPED, false, false), new AttackMove(Moves.POLLEN_PUFF, Type.BUG, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) .attr(StatusCategoryOnAllyAttr) .attr(HealOnAllyAttr, 0.5, true, false) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index a2bcf9e4c0e..7546717ebfb 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -73,4 +73,5 @@ export enum BattlerTagType { SHELL_TRAP = "SHELL_TRAP", DRAGON_CHEER = "DRAGON_CHEER", NO_RETREAT = "NO_RETREAT", + THROAT_CHOPPED = "THROAT_CHOPPED", } diff --git a/src/locales/en/battle.json b/src/locales/en/battle.json index 120ac749acb..0aabaacd99c 100644 --- a/src/locales/en/battle.json +++ b/src/locales/en/battle.json @@ -44,7 +44,9 @@ "moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.", "moveNoPP": "There's no PP left for\nthis move!", "moveDisabled": "{{moveName}} is disabled!", + "moveCannotBeSelected": "{{moveName}} cannot be selected!", "disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!", + "throatChopInterruptedMove": "The effects of Throat Chop prevent\n{{pokemonName}} from using certain moves!", "noPokeballForce": "An unseen force\nprevents using Poké Balls.", "noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!", "noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!", diff --git a/src/test/moves/throat_chop.test.ts b/src/test/moves/throat_chop.test.ts new file mode 100644 index 00000000000..6cd501d1654 --- /dev/null +++ b/src/test/moves/throat_chop.test.ts @@ -0,0 +1,54 @@ +import { BattlerIndex } from "#app/battle"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Moves - Throat Chop", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(Array(4).fill(Moves.GROWL)) + .battleType("single") + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Array(4).fill(Moves.THROAT_CHOP)); + }); + + it("prevents the target from using sound-based moves for two turns", async () => { + await game.classicMode.startBattle([Species.MAGIKARP]); + + game.move.select(Moves.GROWL); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + + // First turn, move is interrupted + await game.phaseInterceptor.to("TurnEndPhase"); + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(0); + + // Second turn, struggle if no valid moves + await game.toNextTurn(); + + game.move.select(Moves.GROWL); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(game.scene.getEnemyPokemon()!.hp).toBeLessThan(game.scene.getEnemyPokemon()!.getMaxHp()); + }, TIMEOUT); +});