mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-14 19:39:29 +02:00
Fixed Encore interactions with Magic Bounce, Magic Coat, etc etc
This commit is contained in:
parent
69157f07bc
commit
664bf555bd
@ -28,6 +28,8 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||||
import type { Move } from "#moves/move";
|
import type { Move } from "#moves/move";
|
||||||
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||||
import type { MovePhase } from "#phases/move-phase";
|
import type { MovePhase } from "#phases/move-phase";
|
||||||
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
||||||
@ -1242,13 +1244,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
public moveId: MoveId;
|
public moveId: MoveId;
|
||||||
|
|
||||||
constructor(sourceId: number) {
|
constructor(sourceId: number) {
|
||||||
super(
|
super(BattlerTagType.ENCORE, BattlerTagLapseType.TURN_END, 3, MoveId.ENCORE, sourceId);
|
||||||
BattlerTagType.ENCORE,
|
|
||||||
[BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE],
|
|
||||||
3,
|
|
||||||
MoveId.ENCORE,
|
|
||||||
sourceId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
||||||
@ -1278,33 +1274,50 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon);
|
// If the target has not moved yet,
|
||||||
if (movePhase) {
|
// replace their upcoming move with the encored move against randomized targets
|
||||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
const movePhase = globalScene.phaseManager.findPhase(
|
||||||
if (movesetMove) {
|
(m): m is MovePhase => m.is("MovePhase") && m.pokemon === pokemon,
|
||||||
const lastMove = pokemon.getLastXMoves(1)[0];
|
);
|
||||||
globalScene.phaseManager.tryReplacePhase(
|
if (!movePhase) {
|
||||||
m => m.is("MovePhase") && m.pokemon === pokemon,
|
return;
|
||||||
globalScene.phaseManager.create(
|
|
||||||
"MovePhase",
|
|
||||||
pokemon,
|
|
||||||
lastMove.targets ?? [],
|
|
||||||
movesetMove,
|
|
||||||
MoveUseMode.NORMAL,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the prior move in the moveset. If it isn't there (presumably due to move forgetting),
|
||||||
|
// just make a new one for time being as the tag will be removed on turn end.
|
||||||
|
// TODO: Investigate this...
|
||||||
|
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId) ?? new PokemonMove(this.moveId);
|
||||||
|
|
||||||
|
const moveTargets = getMoveTargets(pokemon, movePhase.move.moveId);
|
||||||
|
// Spread moves and ones with only 1 valid target will use their normal targeting.
|
||||||
|
// If not, target a random enemy in our target list
|
||||||
|
const targets =
|
||||||
|
moveTargets.multiple || moveTargets.targets.length === 1
|
||||||
|
? moveTargets.targets
|
||||||
|
: [moveTargets.targets[pokemon.randBattleSeedInt(moveTargets.targets.length)]];
|
||||||
|
|
||||||
|
globalScene.phaseManager.tryReplacePhase(
|
||||||
|
m => m.is("MovePhase") && m.pokemon === pokemon,
|
||||||
|
globalScene.phaseManager.create(
|
||||||
|
"MovePhase",
|
||||||
|
pokemon,
|
||||||
|
targets,
|
||||||
|
movesetMove,
|
||||||
|
movePhase.useMode,
|
||||||
|
movePhase.isForcedLast(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the encored move has run out of PP, Encore ends early. Otherwise, Encore lapses based on the AFTER_MOVE battler tag lapse type.
|
* If the encored move has run out of PP, Encore ends early.
|
||||||
|
* Otherwise, Encore's duration reduces at the end of the turn.
|
||||||
* @returns `true` to persist | `false` to end and be removed
|
* @returns `true` to persist | `false` to end and be removed
|
||||||
*/
|
*/
|
||||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||||
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
if (isNullOrUndefined(encoredMove) || encoredMove.getPpRatio() <= 0) {
|
||||||
return !isNullOrUndefined(encoredMove) && encoredMove.getPpRatio() > 0;
|
return false;
|
||||||
}
|
}
|
||||||
return super.lapse(pokemon, lapseType);
|
return super.lapse(pokemon, lapseType);
|
||||||
}
|
}
|
||||||
@ -1489,12 +1502,8 @@ export class MinimizeTag extends SerializableBattlerTag {
|
|||||||
|
|
||||||
export class DrowsyTag extends SerializableBattlerTag {
|
export class DrowsyTag extends SerializableBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.DROWSY;
|
public override readonly tagType = BattlerTagType.DROWSY;
|
||||||
constructor() {
|
constructor(sourceId: number) {
|
||||||
super(BattlerTagType.DROWSY, BattlerTagLapseType.TURN_END, 2, MoveId.YAWN);
|
super(BattlerTagType.DROWSY, BattlerTagLapseType.TURN_END, 2, MoveId.YAWN, sourceId);
|
||||||
}
|
|
||||||
|
|
||||||
canAdd(pokemon: Pokemon): boolean {
|
|
||||||
return globalScene.arena.terrain?.terrainType !== TerrainType.ELECTRIC || !pokemon.isGrounded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(pokemon: Pokemon): void {
|
onAdd(pokemon: Pokemon): void {
|
||||||
@ -1509,6 +1518,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
|||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (!super.lapse(pokemon, lapseType)) {
|
if (!super.lapse(pokemon, lapseType)) {
|
||||||
|
// TODO: Safeguard should not prevent yawn from setting sleep after tag use
|
||||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3675,7 +3685,7 @@ export function getBattlerTag(
|
|||||||
case BattlerTagType.AQUA_RING:
|
case BattlerTagType.AQUA_RING:
|
||||||
return new AquaRingTag();
|
return new AquaRingTag();
|
||||||
case BattlerTagType.DROWSY:
|
case BattlerTagType.DROWSY:
|
||||||
return new DrowsyTag();
|
return new DrowsyTag(sourceId);
|
||||||
case BattlerTagType.TRAPPED:
|
case BattlerTagType.TRAPPED:
|
||||||
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||||
case BattlerTagType.NO_RETREAT:
|
case BattlerTagType.NO_RETREAT:
|
||||||
|
@ -11,6 +11,7 @@ import { WeakenMoveTypeTag } from "#data/arena-tag";
|
|||||||
import { MoveChargeAnim } from "#data/battle-anims";
|
import { MoveChargeAnim } from "#data/battle-anims";
|
||||||
import {
|
import {
|
||||||
CommandedTag,
|
CommandedTag,
|
||||||
|
DrowsyTag,
|
||||||
EncoreTag,
|
EncoreTag,
|
||||||
GulpMissileTag,
|
GulpMissileTag,
|
||||||
HelpingHandTag,
|
HelpingHandTag,
|
||||||
@ -5689,6 +5690,31 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to implement {@linkcode MoveId.YAWN}.
|
||||||
|
* Yawn adds a BattlerTag to its target that puts them to sleep at the end
|
||||||
|
* of the next turn, retaining many of the same checks as normal status setting moves.
|
||||||
|
*/
|
||||||
|
export class YawnAttr extends AddBattlerTagAttr {
|
||||||
|
constructor() {
|
||||||
|
super(BattlerTagType.DROWSY, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCondition(): MoveConditionFunc {
|
||||||
|
return (user, target, move) => {
|
||||||
|
if (!super.getCondition()!(user, target, move)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statused opponents or ones with safeguard active use a generic failure message
|
||||||
|
if (target.status || target.isSafeguarded(user)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a {@link https://bulbapedia.bulbagarden.net/wiki/Seeding | Seeding} effect to the target
|
* Adds a {@link https://bulbapedia.bulbagarden.net/wiki/Seeding | Seeding} effect to the target
|
||||||
* as seen with Leech Seed and Sappy Seed.
|
* as seen with Leech Seed and Sappy Seed.
|
||||||
@ -5916,8 +5942,8 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
|||||||
for (const turnMove of user.getLastXMoves(-1).slice()) {
|
for (const turnMove of user.getLastXMoves(-1).slice()) {
|
||||||
if (
|
if (
|
||||||
// Quick & Wide guard increment the Protect counter without using it for fail chance
|
// Quick & Wide guard increment the Protect counter without using it for fail chance
|
||||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||||
turnMove.result !== MoveResult.SUCCESS
|
turnMove.result !== MoveResult.SUCCESS
|
||||||
) {
|
) {
|
||||||
break;
|
break;
|
||||||
@ -9377,9 +9403,9 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.BRICK_BREAK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 15, -1, 0, 3)
|
new AttackMove(MoveId.BRICK_BREAK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 15, -1, 0, 3)
|
||||||
.attr(RemoveScreensAttr),
|
.attr(RemoveScreensAttr),
|
||||||
new StatusMove(MoveId.YAWN, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new StatusMove(MoveId.YAWN, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
|
.attr(YawnAttr)
|
||||||
.condition((user, target, move) => !target.status && !target.isSafeguarded(user))
|
.reflectable()
|
||||||
.reflectable(),
|
.edgeCase(), // Should not be blocked by safeguard on turn of use
|
||||||
new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
|
new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
|
||||||
.attr(RemoveHeldItemAttr, false)
|
.attr(RemoveHeldItemAttr, false)
|
||||||
|
@ -4429,14 +4429,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return this Pokemon's move history.
|
* Return this Pokemon's move history.
|
||||||
* Entries are sorted in order of OLDEST to NEWEST
|
* Entries are sorted in order of OLDEST to NEWEST.
|
||||||
* @returns An array of {@linkcode TurnMove}, as described above.
|
* @returns An array of {@linkcode TurnMove}s, as described above.
|
||||||
* @see {@linkcode getLastXMoves}
|
* @see {@linkcode getLastXMoves}
|
||||||
*/
|
*/
|
||||||
public getMoveHistory(): TurnMove[] {
|
public getMoveHistory(): TurnMove[] {
|
||||||
return this.summonData.moveHistory;
|
return this.summonData.moveHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a move to the end of this {@linkcode Pokemon}'s move history,
|
||||||
|
* used to record its most recently executed actions.
|
||||||
|
* @param turnMove - The {@linkcode TurnMove} to add
|
||||||
|
*/
|
||||||
public pushMoveHistory(turnMove: TurnMove): void {
|
public pushMoveHistory(turnMove: TurnMove): void {
|
||||||
if (!this.isOnField()) {
|
if (!this.isOnField()) {
|
||||||
return;
|
return;
|
||||||
|
@ -414,6 +414,8 @@ export class PhaseManager {
|
|||||||
* @param phaseFilter filter function to use to find the wanted phase
|
* @param phaseFilter filter function to use to find the wanted phase
|
||||||
* @returns the found phase or undefined if none found
|
* @returns the found phase or undefined if none found
|
||||||
*/
|
*/
|
||||||
|
findPhase<P extends Phase = Phase>(phaseFilter: (phase: Phase) => phase is P): P | undefined;
|
||||||
|
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined;
|
||||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||||
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
||||||
}
|
}
|
||||||
|
@ -175,11 +175,6 @@ export class CommandPhase extends FieldPhase {
|
|||||||
|
|
||||||
this.checkCommander();
|
this.checkCommander();
|
||||||
|
|
||||||
const playerPokemon = this.getPokemon();
|
|
||||||
|
|
||||||
// Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing.
|
|
||||||
playerPokemon.lapseTag(BattlerTagType.ENCORE);
|
|
||||||
|
|
||||||
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
|
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
|
||||||
this.end();
|
this.end();
|
||||||
return;
|
return;
|
||||||
|
@ -1,296 +0,0 @@
|
|||||||
import { allAbilities, allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("Abilities - Magic Bounce", () => {
|
|
||||||
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
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.moveset([MoveId.GROWL, MoveId.SPLASH])
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.MAGIC_BOUNCE)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reflect basic status moves", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce moves while the target is in the semi-invulnerable state", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.move.forceEnemyMove(MoveId.FLY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should individually bounce back multi-target moves", async () => {
|
|
||||||
game.override.battleStyle("double");
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL, 0);
|
|
||||||
game.move.use(MoveId.SPLASH, 1);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
const user = game.scene.getPlayerField()[0];
|
|
||||||
expect(user.getStatStage(Stat.ATK)).toBe(-2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should still bounce back a move that would otherwise fail", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
game.field.getEnemyPokemon().setStatStage(Stat.ATK, -6);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back a move that was just bounced", async () => {
|
|
||||||
game.override.ability(AbilityId.MAGIC_BOUNCE);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should receive the stat change after reflecting a move back to a mirror armor user", async () => {
|
|
||||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back a move from a mold breaker user", async () => {
|
|
||||||
game.override.ability(AbilityId.MOLD_BREAKER);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce back a spread status move against both pokemon", async () => {
|
|
||||||
game.override.battleStyle("double").enemyMoveset([MoveId.SPLASH]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL, 0);
|
|
||||||
game.move.use(MoveId.SPLASH, 1);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => {
|
|
||||||
game.override.battleStyle("double").moveset([MoveId.SPIKES]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce spikes even when the target is protected", async () => {
|
|
||||||
game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.PROTECT]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce spikes when the target is in the semi-invulnerable state", async () => {
|
|
||||||
game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.FLY]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)!["layers"]).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back curse", async () => {
|
|
||||||
game.override.moveset([MoveId.CURSE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.CURSE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getTag(BattlerTagType.CURSED)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: enable when Magic Bounce is fixed to properly reset the hit count
|
|
||||||
it("should not cause encore to be interrupted after bouncing", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
|
|
||||||
const playerAbilitySpy = game.field.mockAbility(playerPokemon, AbilityId.MOLD_BREAKER);
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.use(MoveId.ENCORE);
|
|
||||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
playerAbilitySpy.mockRestore();
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: encore is failing if the last move was virtual.
|
|
||||||
it("should not cause the bounced move to count for encore", async () => {
|
|
||||||
game.override
|
|
||||||
.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE])
|
|
||||||
.enemyMoveset([MoveId.GROWL, MoveId.TACKLE])
|
|
||||||
.enemyAbility(AbilityId.MAGIC_BOUNCE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
|
|
||||||
vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[AbilityId.MOLD_BREAKER]);
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
game.move.use(MoveId.ENCORE);
|
|
||||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Move to a stomping tantrum test file
|
|
||||||
it("should cause stomping tantrum to double in power when the last move was bounced", async () => {
|
|
||||||
game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
|
||||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
|
||||||
|
|
||||||
game.move.select(MoveId.CHARM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.STOMPING_TANTRUM);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: stomping tantrum should consider moves that were bounced
|
|
||||||
it("should boost enemy's stomping tantrum after failed bounce", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
|
||||||
|
|
||||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
|
||||||
|
|
||||||
// Spore gets reflected back onto us
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.CHARM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.STOMPING_TANTRUM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should respect immunities when bouncing a move", async () => {
|
|
||||||
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
|
|
||||||
game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PHANPY]);
|
|
||||||
|
|
||||||
// Turn 1 - thunder wave immunity test
|
|
||||||
game.move.select(MoveId.THUNDER_WAVE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
|
|
||||||
// Turn 2 - soundproof immunity test
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce back a move before the accuracy check", async () => {
|
|
||||||
game.override.moveset([MoveId.SPORE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const attacker = game.field.getPlayerPokemon();
|
|
||||||
|
|
||||||
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should take the accuracy of the magic bounce user into account", async () => {
|
|
||||||
game.override.moveset([MoveId.SPORE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
const opponent = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
@ -3,7 +3,9 @@ import { BattlerIndex } from "#enums/battler-index";
|
|||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
@ -31,7 +33,6 @@ describe("Moves - Encore", () => {
|
|||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.enemyMoveset([MoveId.SPLASH, MoveId.TACKLE])
|
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(100);
|
.enemyLevel(100);
|
||||||
});
|
});
|
||||||
@ -41,74 +42,93 @@ describe("Moves - Encore", () => {
|
|||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.ENCORE);
|
game.move.use(MoveId.ENCORE);
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
expect(enemyPokemon).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
// The enemy AI would normally be inclined to use Tackle, but should be
|
expect(enemyPokemon.isMoveRestricted(MoveId.TACKLE)).toBe(true);
|
||||||
// forced into using Splash.
|
expect(enemyPokemon.isMoveRestricted(MoveId.SPLASH)).toBe(false);
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
|
||||||
|
|
||||||
expect(enemyPokemon.getLastXMoves().every(turnMove => turnMove.move === MoveId.SPLASH)).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should fail against the following moves:", () => {
|
it("should override any pending move phases with the Encored move, while still consuming PP", async () => {
|
||||||
it.each([
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
{ moveId: MoveId.TRANSFORM, name: "Transform", delay: false },
|
|
||||||
{ moveId: MoveId.MIMIC, name: "Mimic", delay: true },
|
|
||||||
{ moveId: MoveId.SKETCH, name: "Sketch", delay: true },
|
|
||||||
{ moveId: MoveId.ENCORE, name: "Encore", delay: false },
|
|
||||||
{ moveId: MoveId.STRUGGLE, name: "Struggle", delay: false },
|
|
||||||
])("$name", async ({ moveId, delay }) => {
|
|
||||||
game.override.enemyMoveset(moveId);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
// Fake enemy having used tackle the turn prior
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
enemy.pushMoveHistory({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
game.move.use(MoveId.ENCORE);
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
if (delay) {
|
expect(enemy).toHaveUsedMove({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
game.move.select(MoveId.SPLASH);
|
expect(enemy.isMoveRestricted(MoveId.TACKLE)).toBe(true);
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
expect(enemy.isMoveRestricted(MoveId.SPLASH)).toBe(false);
|
||||||
await game.toNextTurn();
|
|
||||||
}
|
|
||||||
|
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
|
|
||||||
const turnOrder = delay ? [BattlerIndex.PLAYER, BattlerIndex.ENEMY] : [BattlerIndex.ENEMY, BattlerIndex.PLAYER];
|
|
||||||
await game.setTurnOrder(turnOrder);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
|
||||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Pokemon under both Encore and Torment should alternate between Struggle and restricted move", async () => {
|
it.todo("should end at turn end if the user forgets the Encored move");
|
||||||
const turnOrder = [BattlerIndex.ENEMY, BattlerIndex.PLAYER];
|
|
||||||
game.override.moveset([MoveId.ENCORE, MoveId.TORMENT, MoveId.SPLASH]);
|
// TODO: Make test (presumably involving Spite)
|
||||||
|
it.todo("should end immediately if the move runs out of PP");
|
||||||
|
|
||||||
|
const invalidMoves = [...invalidEncoreMoves].map(m => ({
|
||||||
|
name: MoveId[m],
|
||||||
|
move: m,
|
||||||
|
}));
|
||||||
|
it.each(invalidMoves)("should fail if the target's last move is $name", async ({ move }) => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
enemy.pushMoveHistory({ move, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||||
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the target has not made a move", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||||
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should force a Tormented target to alternate between Struggle and the Encored move", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
await game.setTurnOrder(turnOrder);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
game.move.select(MoveId.TORMENT);
|
|
||||||
await game.setTurnOrder(turnOrder);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.TORMENT)).toBeDefined();
|
|
||||||
|
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
|
||||||
|
game.move.use(MoveId.TORMENT);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.setTurnOrder(turnOrder);
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
expect(enemy).toHaveBattlerTag(BattlerTagType.TORMENT);
|
||||||
const lastMove = enemyPokemon.getLastXMoves()[0];
|
|
||||||
expect(lastMove?.move).toBe(MoveId.STRUGGLE);
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveUsedMove(MoveId.STRUGGLE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ import { MoveUseMode } from "#enums/move-use-mode";
|
|||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import type { EnemyPokemon } from "#field/pokemon";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
@ -93,23 +94,25 @@ describe("Moves - Reflecting effects", () => {
|
|||||||
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, 0);
|
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should receive the stat change after reflecting a move back to a mirror armor user", async () => {
|
it("should take precedence over Mirror Armor", async () => {
|
||||||
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
game.move.use(MoveId.GROWL);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, -1);
|
const enemy = game.field.getPlayerPokemon();
|
||||||
|
expect(enemy).toHaveStatStage(Stat.ATK, -1);
|
||||||
|
expect(enemy).not.toHaveAbilityApplied(AbilityId.MIRROR_ARMOR);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not bounce back curse", async () => {
|
it("should not bounce back non-reflectable effects", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
||||||
|
|
||||||
game.move.use(MoveId.CURSE);
|
game.move.use(MoveId.CURSE);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getTag(BattlerTagType.CURSED)).toBeDefined();
|
expect(game.field.getEnemyPokemon()).toHaveBattlerTag(BattlerTagType.CURSED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not cause encore to be interrupted after bouncing", async () => {
|
it("should not cause encore to be interrupted after bouncing", async () => {
|
||||||
@ -141,23 +144,25 @@ describe("Moves - Reflecting effects", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not cause the bounced move to count for encore", async () => {
|
it("should not cause the bounced move to count for encore", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
game.override.battleStyle("double").enemyAbility(AbilityId.MAGIC_BOUNCE);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.ABRA]);
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
// Fake abra having mold breaker and the enemy having used Tackle
|
||||||
|
const [abra, enemy1, enemy2] = game.scene.getField();
|
||||||
|
game.field.mockAbility(abra, AbilityId.MOLD_BREAKER);
|
||||||
|
enemy1.pushMoveHistory({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
// turn 1
|
// turn 1: Magikarp uses growl as Abra attempts to encore
|
||||||
game.move.use(MoveId.GROWL);
|
game.move.use(MoveId.GROWL, BattlerIndex.PLAYER);
|
||||||
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
game.move.use(MoveId.ENCORE, BattlerIndex.PLAYER_2);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.killPokemon(enemy2 as EnemyPokemon);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
// turn 2
|
// Encore locked into Tackle, replacing the enemy's Growl with another Tackle
|
||||||
game.move.use(MoveId.ENCORE);
|
expect(enemy1.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
||||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, useMode: MoveUseMode.NORMAL });
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should boost stomping tantrum after a failed bounce", async () => {
|
it("should boost stomping tantrum after a failed bounce", async () => {
|
||||||
@ -171,6 +176,7 @@ describe("Moves - Reflecting effects", () => {
|
|||||||
game.move.use(MoveId.YAWN);
|
game.move.use(MoveId.YAWN);
|
||||||
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expect(enemy).toHaveUsedMove({ move: MoveId.YAWN, result: MoveResult.FAIL, useMode: MoveUseMode.REFLECTED });
|
expect(enemy).toHaveUsedMove({ move: MoveId.YAWN, result: MoveResult.FAIL, useMode: MoveUseMode.REFLECTED });
|
||||||
|
|
||||||
game.move.use(MoveId.SPLASH);
|
game.move.use(MoveId.SPLASH);
|
||||||
@ -303,6 +309,17 @@ describe("Moves - Reflecting effects", () => {
|
|||||||
expect(karp1).toHaveStatStage(Stat.ATK, -1);
|
expect(karp1).toHaveStatStage(Stat.ATK, -1);
|
||||||
expect(karp2).toHaveStatStage(Stat.ATK, -1);
|
expect(karp2).toHaveStatStage(Stat.ATK, -1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should bounce spikes even when the target is protected", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPIKES);
|
||||||
|
await game.move.forceEnemyMove(MoveId.PROTECT);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// TODO: Replace this with `expect(game).toHaveArenaTag({tagType: ArenaTagType.SPIKES, side: ArenaTagSide.PLAYER, layers: 1})
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Magic Coat", () => {
|
describe("Magic Coat", () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user