[P2 Bug] Dragon Tail now properly respects abilities like Suction Cups (#4252)

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
NightKev 2024-09-23 08:50:42 -07:00 committed by GitHub
parent b9b69ad834
commit 554d4f0a95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 134 additions and 134 deletions

View File

@ -5174,30 +5174,28 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
} }
export class ForceSwitchOutAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr {
private user: boolean; constructor(
private batonPass: boolean; private selfSwitch: boolean = false,
private batonPass: boolean = false
constructor(user?: boolean, batonPass?: boolean) { ) {
super(false, MoveEffectTrigger.POST_APPLY, false, true); super(false, MoveEffectTrigger.POST_APPLY, false, true);
this.user = !!user;
this.batonPass = !!batonPass;
} }
isBatonPass() { isBatonPass() {
return this.batonPass; return this.batonPass;
} }
// TODO: Why is this a Promise?
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
// Check if the move category is not STATUS or if the switch out condition is not met
if (!this.getSwitchOutCondition()(user, target, move)) { if (!this.getSwitchOutCondition()(user, target, move)) {
return resolve(false); return resolve(false);
} }
// Move the switch out logic inside the conditional block // Move the switch out logic inside the conditional block
// This ensures that the switch out only happens when the conditions are met // This ensures that the switch out only happens when the conditions are met
const switchOutTarget = this.user ? user : target; const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
switchOutTarget.leaveField(!this.batonPass); switchOutTarget.leaveField(!this.batonPass);
@ -5214,7 +5212,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
// for opponent switching out // for opponent switching out
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 (eg: WILD battles) // Switch out logic for everything else (eg: WILD battles)
@ -5256,29 +5256,33 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
getSwitchOutCondition(): MoveConditionFunc { getSwitchOutCondition(): MoveConditionFunc {
return (user, target, move) => { return (user, target, move) => {
const switchOutTarget = (this.user ? user : target); const switchOutTarget = (this.selfSwitch ? user : target);
const player = switchOutTarget instanceof PlayerPokemon; const player = switchOutTarget instanceof PlayerPokemon;
if (!this.user && move.hitsSubstitute(user, target)) { if (!this.selfSwitch) {
if (move.hitsSubstitute(user, target)) {
return false; return false;
} }
if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr))) { const blockedByAbility = new Utils.BooleanHolder(false);
return false; applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
return !blockedByAbility.value;
} }
if (!player && !user.scene.currentBattle.battleType) { if (!player && user.scene.currentBattle.battleType === BattleType.WILD) {
if (this.batonPass) { if (this.batonPass) {
return false; return false;
} }
// Don't allow wild opponents to flee on the boss stage since it can ruin a run early on // Don't allow wild opponents to flee on the boss stage since it can ruin a run early on
if (!(user.scene.currentBattle.waveIndex % 10)) { if (user.scene.currentBattle.waveIndex % 10 === 0) {
return false; return false;
} }
} }
const party = player ? user.scene.getParty() : user.scene.getEnemyParty(); const party = player ? user.scene.getParty() : user.scene.getEnemyParty();
return (!player && !user.scene.currentBattle.battleType) || party.filter(p => p.isAllowedInBattle() && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); return (!player && !user.scene.currentBattle.battleType)
|| party.filter(p => p.isAllowedInBattle()
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount();
}; };
} }
@ -5286,8 +5290,8 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (!user.scene.getEnemyParty().find(p => p.isActive() && !p.isOnField())) { if (!user.scene.getEnemyParty().find(p => p.isActive() && !p.isOnField())) {
return -20; return -20;
} }
let ret = this.user ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
if (this.user && this.batonPass) { if (this.selfSwitch && this.batonPass) {
const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0); const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0);
ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10)); ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10));
} }

View File

