[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:
parent
b9b69ad834
commit
554d4f0a95
|
@ -5174,31 +5174,29 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
|||
}
|
||||
|
||||
export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
private user: boolean;
|
||||
private batonPass: boolean;
|
||||
|
||||
constructor(user?: boolean, batonPass?: boolean) {
|
||||
constructor(
|
||||
private selfSwitch: boolean = false,
|
||||
private batonPass: boolean = false
|
||||
) {
|
||||
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
this.user = !!user;
|
||||
this.batonPass = !!batonPass;
|
||||
}
|
||||
|
||||
isBatonPass() {
|
||||
return this.batonPass;
|
||||
}
|
||||
|
||||
// TODO: Why is this a Promise?
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
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)) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
// Move the switch out logic inside the conditional block
|
||||
// This ensures that the switch out only happens when the conditions are met
|
||||
const switchOutTarget = this.user ? user : target;
|
||||
if (switchOutTarget instanceof PlayerPokemon) {
|
||||
// Move the switch out logic inside the conditional block
|
||||
// This ensures that the switch out only happens when the conditions are met
|
||||
const switchOutTarget = this.selfSwitch ? user : target;
|
||||
if (switchOutTarget instanceof PlayerPokemon) {
|
||||
switchOutTarget.leaveField(!this.batonPass);
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
|
@ -5207,41 +5205,43 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
return;
|
||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
|
||||
// Switch out logic for trainer battles
|
||||
return;
|
||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
|
||||
// Switch out logic for trainer battles
|
||||
switchOutTarget.leaveField(!this.batonPass);
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
// 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);
|
||||
if (switchOutTarget.hp > 0) {
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
// Switch out logic for everything else (eg: WILD battles)
|
||||
switchOutTarget.leaveField(false);
|
||||
} else {
|
||||
// Switch out logic for everything else (eg: WILD battles)
|
||||
switchOutTarget.leaveField(false);
|
||||
|
||||
if (switchOutTarget.hp) {
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500);
|
||||
if (switchOutTarget.hp) {
|
||||
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)) {
|
||||
user.scene.clearEnemyHeldItemModifiers();
|
||||
if (!switchOutTarget.getAlly()?.isActive(true)) {
|
||||
user.scene.clearEnemyHeldItemModifiers();
|
||||
|
||||
if (switchOutTarget.hp) {
|
||||
user.scene.pushPhase(new BattleEndPhase(user.scene));
|
||||
user.scene.pushPhase(new NewBattlePhase(user.scene));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (switchOutTarget.hp) {
|
||||
user.scene.pushPhase(new BattleEndPhase(user.scene));
|
||||
user.scene.pushPhase(new NewBattlePhase(user.scene));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
|
@ -5256,29 +5256,33 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||
|
||||
getSwitchOutCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const switchOutTarget = (this.user ? user : target);
|
||||
const switchOutTarget = (this.selfSwitch ? user : target);
|
||||
const player = switchOutTarget instanceof PlayerPokemon;
|
||||
|
||||
if (!this.user && move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
if (!this.selfSwitch) {
|
||||
if (move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const blockedByAbility = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
|
||||
return !blockedByAbility.value;
|
||||
}
|
||||
|
||||
if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!player && !user.scene.currentBattle.battleType) {
|
||||
if (!player && user.scene.currentBattle.battleType === BattleType.WILD) {
|
||||
if (this.batonPass) {
|
||||
return false;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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())) {
|
||||
return -20;
|
||||
}
|
||||
let ret = this.user ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
|
||||
if (this.user && this.batonPass) {
|
||||
let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
|
||||
if (this.selfSwitch && this.batonPass) {
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import { BattlerIndex } from "#app/battle";
|
||||
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 { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
|
||||
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Dragon Tail", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
|
@ -29,7 +24,7 @@ describe("Moves - Dragon Tail", () => {
|
|||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleType("single")
|
||||
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH])
|
||||
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
|
||||
.enemySpecies(Species.WAILORD)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.startingLevel(5)
|
||||
|
@ -38,109 +33,110 @@ describe("Moves - Dragon Tail", () => {
|
|||
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]);
|
||||
it("should cause opponent to flee, and not crash", async () => {
|
||||
await game.classicMode.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 hasFled = enemyPokemon.switchOutStatus;
|
||||
expect(!isVisible && hasFled).toBe(true);
|
||||
const isVisible = enemyPokemon.visible;
|
||||
const hasFled = enemyPokemon.switchOutStatus;
|
||||
expect(!isVisible && hasFled).toBe(true);
|
||||
|
||||
// simply want to test that the game makes it this far without crashing
|
||||
await game.phaseInterceptor.to(BattleEndPhase);
|
||||
}
|
||||
);
|
||||
// simply want to test that the game makes it this far without crashing
|
||||
await game.phaseInterceptor.to("BattleEndPhase");
|
||||
});
|
||||
|
||||
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]);
|
||||
it("should cause opponent to flee, display ability, and not crash", async () => {
|
||||
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
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 hasFled = enemyPokemon.switchOutStatus;
|
||||
expect(!isVisible && hasFled).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
}
|
||||
);
|
||||
const isVisible = enemyPokemon.visible;
|
||||
const hasFled = enemyPokemon.switchOutStatus;
|
||||
expect(!isVisible && hasFled).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
test(
|
||||
"Double battles should proceed without crashing",
|
||||
async () => {
|
||||
game.override.battleType("double").enemyMoveset(Moves.SPLASH);
|
||||
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]);
|
||||
it("should proceed without crashing in a double battle", async () => {
|
||||
game.override
|
||||
.battleType("double").enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||
|
||||
const leadPokemon = game.scene.getParty()[0]!;
|
||||
const leadPokemon = game.scene.getParty()[0]!;
|
||||
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
|
||||
// second turn
|
||||
game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
// second turn
|
||||
game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase);
|
||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||
}
|
||||
);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
test(
|
||||
"Flee move redirection works",
|
||||
async () => {
|
||||
game.override.battleType("double").enemyMoveset(Moves.SPLASH);
|
||||
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]);
|
||||
it("should redirect targets upon opponent flee", async () => {
|
||||
game.override
|
||||
.battleType("double")
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||
|
||||
const leadPokemon = game.scene.getParty()[0]!;
|
||||
const secPokemon = game.scene.getParty()[1]!;
|
||||
const leadPokemon = game.scene.getParty()[0]!;
|
||||
const secPokemon = game.scene.getParty()[1]!;
|
||||
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
// 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, 0, BattlerIndex.ENEMY);
|
||||
// target the same pokemon, second move should be redirected after first flees
|
||||
game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
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());
|
||||
}
|
||||
);
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
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());
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue