From e340abe75d2d27b17922cb12e90bf19a9b9a0f70 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sun, 13 Oct 2024 20:08:47 -0400 Subject: [PATCH 1/2] [P1 Bug] Fix softlock when a phazing attack activates a reviver seed (#4654) * [P1 Bug] Fix softlock when a phazing attack activates a reviver seed * Polishing tests * Change approach to respect Parting Shot's targeting * Tests: Added checks for correct number of Pokemon on field --- src/data/move.ts | 13 +++---- src/test/moves/dragon_tail.test.ts | 54 ++++++++++++++++++++++++++++++ src/test/moves/u_turn.test.ts | 19 +++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index d4f3b2ce3ee..a77e8096672 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5484,37 +5484,38 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { */ const switchOutTarget = this.selfSwitch ? user : target; if (switchOutTarget instanceof PlayerPokemon) { + // Switch out logic for the player's Pokemon if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } - switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); return true; } return false; } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { + // Switch out logic for trainer battles if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } - // Switch out logic for trainer battles - switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); if (switchOutTarget.hp > 0) { // for opponent switching out + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), false, false), MoveEndPhase); } } else { + // Switch out logic for everything else (eg: WILD battles) if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; } - // Switch out logic for everything else (eg: WILD battles) - switchOutTarget.leaveField(false); - if (switchOutTarget.hp) { + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(false); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); // in double battles redirect potential moves off fled pokemon diff --git a/src/test/moves/dragon_tail.test.ts b/src/test/moves/dragon_tail.test.ts index eb02b09fbb4..cf801eb42c1 100644 --- a/src/test/moves/dragon_tail.test.ts +++ b/src/test/moves/dragon_tail.test.ts @@ -139,4 +139,58 @@ describe("Moves - Dragon Tail", () => { expect(enemy.isFullHp()).toBe(false); }); + + it("should force a switch upon fainting an opponent normally", async () => { + game.override.startingWave(5) + .startingLevel(1000); // To make sure Dragon Tail KO's the opponent + await game.classicMode.startBattle([ Species.DRATINI ]); + + game.move.select(Moves.DRAGON_TAIL); + + await game.toNextTurn(); + + // Make sure the enemy switched to a healthy Pokemon + const enemy = game.scene.getEnemyPokemon()!; + expect(enemy).toBeDefined(); + expect(enemy.isFullHp()).toBe(true); + + // Make sure the enemy has a fainted Pokemon in their party and not on the field + const faintedEnemy = game.scene.getEnemyParty().find(p => !p.isAllowedInBattle()); + expect(faintedEnemy).toBeDefined(); + expect(game.scene.getEnemyField().length).toBe(1); + }); + + it("should not cause a softlock when activating an opponent trainer's reviver seed", async () => { + game.override.startingWave(5) + .enemyHeldItems([{ name: "REVIVER_SEED" }]) + .startingLevel(1000); // To make sure Dragon Tail KO's the opponent + await game.classicMode.startBattle([ Species.DRATINI ]); + + game.move.select(Moves.DRAGON_TAIL); + + await game.toNextTurn(); + + // Make sure the enemy field is not empty and has a revived Pokemon + const enemy = game.scene.getEnemyPokemon()!; + expect(enemy).toBeDefined(); + expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2)); + expect(game.scene.getEnemyField().length).toBe(1); + }); + + it("should not cause a softlock when activating a player's reviver seed", async () => { + game.override.startingHeldItems([{ name: "REVIVER_SEED" }]) + .enemyMoveset(Moves.DRAGON_TAIL) + .enemyLevel(1000); // To make sure Dragon Tail KO's the player + await game.classicMode.startBattle([ Species.DRATINI, Species.BULBASAUR ]); + + game.move.select(Moves.SPLASH); + + await game.toNextTurn(); + + // Make sure the player's field is not empty and has a revived Pokemon + const dratini = game.scene.getPlayerPokemon()!; + expect(dratini).toBeDefined(); + expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2)); + expect(game.scene.getPlayerField().length).toBe(1); + }); }); diff --git a/src/test/moves/u_turn.test.ts b/src/test/moves/u_turn.test.ts index 17e02cb50ef..b995c20f503 100644 --- a/src/test/moves/u_turn.test.ts +++ b/src/test/moves/u_turn.test.ts @@ -96,4 +96,23 @@ describe("Moves - U-turn", () => { expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); }, 20000); + + it("still forces a switch if u-turn KO's the opponent", async () => { + game.override.startingLevel(1000); // Ensure that U-Turn KO's the opponent + await game.classicMode.startBattle([ + Species.RAICHU, + Species.SHUCKLE + ]); + const enemy = game.scene.getEnemyPokemon()!; + + // KO the opponent with U-Turn + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to(TurnEndPhase); + expect(enemy.isFainted()).toBe(true); + + // Check that U-Turn forced a switch + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE); + }); }); From 8981f0e7a8ee63743b60f1665967859940b1a4b8 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:20:55 -0700 Subject: [PATCH 2/2] Trainer party de-duplication checks static pokemon too (#4585) Co-authored-by: frutescens Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/field/trainer.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 2ec9d07e474..5110787452f 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -383,7 +383,7 @@ export default class Trainer extends Phaser.GameObjects.Container { const battle = this.scene.currentBattle; const template = this.getPartyTemplate(); - let species: PokemonSpecies; + let baseSpecies: PokemonSpecies; if (this.config.speciesPools) { const tierValue = Utils.randSeedInt(512); let tier = tierValue >= 156 ? TrainerPoolTier.COMMON : tierValue >= 32 ? TrainerPoolTier.UNCOMMON : tierValue >= 6 ? TrainerPoolTier.RARE : tierValue >= 1 ? TrainerPoolTier.SUPER_RARE : TrainerPoolTier.ULTRA_RARE; @@ -393,17 +393,17 @@ export default class Trainer extends Phaser.GameObjects.Container { tier--; } const tierPool = this.config.speciesPools[tier]; - species = getPokemonSpecies(Utils.randSeedItem(tierPool)); + baseSpecies = getPokemonSpecies(Utils.randSeedItem(tierPool)); } else { - species = this.scene.randomSpecies(battle.waveIndex, level, false, this.config.speciesFilter); + baseSpecies = this.scene.randomSpecies(battle.waveIndex, level, false, this.config.speciesFilter); } - let ret = getPokemonSpecies(species.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex)); + let ret = getPokemonSpecies(baseSpecies.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex)); let retry = false; console.log(ret.getName()); - if (pokemonPrevolutions.hasOwnProperty(species.speciesId) && ret.speciesId !== species.speciesId) { + if (pokemonPrevolutions.hasOwnProperty(baseSpecies.speciesId) && ret.speciesId !== baseSpecies.speciesId) { retry = true; } else if (template.isBalanced(battle.enemyParty.length)) { const partyMemberTypes = battle.enemyParty.map(p => p.getTypes(true)).flat(); @@ -417,7 +417,7 @@ export default class Trainer extends Phaser.GameObjects.Container { console.log("Attempting reroll of species evolution to fit specialty type..."); let evoAttempt = 0; while (retry && evoAttempt++ < 10) { - ret = getPokemonSpecies(species.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex)); + ret = getPokemonSpecies(baseSpecies.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex)); console.log(ret.name); if (this.config.specialtyTypes.find(t => ret.isOfType(t))) { retry = false; @@ -426,7 +426,7 @@ export default class Trainer extends Phaser.GameObjects.Container { } // Prompts reroll of party member species if species already present in the enemy party - if (this.checkDuplicateSpecies(ret)) { + if (this.checkDuplicateSpecies(ret, baseSpecies)) { console.log("Duplicate species detected, prompting reroll..."); retry = true; } @@ -442,13 +442,16 @@ export default class Trainer extends Phaser.GameObjects.Container { /** * Checks if the enemy trainer already has the Pokemon species in their party * @param {PokemonSpecies} species {@linkcode PokemonSpecies} + * @param {PokemonSpecies} baseSpecies {@linkcode PokemonSpecies} - baseSpecies of the Pokemon if species is forced to evolve * @returns `true` if the species is already present in the party */ - checkDuplicateSpecies(species: PokemonSpecies): boolean { + checkDuplicateSpecies(species: PokemonSpecies, baseSpecies: PokemonSpecies): boolean { + const staticPartyPokemon = (signatureSpecies[TrainerType[this.config.trainerType]] ?? []).flat(1); + const currentPartySpecies = this.scene.getEnemyParty().map(p => { return p.species.speciesId; }); - return currentPartySpecies.includes(species.speciesId); + return currentPartySpecies.includes(species.speciesId) || staticPartyPokemon.includes(baseSpecies.speciesId); } getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] {