mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-18 06:51:08 +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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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);
|
||||
}
|
||||
} else {
|
||||
// Switch out logic for everything else
|
||||
switchOutTarget.setVisible(false);
|
||||
// Switch out logic for everything else (eg: WILD battles)
|
||||
switchOutTarget.leaveField(false);
|
||||
|
||||
if (switchOutTarget.hp) {
|
||||
switchOutTarget.hideInfo().then(() => switchOutTarget.destroy());
|
||||
switchOutTarget.scene.field.remove(switchOutTarget);
|
||||
switchOutTarget.setWildFlee(true);
|
||||
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)) {
|
||||
@ -7585,7 +7590,8 @@ export function initMoves() {
|
||||
new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
|
||||
.attr(CritOnlyAttr),
|
||||
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)
|
||||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, true),
|
||||
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 pauseEvolutions: boolean;
|
||||
public pokerus: boolean;
|
||||
public wildFlee: boolean;
|
||||
|
||||
public fusionSpecies: PokemonSpecies | null;
|
||||
public fusionFormIndex: integer;
|
||||
@ -129,6 +130,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.species = species;
|
||||
this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL;
|
||||
this.level = level;
|
||||
this.wildFlee = false;
|
||||
|
||||
// Determine the ability index
|
||||
if (abilityIndex !== undefined) {
|
||||
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}
|
||||
* @returns {boolean} True if pokemon is allowed in battle
|
||||
*/
|
||||
isAllowedInBattle(): boolean {
|
||||
const challengeAllowed = new Utils.BooleanHolder(true);
|
||||
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 {
|
||||
@ -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> {
|
||||
return this.battleInfo.updateInfo(this, instant);
|
||||
}
|
||||
|
@ -2576,6 +2576,15 @@ export class BattleEndPhase extends BattlePhase {
|
||||
|
||||
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 {
|
||||
@ -3830,6 +3839,7 @@ export class FaintPhase extends PokemonPhase {
|
||||
doFaint(): void {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
|
||||
// Track total times pokemon have been KO'd for supreme overlord/last respects
|
||||
if (pokemon.isPlayer()) {
|
||||
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) {
|
||||
const allyPokemon = pokemon.getAlly();
|
||||
if (allyPokemon?.isActive(true)) {
|
||||
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);
|
||||
}
|
||||
this.scene.redirectPokemonMoves(pokemon, allyPokemon);
|
||||
}
|
||||
|
||||
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