From a919b9c0afc0ea3d49af2aff00bfb503ccc497fe Mon Sep 17 00:00:00 2001 From: Raidette <73134872+Raidette@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:35:04 +0200 Subject: [PATCH] [Move] Implement After You (#1789) * Complete after you implementation (no localization) * reset override changes * Remove hardcoded English text, add tests * Fix test * Make sure phases occur in the correct order * fix after-you issues - fix i18n interpolation ot state "target name" and not "pokemon name" as the target takes the offer, not the user - fix some tsdocs - add override to apply - update scene.findPhase to be able to use generic types. Add tsdocs * add move-trigger.afterYou for DE * fix after_you.test.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --- src/battle-scene.ts | 10 ++++- src/data/move.ts | 35 ++++++++++++++++- src/locales/de/move-trigger.json | 3 +- src/locales/en/move-trigger.json | 5 ++- src/test/moves/after_you.test.ts | 65 ++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/test/moves/after_you.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index ff4258a13f5..72778fa8589 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2193,8 +2193,14 @@ export default class BattleScene extends SceneBase { return true; } - findPhase(phaseFilter: (phase: Phase) => boolean): Phase | undefined { - return this.phaseQueue.find(phaseFilter); + /** + * Find a specific {@linkcode Phase} in the phase queue. + * + * @param phaseFilter filter function to use to find the wanted phase + * @returns the found phase or undefined if none found + */ + findPhase

(phaseFilter: (phase: P) => boolean): P | undefined { + return this.phaseQueue.find(phaseFilter) as P; } tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { diff --git a/src/data/move.ts b/src/data/move.ts index d9e385fdd0e..7800d6df12a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6272,12 +6272,42 @@ export class VariableTargetAttr extends MoveAttr { } } +/** + * Attribute for {@linkcode Moves.AFTER_YOU} + * + * [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move)) + */ +export class AfterYouAttr extends MoveEffectAttr { + /** + * Allows the target of this move to act right after the user. + * + * @param user {@linkcode Pokemon} that is using the move. + * @param target {@linkcode Pokemon} that will move right after this move is used. + * @param move {@linkcode Move} {@linkcode Moves.AFTER_YOU} + * @param _args N/A + * @returns true + */ + override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { + user.scene.queueMessage(i18next.t("moveTriggers:afterYou", {targetName: getPokemonNameWithAffix(target)})); + + //Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete. + const nextAttackPhase = target.scene.findPhase((phase) => phase.pokemon === target); + if (nextAttackPhase && target.scene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { + target.scene.prependToPhase(new MovePhase(target.scene, target, [...nextAttackPhase.targets], nextAttackPhase.move), MovePhase); + } + + return true; + } +} + const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax(); +const failIfSingleBattle: MoveConditionFunc = (user, target, move) => user.scene.currentBattle.double; + const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const cancelled = new Utils.BooleanHolder(false); user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); @@ -7925,7 +7955,10 @@ export function initMoves() { .attr(AbilityGiveAttr), new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() - .unimplemented(), + .target(MoveTarget.NEAR_OTHER) + .condition(failIfSingleBattle) + .condition((user, target, move) => !target.turnData.acted) + .attr(AfterYouAttr), new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) .soundBased() .partial(), diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json index 61283c9e62e..01b22429fb3 100644 --- a/src/locales/de/move-trigger.json +++ b/src/locales/de/move-trigger.json @@ -66,5 +66,6 @@ "revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", - "safeguard": "{{targetName}} wird durch Bodyguard geschützt!" + "safeguard": "{{targetName}} wird durch Bodyguard geschützt!", + "afterYou": "{{targetName}} lässt sich auf Galanterie ein!" } diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json index 867905c5a9f..375ea354d33 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -67,5 +67,6 @@ "revivalBlessing": "{{pokemonName}} was revived!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", - "safeguard": "{{targetName}} is protected by Safeguard!" -} \ No newline at end of file + "safeguard": "{{targetName}} is protected by Safeguard!", + "afterYou": "{{pokemonName}} took the kind offer!" +} diff --git a/src/test/moves/after_you.test.ts b/src/test/moves/after_you.test.ts new file mode 100644 index 00000000000..efce1b28a17 --- /dev/null +++ b/src/test/moves/after_you.test.ts @@ -0,0 +1,65 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#app/enums/abilities"; +import { MoveResult } from "#app/field/pokemon"; +import { MovePhase } from "#app/phases/move-phase"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - After You", () => { + 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") + .enemyLevel(5) + .enemySpecies(Species.PIKACHU) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .ability(Abilities.BALL_FETCH) + .moveset([Moves.AFTER_YOU, Moves.SPLASH]); + }); + + it("makes the target move immediately after the user", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.SHUCKLE]); + + game.move.select(Moves.AFTER_YOU, 0, BattlerIndex.PLAYER_2); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.phaseInterceptor.to(MovePhase, false); + const phase = game.scene.getCurrentPhase() as MovePhase; + expect(phase.pokemon).toBe(game.scene.getPlayerField()[1]); + await game.phaseInterceptor.to("MoveEndPhase"); + }, TIMEOUT); + + it("fails if target already moved", async () => { + game.override.enemySpecies(Species.SHUCKLE); + await game.classicMode.startBattle([Species.REGIELEKI, Species.PIKACHU]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.AFTER_YOU, 1, BattlerIndex.PLAYER); + + await game.phaseInterceptor.to("MoveEndPhase"); + await game.phaseInterceptor.to("MoveEndPhase"); + await game.phaseInterceptor.to(MovePhase); + + expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + }, TIMEOUT); +});