mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-14 11:29:28 +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 { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||
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 { MovePhase } from "#phases/move-phase";
|
||||
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
||||
@ -1242,13 +1244,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
public moveId: MoveId;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(
|
||||
BattlerTagType.ENCORE,
|
||||
[BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE],
|
||||
3,
|
||||
MoveId.ENCORE,
|
||||
sourceId,
|
||||
);
|
||||
super(BattlerTagType.ENCORE, BattlerTagLapseType.TURN_END, 3, MoveId.ENCORE, sourceId);
|
||||
}
|
||||
|
||||
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 (movePhase) {
|
||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
if (movesetMove) {
|
||||
const lastMove = pokemon.getLastXMoves(1)[0];
|
||||
globalScene.phaseManager.tryReplacePhase(
|
||||
m => m.is("MovePhase") && m.pokemon === pokemon,
|
||||
globalScene.phaseManager.create(
|
||||
"MovePhase",
|
||||
pokemon,
|
||||
lastMove.targets ?? [],
|
||||
movesetMove,
|
||||
MoveUseMode.NORMAL,
|
||||
),
|
||||
);
|
||||
}
|
||||
// If the target has not moved yet,
|
||||
// replace their upcoming move with the encored move against randomized targets
|
||||
const movePhase = globalScene.phaseManager.findPhase(
|
||||
(m): m is MovePhase => m.is("MovePhase") && m.pokemon === pokemon,
|
||||
);
|
||||
if (!movePhase) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
return !isNullOrUndefined(encoredMove) && encoredMove.getPpRatio() > 0;
|
||||
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
if (isNullOrUndefined(encoredMove) || encoredMove.getPpRatio() <= 0) {
|
||||
return false;
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
@ -1489,12 +1502,8 @@ export class MinimizeTag extends SerializableBattlerTag {
|
||||
|
||||
export class DrowsyTag extends SerializableBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.DROWSY;
|
||||
constructor() {
|
||||
super(BattlerTagType.DROWSY, BattlerTagLapseType.TURN_END, 2, MoveId.YAWN);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
return globalScene.arena.terrain?.terrainType !== TerrainType.ELECTRIC || !pokemon.isGrounded();
|
||||
constructor(sourceId: number) {
|
||||
super(BattlerTagType.DROWSY, BattlerTagLapseType.TURN_END, 2, MoveId.YAWN, sourceId);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -1509,6 +1518,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (!super.lapse(pokemon, lapseType)) {
|
||||
// TODO: Safeguard should not prevent yawn from setting sleep after tag use
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
||||
return false;
|
||||
}
|
||||
@ -3675,7 +3685,7 @@ export function getBattlerTag(
|
||||
case BattlerTagType.AQUA_RING:
|
||||
return new AquaRingTag();
|
||||
case BattlerTagType.DROWSY:
|
||||
return new DrowsyTag();
|
||||
return new DrowsyTag(sourceId);
|
||||
case BattlerTagType.TRAPPED:
|
||||
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
case BattlerTagType.NO_RETREAT:
|
||||
|
@ -11,6 +11,7 @@ import { WeakenMoveTypeTag } from "#data/arena-tag";
|
||||
import { MoveChargeAnim } from "#data/battle-anims";
|
||||
import {
|
||||
CommandedTag,
|
||||
DrowsyTag,
|
||||
EncoreTag,
|
||||
GulpMissileTag,
|
||||
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
|
||||
* 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()) {
|
||||
if (
|
||||
// Quick & Wide guard increment the Protect counter without using it for fail chance
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
turnMove.result !== MoveResult.SUCCESS
|
||||
) {
|
||||
break;
|
||||
@ -9377,9 +9403,9 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.BRICK_BREAK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 15, -1, 0, 3)
|
||||
.attr(RemoveScreensAttr),
|
||||
new StatusMove(MoveId.YAWN, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
|
||||
.condition((user, target, move) => !target.status && !target.isSafeguarded(user))
|
||||
.reflectable(),
|
||||
.attr(YawnAttr)
|
||||
.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)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
|
||||
.attr(RemoveHeldItemAttr, false)
|
||||
|
@ -4429,14 +4429,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/**
|
||||
* Return this Pokemon's move history.
|
||||
* Entries are sorted in order of OLDEST to NEWEST
|
||||
* @returns An array of {@linkcode TurnMove}, as described above.
|
||||
* Entries are sorted in order of OLDEST to NEWEST.
|
||||
* @returns An array of {@linkcode TurnMove}s, as described above.
|
||||
* @see {@linkcode getLastXMoves}
|
||||
*/
|
||||
public getMoveHistory(): TurnMove[] {
|
||||
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 {
|
||||
if (!this.isOnField()) {
|
||||
return;
|
||||
|
@ -414,6 +414,8 @@ export class PhaseManager {
|
||||
* @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: 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 {
|
||||
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
||||
}
|
||||
|
@ -175,11 +175,6 @@ export class CommandPhase extends FieldPhase {
|
||||
|
||||
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) {
|
||||
this.end();
|
||||
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 { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -31,7 +33,6 @@ describe("Moves - Encore", () => {
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset([MoveId.SPLASH, MoveId.TACKLE])
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
@ -41,74 +42,93 @@ describe("Moves - Encore", () => {
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.ENCORE);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
|
||||
game.move.use(MoveId.ENCORE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
// The enemy AI would normally be inclined to use Tackle, but should be
|
||||
// forced into using Splash.
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.getLastXMoves().every(turnMove => turnMove.move === MoveId.SPLASH)).toBeTruthy();
|
||||
expect(enemyPokemon).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||
expect(enemyPokemon.isMoveRestricted(MoveId.TACKLE)).toBe(true);
|
||||
expect(enemyPokemon.isMoveRestricted(MoveId.SPLASH)).toBe(false);
|
||||
});
|
||||
|
||||
describe("should fail against the following moves:", () => {
|
||||
it.each([
|
||||
{ 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);
|
||||
it("should override any pending move phases with the Encored move, while still consuming PP", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
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();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
game.move.use(MoveId.ENCORE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
if (delay) {
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
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();
|
||||
});
|
||||
expect(enemy).toHaveUsedMove({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||
expect(enemy.isMoveRestricted(MoveId.TACKLE)).toBe(true);
|
||||
expect(enemy.isMoveRestricted(MoveId.SPLASH)).toBe(false);
|
||||
});
|
||||
|
||||
it("Pokemon under both Encore and Torment should alternate between Struggle and restricted move", async () => {
|
||||
const turnOrder = [BattlerIndex.ENEMY, BattlerIndex.PLAYER];
|
||||
game.override.moveset([MoveId.ENCORE, MoveId.TORMENT, MoveId.SPLASH]);
|
||||
it.todo("should end at turn end if the user forgets the Encored move");
|
||||
|
||||
// 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]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
game.move.select(MoveId.ENCORE);
|
||||
await game.setTurnOrder(turnOrder);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.ENCORE);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
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();
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.setTurnOrder(turnOrder);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
const lastMove = enemyPokemon.getLastXMoves()[0];
|
||||
expect(lastMove?.move).toBe(MoveId.STRUGGLE);
|
||||
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.TORMENT);
|
||||
|
||||
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 { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { EnemyPokemon } from "#field/pokemon";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.use(MoveId.GROWL);
|
||||
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]);
|
||||
|
||||
game.move.use(MoveId.CURSE);
|
||||
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 () => {
|
||||
@ -141,23 +144,25 @@ describe("Moves - Reflecting effects", () => {
|
||||
});
|
||||
|
||||
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
|
||||
game.move.use(MoveId.GROWL);
|
||||
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||
// turn 1: Magikarp uses growl as Abra attempts to encore
|
||||
game.move.use(MoveId.GROWL, BattlerIndex.PLAYER);
|
||||
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();
|
||||
|
||||
// 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);
|
||||
// Encore locked into Tackle, replacing the enemy's Growl with another Tackle
|
||||
expect(enemy1.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
||||
expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, useMode: MoveUseMode.NORMAL });
|
||||
});
|
||||
|
||||
it("should boost stomping tantrum after a failed bounce", async () => {
|
||||
@ -171,6 +176,7 @@ describe("Moves - Reflecting effects", () => {
|
||||
game.move.use(MoveId.YAWN);
|
||||
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy).toHaveUsedMove({ move: MoveId.YAWN, result: MoveResult.FAIL, useMode: MoveUseMode.REFLECTED });
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
@ -303,6 +309,17 @@ describe("Moves - Reflecting effects", () => {
|
||||
expect(karp1).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", () => {
|
||||
|
Loading…
Reference in New Issue
Block a user