[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>
This commit is contained in:
parent
e959595471
commit
a919b9c0af
|
@ -2193,8 +2193,14 @@ export default class BattleScene extends SceneBase {
|
||||||
return true;
|
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<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||||
|
return this.phaseQueue.find(phaseFilter) as P;
|
||||||
}
|
}
|
||||||
|
|
||||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
||||||
|
|
|
@ -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<MovePhase>((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 failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY);
|
||||||
|
|
||||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||||
|
|
||||||
const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax();
|
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 failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||||
const cancelled = new Utils.BooleanHolder(false);
|
const cancelled = new Utils.BooleanHolder(false);
|
||||||
user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled));
|
user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled));
|
||||||
|
@ -7925,7 +7955,10 @@ export function initMoves() {
|
||||||
.attr(AbilityGiveAttr),
|
.attr(AbilityGiveAttr),
|
||||||
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
|
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||||
.ignoresProtect()
|
.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)
|
new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
|
||||||
.soundBased()
|
.soundBased()
|
||||||
.partial(),
|
.partial(),
|
||||||
|
|
|
@ -66,5 +66,6 @@
|
||||||
"revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!",
|
"revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!",
|
||||||
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
|
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
|
||||||
"exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!",
|
"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!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,5 +67,6 @@
|
||||||
"revivalBlessing": "{{pokemonName}} was revived!",
|
"revivalBlessing": "{{pokemonName}} was revived!",
|
||||||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||||
"safeguard": "{{targetName}} is protected by Safeguard!"
|
"safeguard": "{{targetName}} is protected by Safeguard!",
|
||||||
}
|
"afterYou": "{{pokemonName}} took the kind offer!"
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
Loading…
Reference in New Issue