[Bug][Beta] Fix phazing moves forcing switches into fainted/ineligible Pokemon (#4951)
This commit is contained in:
parent
5fed690187
commit
75af359154
|
@ -5999,14 +5999,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
// Find indices of off-field Pokemon that are eligible to be switched into
|
||||||
|
const eligibleNewIndices: number[] = [];
|
||||||
|
switchOutTarget.scene.getPlayerParty().forEach((pokemon, index) => {
|
||||||
|
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
|
||||||
|
eligibleNewIndices.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eligibleNewIndices.length < 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (switchOutTarget.hp > 0) {
|
if (switchOutTarget.hp > 0) {
|
||||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||||
switchOutTarget.leaveField(true);
|
switchOutTarget.leaveField(true);
|
||||||
const slotIndex = Utils.randIntRange(user.scene.currentBattle.getBattlerCount(), user.scene.getPlayerParty().length);
|
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||||
user.scene.prependToPhase(
|
user.scene.prependToPhase(
|
||||||
new SwitchSummonPhase(
|
new SwitchSummonPhase(
|
||||||
user.scene,
|
user.scene,
|
||||||
|
@ -6035,14 +6043,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
||||||
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
// Find indices of off-field Pokemon that are eligible to be switched into
|
||||||
|
const eligibleNewIndices: number[] = [];
|
||||||
|
switchOutTarget.scene.getEnemyParty().forEach((pokemon, index) => {
|
||||||
|
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
|
||||||
|
eligibleNewIndices.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eligibleNewIndices.length < 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (switchOutTarget.hp > 0) {
|
if (switchOutTarget.hp > 0) {
|
||||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||||
switchOutTarget.leaveField(true);
|
switchOutTarget.leaveField(true);
|
||||||
const slotIndex = Utils.randIntRange(user.scene.currentBattle.getBattlerCount(), user.scene.getEnemyParty().length);
|
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||||
user.scene.prependToPhase(
|
user.scene.prependToPhase(
|
||||||
new SwitchSummonPhase(
|
new SwitchSummonPhase(
|
||||||
user.scene,
|
user.scene,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { allMoves } from "#app/data/move";
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { Status } from "#app/data/status-effect";
|
||||||
|
import { Challenges } from "#enums/challenges";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { Type } from "#enums/type";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
|
@ -193,4 +197,122 @@ describe("Moves - Dragon Tail", () => {
|
||||||
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
|
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
|
||||||
expect(game.scene.getPlayerField().length).toBe(1);
|
expect(game.scene.getPlayerField().length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should force switches randomly", async () => {
|
||||||
|
game.override.enemyMoveset(Moves.DRAGON_TAIL)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
|
||||||
|
|
||||||
|
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isOnField()).toBe(false);
|
||||||
|
expect(charmander.isOnField()).toBe(true);
|
||||||
|
expect(squirtle.isOnField()).toBe(false);
|
||||||
|
expect(bulbasaur.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min + 1;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isOnField()).toBe(false);
|
||||||
|
expect(charmander.isOnField()).toBe(false);
|
||||||
|
expect(squirtle.isOnField()).toBe(true);
|
||||||
|
expect(charmander.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
|
||||||
|
game.override.enemyMoveset(Moves.DRAGON_TAIL)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(1);
|
||||||
|
// Mono-Water challenge, Eevee is ineligible
|
||||||
|
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
|
||||||
|
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||||
|
|
||||||
|
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(lapras.isOnField()).toBe(false);
|
||||||
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
expect(toxapex.isOnField()).toBe(true);
|
||||||
|
expect(primarina.isOnField()).toBe(false);
|
||||||
|
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not force a switch to a fainted Pokemon", async () => {
|
||||||
|
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||||
|
|
||||||
|
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Eevee faints
|
||||||
|
eevee.hp = 0;
|
||||||
|
eevee.status = new Status(StatusEffect.FAINT);
|
||||||
|
expect(eevee.isFainted()).toBe(true);
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(lapras.isOnField()).toBe(false);
|
||||||
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
expect(toxapex.isOnField()).toBe(true);
|
||||||
|
expect(primarina.isOnField()).toBe(false);
|
||||||
|
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not force a switch if there are no available Pokemon to switch into", async () => {
|
||||||
|
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
|
||||||
|
|
||||||
|
const [ lapras, eevee ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Eevee faints
|
||||||
|
eevee.hp = 0;
|
||||||
|
eevee.status = new Status(StatusEffect.FAINT);
|
||||||
|
expect(eevee.isFainted()).toBe(true);
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(lapras.isOnField()).toBe(true);
|
||||||
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { Challenges } from "#enums/challenges";
|
||||||
|
import { Type } from "#enums/type";
|
||||||
import { MoveResult } from "#app/field/pokemon";
|
import { MoveResult } from "#app/field/pokemon";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import GameManager from "#test/utils/gameManager";
|
import GameManager from "#test/utils/gameManager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { Status } from "#app/data/status-effect";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
|
||||||
describe("Moves - Whirlwind", () => {
|
describe("Moves - Whirlwind", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
|
@ -25,8 +29,9 @@ describe("Moves - Whirlwind", () => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.battleType("single")
|
.battleType("single")
|
||||||
|
.moveset(Moves.SPLASH)
|
||||||
.enemyAbility(Abilities.BALL_FETCH)
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
.enemyMoveset(Moves.WHIRLWIND)
|
.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ])
|
||||||
.enemySpecies(Species.PIDGEY);
|
.enemySpecies(Species.PIDGEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,10 +46,114 @@ describe("Moves - Whirlwind", () => {
|
||||||
const staraptor = game.scene.getPlayerPokemon()!;
|
const staraptor = game.scene.getPlayerPokemon()!;
|
||||||
|
|
||||||
game.move.select(move);
|
game.move.select(move);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
||||||
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should force switches randomly", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
|
||||||
|
|
||||||
|
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isOnField()).toBe(false);
|
||||||
|
expect(charmander.isOnField()).toBe(true);
|
||||||
|
expect(squirtle.isOnField()).toBe(false);
|
||||||
|
|
||||||
|
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min + 1;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isOnField()).toBe(false);
|
||||||
|
expect(charmander.isOnField()).toBe(false);
|
||||||
|
expect(squirtle.isOnField()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
|
||||||
|
// Mono-Water challenge, Eevee is ineligible
|
||||||
|
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
|
||||||
|
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||||
|
|
||||||
|
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(lapras.isOnField()).toBe(false);
|
||||||
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
expect(toxapex.isOnField()).toBe(true);
|
||||||
|
expect(primarina.isOnField()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not force a switch to a fainted Pokemon", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||||
|
|
||||||
|
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Eevee faints
|
||||||
|
eevee.hp = 0;
|
||||||
|
eevee.status = new Status(StatusEffect.FAINT);
|
||||||
|
expect(eevee.isFainted()).toBe(true);
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(lapras.isOnField()).toBe(false);
|
||||||
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
expect(toxapex.isOnField()).toBe(true);
|
||||||
|
expect(primarina.isOnField()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not force a switch if there are no available Pokemon to switch into", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
|
||||||
|
|
||||||
|
const [ lapras, eevee ] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
// Turn 1: Eevee faints
|
||||||
|
eevee.hp = 0;
|
||||||
|
eevee.status = new Status(StatusEffect.FAINT);
|
||||||
|
expect(eevee.isFainted()).toBe(true);
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||||
|
return min;
|
||||||
|
});
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(lapras.isOnField()).toBe(true);
|
||||||
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue