mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-30 20:57:13 +00:00
[BUG] - dragon tail switchout ability in wild battles proc crashes game (#3346)
* fixing switchout ability doubles bug, refactor move redirect code * added unit test for dragon tail * updating test * addressing errors from pages deployment * pages deployment still failing * typedoc * please let this be the one * formatting and test fixing * await starting battle should go after overrides
This commit is contained in:
parent
a272d28cdf
commit
8faf27efc9
@ -768,6 +768,27 @@ export default class BattleScene extends SceneBase {
|
|||||||
: ret;
|
: ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in doubles battles to redirect moves from one pokemon to another when one faints or is removed from the field
|
||||||
|
* @param removedPokemon {@linkcode Pokemon} the pokemon that is being removed from the field (flee, faint), moves to be redirected FROM
|
||||||
|
* @param allyPokemon {@linkcode Pokemon} the pokemon that will have the moves be redirected TO
|
||||||
|
*/
|
||||||
|
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||||
|
// failsafe: if not a double battle just return
|
||||||
|
if (this.currentBattle.double === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (allyPokemon?.isActive(true)) {
|
||||||
|
let targetingMovePhase: MovePhase;
|
||||||
|
do {
|
||||||
|
targetingMovePhase = this.findPhase(mp => mp instanceof MovePhase && mp.targets.length === 1 && mp.targets[0] === removedPokemon.getBattlerIndex() && mp.pokemon.isPlayer() !== allyPokemon.isPlayer()) as MovePhase;
|
||||||
|
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
|
||||||
|
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
|
||||||
|
}
|
||||||
|
} while (targetingMovePhase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere
|
* Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere
|
||||||
* @returns {ModifierBar}
|
* @returns {ModifierBar}
|
||||||
|
@ -4883,13 +4883,18 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), false, this.batonPass, false), MoveEndPhase);
|
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), false, this.batonPass, false), MoveEndPhase);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Switch out logic for everything else
|
// Switch out logic for everything else (eg: WILD battles)
|
||||||
switchOutTarget.setVisible(false);
|
switchOutTarget.leaveField(false);
|
||||||
|
|
||||||
if (switchOutTarget.hp) {
|
if (switchOutTarget.hp) {
|
||||||
switchOutTarget.hideInfo().then(() => switchOutTarget.destroy());
|
switchOutTarget.setWildFlee(true);
|
||||||
switchOutTarget.scene.field.remove(switchOutTarget);
|
|
||||||
user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500);
|
user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500);
|
||||||
|
|
||||||
|
// in double battles redirect potential moves off fled pokemon
|
||||||
|
if (switchOutTarget.scene.currentBattle.double) {
|
||||||
|
const allyPokemon = switchOutTarget.getAlly();
|
||||||
|
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!switchOutTarget.getAlly()?.isActive(true)) {
|
if (!switchOutTarget.getAlly()?.isActive(true)) {
|
||||||
@ -7585,7 +7590,8 @@ export function initMoves() {
|
|||||||
new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
|
new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
|
||||||
.attr(CritOnlyAttr),
|
.attr(CritOnlyAttr),
|
||||||
new AttackMove(Moves.DRAGON_TAIL, Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
|
new AttackMove(Moves.DRAGON_TAIL, Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
|
||||||
.attr(ForceSwitchOutAttr),
|
.attr(ForceSwitchOutAttr)
|
||||||
|
.hidesTarget(),
|
||||||
new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5)
|
new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5)
|
||||||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, true),
|
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, true),
|
||||||
new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
|
new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
|
||||||
|
@ -88,6 +88,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
public luck: integer;
|
public luck: integer;
|
||||||
public pauseEvolutions: boolean;
|
public pauseEvolutions: boolean;
|
||||||
public pokerus: boolean;
|
public pokerus: boolean;
|
||||||
|
public wildFlee: boolean;
|
||||||
|
|
||||||
public fusionSpecies: PokemonSpecies | null;
|
public fusionSpecies: PokemonSpecies | null;
|
||||||
public fusionFormIndex: integer;
|
public fusionFormIndex: integer;
|
||||||
@ -129,6 +130,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
this.species = species;
|
this.species = species;
|
||||||
this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL;
|
this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL;
|
||||||
this.level = level;
|
this.level = level;
|
||||||
|
this.wildFlee = false;
|
||||||
|
|
||||||
// Determine the ability index
|
// Determine the ability index
|
||||||
if (abilityIndex !== undefined) {
|
if (abilityIndex !== undefined) {
|
||||||
this.abilityIndex = abilityIndex; // Use the provided ability index if it is defined
|
this.abilityIndex = abilityIndex; // Use the provided ability index if it is defined
|
||||||
@ -298,14 +301,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this pokemon is both not fainted and allowed to be in battle.
|
* Check if this pokemon is both not fainted (or a fled wild pokemon) and allowed to be in battle.
|
||||||
* This is frequently a better alternative to {@link isFainted}
|
* This is frequently a better alternative to {@link isFainted}
|
||||||
* @returns {boolean} True if pokemon is allowed in battle
|
* @returns {boolean} True if pokemon is allowed in battle
|
||||||
*/
|
*/
|
||||||
isAllowedInBattle(): boolean {
|
isAllowedInBattle(): boolean {
|
||||||
const challengeAllowed = new Utils.BooleanHolder(true);
|
const challengeAllowed = new Utils.BooleanHolder(true);
|
||||||
applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed);
|
applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed);
|
||||||
return !this.isFainted() && challengeAllowed.value;
|
return !this.isFainted() && !this.wildFlee && challengeAllowed.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive(onField?: boolean): boolean {
|
isActive(onField?: boolean): boolean {
|
||||||
@ -1779,6 +1782,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets if the pokemon has fled (implies it's a wild pokemon)
|
||||||
|
* @param status - boolean
|
||||||
|
*/
|
||||||
|
setWildFlee(status: boolean): void {
|
||||||
|
this.wildFlee = status;
|
||||||
|
}
|
||||||
|
|
||||||
updateInfo(instant?: boolean): Promise<void> {
|
updateInfo(instant?: boolean): Promise<void> {
|
||||||
return this.battleInfo.updateInfo(this, instant);
|
return this.battleInfo.updateInfo(this, instant);
|
||||||
}
|
}
|
||||||
|
@ -2576,6 +2576,15 @@ export class BattleEndPhase extends BattlePhase {
|
|||||||
|
|
||||||
this.scene.updateModifiers().then(() => this.end());
|
this.scene.updateModifiers().then(() => this.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
// removing pokemon at the end of a battle
|
||||||
|
for (const p of this.scene.getEnemyParty()) {
|
||||||
|
p.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NewBattlePhase extends BattlePhase {
|
export class NewBattlePhase extends BattlePhase {
|
||||||
@ -3830,6 +3839,7 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
doFaint(): void {
|
doFaint(): void {
|
||||||
const pokemon = this.getPokemon();
|
const pokemon = this.getPokemon();
|
||||||
|
|
||||||
|
|
||||||
// Track total times pokemon have been KO'd for supreme overlord/last respects
|
// Track total times pokemon have been KO'd for supreme overlord/last respects
|
||||||
if (pokemon.isPlayer()) {
|
if (pokemon.isPlayer()) {
|
||||||
this.scene.currentBattle.playerFaints += 1;
|
this.scene.currentBattle.playerFaints += 1;
|
||||||
@ -3880,17 +3890,10 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in double battles redirect potential moves off fainted pokemon
|
||||||
if (this.scene.currentBattle.double) {
|
if (this.scene.currentBattle.double) {
|
||||||
const allyPokemon = pokemon.getAlly();
|
const allyPokemon = pokemon.getAlly();
|
||||||
if (allyPokemon?.isActive(true)) {
|
this.scene.redirectPokemonMoves(pokemon, allyPokemon);
|
||||||
let targetingMovePhase: MovePhase;
|
|
||||||
do {
|
|
||||||
targetingMovePhase = this.scene.findPhase(mp => mp instanceof MovePhase && mp.targets.length === 1 && mp.targets[0] === pokemon.getBattlerIndex() && mp.pokemon.isPlayer() !== allyPokemon.isPlayer()) as MovePhase;
|
|
||||||
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
|
|
||||||
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
|
|
||||||
}
|
|
||||||
} while (targetingMovePhase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
||||||
|
166
src/test/moves/dragon_tail.test.ts
Normal file
166
src/test/moves/dragon_tail.test.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { allMoves } from "#app/data/move.js";
|
||||||
|
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||||
|
import { BattleEndPhase, BerryPhase, TurnEndPhase} from "#app/phases.js";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import GameManager from "../utils/gameManager";
|
||||||
|
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||||
|
import { BattlerIndex } from "#app/battle.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Moves - Dragon Tail", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override.battleType("single")
|
||||||
|
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH])
|
||||||
|
.enemySpecies(Species.WAILORD)
|
||||||
|
.enemyMoveset(SPLASH_ONLY)
|
||||||
|
.startingLevel(5)
|
||||||
|
.enemyLevel(5);
|
||||||
|
|
||||||
|
vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Single battle should cause opponent to flee, and not crash",
|
||||||
|
async () => {
|
||||||
|
await game.startBattle([Species.DRATINI]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
expect(enemyPokemon).toBeDefined();
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_TAIL));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(BerryPhase);
|
||||||
|
|
||||||
|
const isVisible = enemyPokemon.visible;
|
||||||
|
const hasFled = enemyPokemon.wildFlee;
|
||||||
|
expect(!isVisible && hasFled).toBe(true);
|
||||||
|
|
||||||
|
// simply want to test that the game makes it this far without crashing
|
||||||
|
await game.phaseInterceptor.to(BattleEndPhase);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Single battle should cause opponent to flee, display ability, and not crash",
|
||||||
|
async () => {
|
||||||
|
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
|
await game.startBattle([Species.DRATINI]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
expect(leadPokemon).toBeDefined();
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
expect(enemyPokemon).toBeDefined();
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_TAIL));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(BerryPhase);
|
||||||
|
|
||||||
|
const isVisible = enemyPokemon.visible;
|
||||||
|
const hasFled = enemyPokemon.wildFlee;
|
||||||
|
expect(!isVisible && hasFled).toBe(true);
|
||||||
|
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Double battles should proceed without crashing" ,
|
||||||
|
async () => {
|
||||||
|
game.override.battleType("double").enemyMoveset(SPLASH_ONLY);
|
||||||
|
game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
|
||||||
|
.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
|
await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getParty()[0]!;
|
||||||
|
const secPokemon = game.scene.getParty()[1]!;
|
||||||
|
expect(leadPokemon).toBeDefined();
|
||||||
|
expect(secPokemon).toBeDefined();
|
||||||
|
|
||||||
|
const enemyLeadPokemon = game.scene.currentBattle.enemyParty[0]!;
|
||||||
|
const enemySecPokemon = game.scene.currentBattle.enemyParty[1]!;
|
||||||
|
expect(enemyLeadPokemon).toBeDefined();
|
||||||
|
expect(enemySecPokemon).toBeDefined();
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_TAIL));
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
const isVisibleLead = enemyLeadPokemon.visible;
|
||||||
|
const hasFledLead = enemyLeadPokemon.wildFlee;
|
||||||
|
const isVisibleSec = enemySecPokemon.visible;
|
||||||
|
const hasFledSec = enemySecPokemon.wildFlee;
|
||||||
|
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
|
||||||
|
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||||
|
|
||||||
|
// second turn
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.FLAMETHROWER));
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY_2);
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(BerryPhase);
|
||||||
|
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Flee move redirection works" ,
|
||||||
|
async () => {
|
||||||
|
game.override.battleType("double").enemyMoveset(SPLASH_ONLY);
|
||||||
|
game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]);
|
||||||
|
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
|
await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getParty()[0]!;
|
||||||
|
const secPokemon = game.scene.getParty()[1]!;
|
||||||
|
expect(leadPokemon).toBeDefined();
|
||||||
|
expect(secPokemon).toBeDefined();
|
||||||
|
|
||||||
|
const enemyLeadPokemon = game.scene.currentBattle.enemyParty[0]!;
|
||||||
|
const enemySecPokemon = game.scene.currentBattle.enemyParty[1]!;
|
||||||
|
expect(enemyLeadPokemon).toBeDefined();
|
||||||
|
expect(enemySecPokemon).toBeDefined();
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_TAIL));
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
|
||||||
|
// target the same pokemon, second move should be redirected after first flees
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_TAIL));
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(BerryPhase);
|
||||||
|
|
||||||
|
const isVisibleLead = enemyLeadPokemon.visible;
|
||||||
|
const hasFledLead = enemyLeadPokemon.wildFlee;
|
||||||
|
const isVisibleSec = enemySecPokemon.visible;
|
||||||
|
const hasFledSec = enemySecPokemon.wildFlee;
|
||||||
|
expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true);
|
||||||
|
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||||
|
expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp());
|
||||||
|
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
|
||||||
|
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user