@ -1,16 +1,11 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { BerryPhase } from "#app/phases/berry-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
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 Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import GameManager from "../utils/gameManager";
describe("Moves - Dragon Tail", () => { describe("Moves - Dragon Tail", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -29,7 +24,7 @@ describe("Moves - Dragon Tail", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleType("single") game.override.battleType("single")
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH]) .moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
.enemySpecies(Species.WAILORD) .enemySpecies(Species.WAILORD)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.startingLevel(5) .startingLevel(5)
@ -38,53 +33,45 @@ describe("Moves - Dragon Tail", () => {
vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100); vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100);
}); });
test( it("should cause opponent to flee, and not crash", async () => {
"Single battle should cause opponent to flee, and not crash", await game.classicMode.startBattle([Species.DRATINI]);
async () => {
await game.startBattle([Species.DRATINI]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DRAGON_TAIL); game.move.select(Moves.DRAGON_TAIL);
await game.phaseInterceptor.to(BerryPhase); await game.phaseInterceptor.to("BerryPhase");
const isVisible = enemyPokemon.visible; const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus; const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true); expect(!isVisible && hasFled).toBe(true);
// simply want to test that the game makes it this far without crashing // simply want to test that the game makes it this far without crashing
await game.phaseInterceptor.to(BattleEndPhase); await game.phaseInterceptor.to("BattleEndPhase");
} });
);
test( it("should cause opponent to flee, display ability, and not crash", async () => {
"Single battle should cause opponent to flee, display ability, and not crash",
async () => {
game.override.enemyAbility(Abilities.ROUGH_SKIN); game.override.enemyAbility(Abilities.ROUGH_SKIN);
await game.startBattle([Species.DRATINI]); await game.classicMode.startBattle([Species.DRATINI]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DRAGON_TAIL); game.move.select(Moves.DRAGON_TAIL);
await game.phaseInterceptor.to(BerryPhase); await game.phaseInterceptor.to("BerryPhase");
const isVisible = enemyPokemon.visible; const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus; const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true); expect(!isVisible && hasFled).toBe(true);
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
} });
);
test( it("should proceed without crashing in a double battle", async () => {
"Double battles should proceed without crashing", game.override
async () => { .battleType("double").enemyMoveset(Moves.SPLASH)
game.override.battleType("double").enemyMoveset(Moves.SPLASH);
game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
.enemyAbility(Abilities.ROUGH_SKIN); .enemyAbility(Abilities.ROUGH_SKIN);
await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
const leadPokemon = game.scene.getParty()[0]!; const leadPokemon = game.scene.getParty()[0]!;
@ -94,7 +81,7 @@ describe("Moves - Dragon Tail", () => {
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
const isVisibleLead = enemyLeadPokemon.visible; const isVisibleLead = enemyLeadPokemon.visible;
const hasFledLead = enemyLeadPokemon.switchOutStatus; const hasFledLead = enemyLeadPokemon.switchOutStatus;
@ -107,18 +94,16 @@ describe("Moves - Dragon Tail", () => {
game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2); game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(BerryPhase); await game.phaseInterceptor.to("BerryPhase");
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
} });
);
test( it("should redirect targets upon opponent flee", async () => {
"Flee move redirection works", game.override
async () => { .battleType("double")
game.override.battleType("double").enemyMoveset(Moves.SPLASH); .enemyMoveset(Moves.SPLASH)
game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]); .enemyAbility(Abilities.ROUGH_SKIN);
game.override.enemyAbility(Abilities.ROUGH_SKIN); await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
const leadPokemon = game.scene.getParty()[0]!; const leadPokemon = game.scene.getParty()[0]!;
const secPokemon = game.scene.getParty()[1]!; const secPokemon = game.scene.getParty()[1]!;
@ -130,7 +115,7 @@ describe("Moves - Dragon Tail", () => {
// target the same pokemon, second move should be redirected after first flees // target the same pokemon, second move should be redirected after first flees
game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY); game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY);
await game.phaseInterceptor.to(BerryPhase); await game.phaseInterceptor.to("BerryPhase");
const isVisibleLead = enemyLeadPokemon.visible; const isVisibleLead = enemyLeadPokemon.visible;
const hasFledLead = enemyLeadPokemon.switchOutStatus; const hasFledLead = enemyLeadPokemon.switchOutStatus;
@ -141,6 +126,17 @@ describe("Moves - Dragon Tail", () => {
expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp()); expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp());
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
} });
);
it("doesn't switch out if the target has suction cups", async () => {
game.override.enemyAbility(Abilities.SUCTION_CUPS);
await game.classicMode.startBattle([Species.REGIELEKI]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DRAGON_TAIL);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.isFullHp()).toBe(false);
});
}); });