mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-05 07:52:17 +02:00
Added mixin for ForceSwitch
to handle shared logic among move/ability classes
This commit is contained in:
parent
02cac77853
commit
37ad950093
@ -871,6 +871,51 @@ export default class BattleScene extends SceneBase {
|
||||
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
||||
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
||||
|
||||
* Used for switch out logic checks.
|
||||
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
||||
* @returns An array of all {@linkcode PlayerPokemon} in reserve able to be switched into.
|
||||
* @overload
|
||||
*/
|
||||
public getBackupPartyMembers(player: true): PlayerPokemon[];
|
||||
/**
|
||||
* Return all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
||||
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
||||
|
||||
* Used for switch out logic checks.
|
||||
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
||||
* @param trainerSlot - The {@linkcode EnemyPokemon.trainerSlot | trainer slot} of the Pokemon being switched out;
|
||||
* used to verify ownership in multi battles.
|
||||
* @returns An array of all {@linkcode EnemyPokemon} in reserve able to be switched into.
|
||||
* @overload
|
||||
*/
|
||||
public getBackupPartyMembers(player: false, trainerSlot: number): EnemyPokemon[];
|
||||
/**
|
||||
* Return all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
||||
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
||||
|
||||
* Used for switch out logic checks.
|
||||
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
||||
* @param trainerSlot - The enemy Pokemon's {@linkcode EnemyPokemon.trainerSlot | trainer slot} for opposing trainers;
|
||||
* used to verify ownership in multi battles and unused for player pokemon.
|
||||
* @returns An array of all {@linkcode PlayerPokemon}/{@linkcode EnemyPokemon} in reserve able to be switched into.
|
||||
* @overload
|
||||
*/
|
||||
public getBackupPartyMembers(player: boolean, trainerSlot: number | undefined): PlayerPokemon | EnemyPokemon[];
|
||||
|
||||
public getBackupPartyMembers<B extends boolean = never, R = B extends true ? PlayerPokemon : EnemyPokemon>(
|
||||
player: B,
|
||||
trainerSlot?: number,
|
||||
): R[] {
|
||||
return (player ? this.getPlayerParty() : this.getEnemyParty()).filter(
|
||||
(p: PlayerPokemon | EnemyPokemon) =>
|
||||
p.isAllowedInBattle() && !p.isOnField() && (p instanceof PlayerPokemon || p.trainerSlot !== trainerSlot),
|
||||
) as R[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Pokemon on both sides of the battle - player first, then enemy.
|
||||
* Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type.
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
CopyMoveAttr,
|
||||
NeutralDamageAgainstFlyingTypeMultiplierAttr,
|
||||
FixedDamageAttr,
|
||||
type MoveAttr,
|
||||
} from "#app/data/moves/move";
|
||||
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
@ -55,7 +56,7 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { SwitchType, type NormalSwitchType } from "#enums/switch-type";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { MoveTarget } from "#enums/MoveTarget";
|
||||
import { MoveCategory } from "#enums/MoveCategory";
|
||||
@ -67,7 +68,7 @@ import { BerryUsedEvent } from "#app/events/battle-scene";
|
||||
|
||||
|
||||
// Type imports
|
||||
import type { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import type { Weather } from "#app/data/weather";
|
||||
import type { BattlerTag } from "#app/data/battler-tags";
|
||||
@ -77,6 +78,7 @@ import type Move from "#app/data/moves/move";
|
||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves";
|
||||
import { ForceSwitch } from "../mixins/force-switch";
|
||||
|
||||
export class BlockRecoilDamageAttr extends AbAttr {
|
||||
constructor() {
|
||||
@ -5537,128 +5539,6 @@ function applySingleAbAttrs<TAttr extends AbAttr>(
|
||||
}
|
||||
}
|
||||
|
||||
class ForceSwitchOutHelper {
|
||||
constructor(private switchType: SwitchType) {}
|
||||
|
||||
/**
|
||||
* Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type.
|
||||
*
|
||||
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
|
||||
* @returns `true` if the switch is successful
|
||||
*/
|
||||
public switchOutLogic(pokemon: Pokemon): boolean {
|
||||
const switchOutTarget = pokemon;
|
||||
/**
|
||||
* If the switch-out target is a player-controlled Pokémon, the function checks:
|
||||
* - Whether there are available party members to switch in.
|
||||
* - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated.
|
||||
*/
|
||||
if (switchOutTarget instanceof PlayerPokemon) {
|
||||
if (globalScene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.prependToPhase(new SwitchPhase(this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* For non-wild battles, it checks if the opposing party has any available Pokémon to switch in.
|
||||
* If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated.
|
||||
*/
|
||||
} else if (globalScene.currentBattle.battleType !== BattleType.WILD) {
|
||||
if (globalScene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
return false;
|
||||
}
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
const summonIndex = (globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0);
|
||||
globalScene.prependToPhase(new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false), MoveEndPhase);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles).
|
||||
* It will not flee if it is a Mystery Encounter with fleeing disabled (checked in `getSwitchOutCondition()`) or if it is a wave 10x wild boss
|
||||
*/
|
||||
} else {
|
||||
const allyPokemon = switchOutTarget.getAlly();
|
||||
|
||||
if (!globalScene.currentBattle.waveIndex || globalScene.currentBattle.waveIndex % 10 === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(false);
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
||||
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allyPokemon?.isActive(true)) {
|
||||
globalScene.clearEnemyHeldItemModifiers();
|
||||
|
||||
if (switchOutTarget.hp) {
|
||||
globalScene.pushPhase(new BattleEndPhase(false));
|
||||
|
||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
globalScene.pushPhase(new SelectBiomePhase());
|
||||
}
|
||||
|
||||
globalScene.pushPhase(new NewBattlePhase());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions.
|
||||
*
|
||||
* @param pokemon The Pokémon attempting to switch out.
|
||||
* @param opponent The opponent Pokémon.
|
||||
* @returns `true` if the switch-out condition is met
|
||||
*/
|
||||
public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean {
|
||||
const switchOutTarget = pokemon;
|
||||
const player = switchOutTarget instanceof PlayerPokemon;
|
||||
|
||||
if (player) {
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
|
||||
return !blockedByAbility.value;
|
||||
}
|
||||
|
||||
if (!player && globalScene.currentBattle.battleType === BattleType.WILD) {
|
||||
if (!globalScene.currentBattle.waveIndex && globalScene.currentBattle.waveIndex % 10 === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!player && globalScene.currentBattle.isBattleMysteryEncounter() && !globalScene.currentBattle.mysteryEncounter?.fleeAllowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const party = player ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
return (!player && globalScene.currentBattle.battleType === BattleType.WILD)
|
||||
|| party.filter(p => p.isAllowedInBattle() && !p.isOnField()
|
||||
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a message if the switch-out attempt fails due to ability effects.
|
||||
*
|
||||
* @param target The target Pokémon.
|
||||
* @returns The failure message, or `null` if no failure.
|
||||
*/
|
||||
public getFailedText(target: Pokemon): string | null {
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
|
||||
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the amount of recovery from the Shell Bell item.
|
||||
*
|
||||
@ -5712,12 +5592,13 @@ export class PostDamageAbAttr extends AbAttr {
|
||||
* @extends PostDamageAbAttr
|
||||
* @see {@linkcode applyPostDamage}
|
||||
*/
|
||||
export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
|
||||
private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH);
|
||||
export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) {
|
||||
private hpRatio: number;
|
||||
|
||||
constructor(hpRatio = 0.5) {
|
||||
constructor(selfSwitch = true, switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) {
|
||||
super();
|
||||
this.selfSwitch = selfSwitch;
|
||||
this.switchType = switchType;
|
||||
this.hpRatio = hpRatio;
|
||||
}
|
||||
|
||||
@ -5766,7 +5647,7 @@ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
|
||||
const shellBellHeal = calculateShellBellRecovery(pokemon);
|
||||
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) {
|
||||
for (const opponent of pokemon.getOpponents()) {
|
||||
if (!this.helper.getSwitchOutCondition(pokemon, opponent)) {
|
||||
if (!this.canSwitchOut(pokemon, opponent)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -5779,20 +5660,14 @@ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
|
||||
|
||||
/**
|
||||
* Applies the switch-out logic after the Pokémon takes damage.
|
||||
* Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and
|
||||
* the Pokémon's health after damage to determine whether the switch-out should occur.
|
||||
*
|
||||
* @param pokemon The Pokémon that took damage.
|
||||
* @param damage N/A
|
||||
* @param passive N/A
|
||||
* @param simulated Whether the ability is being simulated.
|
||||
* @param args N/A
|
||||
* @param source N/A
|
||||
*/
|
||||
public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): void {
|
||||
this.helper.switchOutLogic(pokemon);
|
||||
this.doSwitch(pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAbAttrsInternal<TAttr extends AbAttr>(
|
||||
attrType: Constructor<TAttr>,
|
||||
pokemon: Pokemon | null,
|
||||
|
184
src/data/mixins/force-switch.ts
Normal file
184
src/data/mixins/force-switch.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { SwitchPhase } from "#app/phases/switch-phase";
|
||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||
import { BooleanHolder } from "#app/utils/common";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { type NormalSwitchType, SwitchType } from "#enums/switch-type";
|
||||
import i18next from "i18next";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { applyAbAttrs, ForceSwitchOutImmunityAbAttr } from "../abilities/ability";
|
||||
import type { MoveAttr } from "../moves/move";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { AbAttr } from "../abilities/ab-attrs/ab-attr";
|
||||
import type { TrainerSlot } from "#enums/trainer-slot";
|
||||
|
||||
// NB: This shouldn't be terribly hard to extend from if switching items are added (à la Eject Button)
|
||||
type SubMoveOrAbAttr = (new (...args: any[]) => MoveAttr) | (new (...args: any[]) => AbAttr);
|
||||
|
||||
/** Mixin to handle shared logic for switch-in moves and abilities. */
|
||||
export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
||||
return class ForceSwitchClass extends Base {
|
||||
protected selfSwitch = false;
|
||||
protected switchType: NormalSwitchType = SwitchType.SWITCH;
|
||||
|
||||
/**
|
||||
* Determines if a Pokémon can be forcibly switched out based on its status, the opponent's status and battle conditions.
|
||||
* @see {@linkcode performOpponentChecks} for opponent-related check code.
|
||||
|
||||
* @param switchOutTarget - The {@linkcode Pokemon} attempting to switch out.
|
||||
* @param opponent - The {@linkcode Pokemon} opposing the currently switched out Pokemon.
|
||||
* Unused if {@linkcode selfSwitch} is `true`, in which case it should conventionally be set to `undefined`.
|
||||
* @returns Whether {@linkcode switchOutTarget} can be switched out by the current Move or Ability.
|
||||
*/
|
||||
protected canSwitchOut(switchOutTarget: Pokemon, opponent: Pokemon | undefined): boolean {
|
||||
const isPlayer = switchOutTarget instanceof PlayerPokemon;
|
||||
|
||||
if (!this.selfSwitch && opponent && !this.performOpponentChecks(switchOutTarget, opponent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPlayer && globalScene.currentBattle.battleType === BattleType.WILD) {
|
||||
// enemies should not be allowed to flee with baton pass, nor by any means on X0 waves (don't want easy boss wins)
|
||||
return this.switchType !== SwitchType.BATON_PASS && globalScene.currentBattle.waveIndex % 10 !== 0;
|
||||
}
|
||||
|
||||
// Finally, ensure that we have valid switch out targets.
|
||||
const reservePartyMembers = globalScene.getBackupPartyMembers(
|
||||
isPlayer,
|
||||
(switchOutTarget as EnemyPokemon).trainerSlot as TrainerSlot | undefined,
|
||||
); // evaluates to `undefined` if not present
|
||||
if (reservePartyMembers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected performOpponentChecks(switchOutTarget: Pokemon, opponent: Pokemon): boolean {
|
||||
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out by enemies
|
||||
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
|
||||
if (commandedTag?.getSourcePokemon()?.isActive(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for opposing switch block abilities (Suction Cups and co)
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
|
||||
if (!blockedByAbility.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!(switchOutTarget instanceof PlayerPokemon) &&
|
||||
globalScene.currentBattle.isBattleMysteryEncounter() &&
|
||||
!globalScene.currentBattle.mysteryEncounter?.fleeAllowed
|
||||
) {
|
||||
// Wild opponents cannot be force switched during MEs with flee disabled
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function to handle the actual "switching out" of Pokemon.
|
||||
* @param switchOutTarget - The {@linkcode Pokemon} (player or enemy) attempting to switch out.
|
||||
*/
|
||||
protected doSwitch(switchOutTarget: Pokemon): void {
|
||||
if (switchOutTarget instanceof PlayerPokemon) {
|
||||
this.trySwitchPlayerPokemon(switchOutTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(switchOutTarget instanceof EnemyPokemon)) {
|
||||
console.warn("Switched out target not instance of Player or enemy Pokemon!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (globalScene.currentBattle.battleType !== BattleType.WILD) {
|
||||
this.trySwitchTrainerPokemon(switchOutTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tryFleeWildPokemon(switchOutTarget);
|
||||
}
|
||||
|
||||
private trySwitchPlayerPokemon(switchOutTarget: PlayerPokemon): void {
|
||||
// If not forced to switch, add a SwitchPhase to allow picking the next switched in Pokemon.
|
||||
if (this.switchType !== SwitchType.FORCE_SWITCH) {
|
||||
globalScene.appendToPhase(
|
||||
new SwitchPhase(this.switchType, switchOutTarget.getFieldIndex(), true, true),
|
||||
MoveEndPhase,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pick a random player pokemon to switch out.
|
||||
const reservePartyMembers = globalScene.getBackupPartyMembers(true);
|
||||
const switchOutIndex = switchOutTarget.randSeedInt(reservePartyMembers.length);
|
||||
|
||||
globalScene.appendToPhase(
|
||||
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), switchOutIndex, false, true),
|
||||
MoveEndPhase,
|
||||
);
|
||||
}
|
||||
|
||||
private trySwitchTrainerPokemon(switchOutTarget: EnemyPokemon): void {
|
||||
// fallback for no trainer
|
||||
if (!globalScene.currentBattle.trainer) {
|
||||
console.warn("Enemy trainer switch logic approached without a trainer!");
|
||||
return;
|
||||
}
|
||||
// Forced switches will to pick a random eligible pokemon, while
|
||||
// choice-based switching uses the trainer's default switch behavior
|
||||
const reservePartyMembers = globalScene.getBackupPartyMembers(false, switchOutTarget.trainerSlot);
|
||||
const summonIndex =
|
||||
this.switchType === SwitchType.FORCE_SWITCH
|
||||
? switchOutTarget.randSeedInt(reservePartyMembers.length)
|
||||
: (globalScene.currentBattle.trainer.getNextSummonIndex(switchOutTarget.trainerSlot) ?? 0);
|
||||
globalScene.appendToPhase(
|
||||
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false),
|
||||
MoveEndPhase,
|
||||
);
|
||||
}
|
||||
|
||||
private tryFleeWildPokemon(switchOutTarget: EnemyPokemon): void {
|
||||
// flee wild pokemon, redirecting moves to an ally in doubles as applicable.
|
||||
switchOutTarget.leaveField(false);
|
||||
globalScene.queueMessage(
|
||||
i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }),
|
||||
null,
|
||||
true,
|
||||
500,
|
||||
);
|
||||
|
||||
const allyPokemon = switchOutTarget.getAlly();
|
||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
||||
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||
}
|
||||
|
||||
// End battle if no enemies are active and enemy wasn't already KO'd (kos do )
|
||||
if (!allyPokemon?.isActive(true) && !switchOutTarget.isFainted()) {
|
||||
globalScene.clearEnemyHeldItemModifiers();
|
||||
|
||||
globalScene.pushPhase(new BattleEndPhase(false));
|
||||
|
||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
globalScene.pushPhase(new SelectBiomePhase());
|
||||
}
|
||||
|
||||
globalScene.pushPhase(new NewBattlePhase());
|
||||
}
|
||||
}
|
||||
|
||||
public isBatonPass(): boolean {
|
||||
return this.switchType === SwitchType.BATON_PASS;
|
||||
}
|
||||
};
|
||||
}
|
@ -48,7 +48,6 @@ import {
|
||||
ConfusionOnStatusEffectAbAttr,
|
||||
FieldMoveTypePowerBoostAbAttr,
|
||||
FieldPreventExplosiveMovesAbAttr,
|
||||
ForceSwitchOutImmunityAbAttr,
|
||||
HealFromBerryUseAbAttr,
|
||||
IgnoreContactAbAttr,
|
||||
IgnoreMoveEffectsAbAttr,
|
||||
@ -108,7 +107,7 @@ import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||
import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms";
|
||||
import type { GameMode } from "#app/game-mode";
|
||||
import { applyChallenges, ChallengeType } from "../challenge";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { SwitchType, type NormalSwitchType } from "#enums/switch-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
|
||||
@ -122,7 +121,7 @@ import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
|
||||
import { MultiHitType } from "#enums/MultiHitType";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { ForceSwitch } from "../mixins/force-switch";
|
||||
|
||||
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
||||
type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
||||
@ -6210,218 +6209,35 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
|
||||
export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) {
|
||||
constructor(
|
||||
private selfSwitch: boolean = false,
|
||||
private switchType: SwitchType = SwitchType.SWITCH
|
||||
selfSwitch: boolean = false,
|
||||
switchType: NormalSwitchType = SwitchType.SWITCH
|
||||
) {
|
||||
super(false, { lastHitOnly: true });
|
||||
this.selfSwitch = selfSwitch;
|
||||
this.switchType = switchType;
|
||||
}
|
||||
|
||||
isBatonPass() {
|
||||
return this.switchType === SwitchType.BATON_PASS;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// Check if the move category is not STATUS or if the switch out condition is not met
|
||||
if (!this.getSwitchOutCondition()(user, target, move)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The {@linkcode Pokemon} to be switched out with this effect */
|
||||
const switchOutTarget = this.selfSwitch ? user : target;
|
||||
|
||||
// If the switch-out target is a Dondozo with a Tatsugiri in its mouth
|
||||
// (e.g. when it uses Flip Turn), make it spit out the Tatsugiri before switching out.
|
||||
switchOutTarget.lapseTag(BattlerTagType.COMMANDED);
|
||||
|
||||
if (switchOutTarget instanceof PlayerPokemon) {
|
||||
/**
|
||||
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
|
||||
* If it did, the user of U-turn or Volt Switch will not be switched out.
|
||||
*/
|
||||
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr)
|
||||
&& [ Moves.U_TURN, Moves.VOLT_SWITCH, Moves.FLIP_TURN ].includes(move.id)
|
||||
) {
|
||||
if (this.hpDroppedBelowHalf(target)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find indices of off-field Pokemon that are eligible to be switched into
|
||||
const eligibleNewIndices: number[] = [];
|
||||
globalScene.getPlayerParty().forEach((pokemon, index) => {
|
||||
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
|
||||
eligibleNewIndices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (eligibleNewIndices.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.prependToPhase(
|
||||
new SwitchSummonPhase(
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
slotIndex,
|
||||
false,
|
||||
true
|
||||
),
|
||||
MoveEndPhase
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.prependToPhase(
|
||||
new SwitchPhase(
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
true,
|
||||
true
|
||||
),
|
||||
MoveEndPhase
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (globalScene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
||||
// Find indices of off-field Pokemon that are eligible to be switched into
|
||||
const isPartnerTrainer = globalScene.currentBattle.trainer?.isPartner();
|
||||
const eligibleNewIndices: number[] = [];
|
||||
globalScene.getEnemyParty().forEach((pokemon, index) => {
|
||||
if (pokemon.isAllowedInBattle() && !pokemon.isOnField() && (!isPartnerTrainer || pokemon.trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)) {
|
||||
eligibleNewIndices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (eligibleNewIndices.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.prependToPhase(
|
||||
new SwitchSummonPhase(
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
slotIndex,
|
||||
false,
|
||||
false
|
||||
),
|
||||
MoveEndPhase
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.prependToPhase(
|
||||
new SwitchSummonPhase(
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
(globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
|
||||
false,
|
||||
false
|
||||
),
|
||||
MoveEndPhase
|
||||
);
|
||||
}
|
||||
}
|
||||
} else { // Switch out logic for wild pokemon
|
||||
/**
|
||||
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
|
||||
* If it did, the user of U-turn or Volt Switch will not be switched out.
|
||||
*/
|
||||
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr)
|
||||
&& [ Moves.U_TURN, Moves.VOLT_SWITCH, Moves.FLIP_TURN ].includes(move.id)
|
||||
) {
|
||||
if (this.hpDroppedBelowHalf(target)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const allyPokemon = switchOutTarget.getAlly();
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(false);
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||
|
||||
// in double battles redirect potential moves off fled pokemon
|
||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
||||
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||
}
|
||||
}
|
||||
|
||||
// clear out enemy held item modifiers of the switch out target
|
||||
globalScene.clearEnemyHeldItemModifiers(switchOutTarget);
|
||||
|
||||
if (!allyPokemon?.isActive(true) && switchOutTarget.hp) {
|
||||
globalScene.pushPhase(new BattleEndPhase(false));
|
||||
|
||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
globalScene.pushPhase(new SelectBiomePhase());
|
||||
}
|
||||
|
||||
globalScene.pushPhase(new NewBattlePhase());
|
||||
}
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
this.doSwitch(this.selfSwitch ? user : target)
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => (move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move));
|
||||
return this.getSwitchOutCondition();
|
||||
}
|
||||
|
||||
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
|
||||
if (blockedByAbility.value) {
|
||||
return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getSwitchOutCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const switchOutTarget = (this.selfSwitch ? user : target);
|
||||
const player = switchOutTarget instanceof PlayerPokemon;
|
||||
const [switchOutTarget, opponent] = this.selfSwitch ? [user, target] : [target, user];
|
||||
|
||||
if (!this.selfSwitch) {
|
||||
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out
|
||||
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
|
||||
if (commandedTag?.getSourcePokemon()?.isActive(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!player && globalScene.currentBattle.isBattleMysteryEncounter() && !globalScene.currentBattle.mysteryEncounter?.fleeAllowed) {
|
||||
// Don't allow wild opponents to be force switched during MEs with flee disabled
|
||||
return false;
|
||||
}
|
||||
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
|
||||
if (blockedByAbility.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!player && globalScene.currentBattle.battleType === BattleType.WILD) {
|
||||
// wild pokemon cannot switch out with baton pass.
|
||||
return !this.isBatonPass()
|
||||
&& globalScene.currentBattle.waveIndex % 10 !== 0
|
||||
// Don't allow wild mons to flee with U-turn et al.
|
||||
&& !(this.selfSwitch && MoveCategory.STATUS !== move.category);
|
||||
if (switchOutTarget instanceof EnemyPokemon && globalScene.currentBattle.battleType === BattleType.WILD && !(this.selfSwitch && move.category !== MoveCategory.STATUS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const party = player ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
return party.filter(p => p.isAllowedInBattle() && !p.isOnField()
|
||||
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > 0;
|
||||
return this.canSwitchOut(switchOutTarget, opponent)
|
||||
};
|
||||
}
|
||||
|
||||
@ -6460,7 +6276,8 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr {
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
// chilly reception move will go through if the weather is change-able to snow, or the user can switch out, else move will fail
|
||||
// chilly reception will succeed if the weather is changeable to snow OR the user can be switched out,
|
||||
// only failing if neither is the case.
|
||||
return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move);
|
||||
}
|
||||
}
|
||||
|
@ -343,16 +343,13 @@ export default class MysteryEncounter implements IMysteryEncounter {
|
||||
* can cause scenarios where there are not enough Pokemon that are sufficient for all requirements.
|
||||
*/
|
||||
private meetsPrimaryRequirementAndPrimaryPokemonSelected(): boolean {
|
||||
let qualified: PlayerPokemon[] = globalScene.getPlayerParty();
|
||||
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
|
||||
const activeMon = globalScene.getPlayerParty().filter(p => p.isActive(true));
|
||||
if (activeMon.length > 0) {
|
||||
this.primaryPokemon = activeMon[0];
|
||||
} else {
|
||||
this.primaryPokemon = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle())[0];
|
||||
}
|
||||
// If we lack specified criterion, grab the first on-field pokemon, or else the first pokemon allowed in battle
|
||||
const activeMons = qualified.filter(p => p.isAllowedInBattle());
|
||||
this.primaryPokemon = activeMons.find(p => p.isOnField()) ?? activeMons[0];
|
||||
return true;
|
||||
}
|
||||
let qualified: PlayerPokemon[] = globalScene.getPlayerParty();
|
||||
for (const req of this.primaryPokemonRequirements) {
|
||||
if (req.meetsRequirement()) {
|
||||
qualified = qualified.filter(pkmn => req.queryParty(globalScene.getPlayerParty()).includes(pkmn));
|
||||
|
@ -14,3 +14,5 @@ export enum SwitchType {
|
||||
/** Force switchout to a random party member */
|
||||
FORCE_SWITCH,
|
||||
}
|
||||
|
||||
export type NormalSwitchType = Exclude<SwitchType, SwitchType.INITIAL_SWITCH>
|
@ -5723,8 +5723,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData}
|
||||
* in preparation for switching pokemon, as well as removing any relevant on-switch tags.
|
||||
* @remarks
|
||||
* This **SHOULD NOT** be called when a `SummonPhase` or `SwitchSummonPhase` is already being added,
|
||||
* both of which call this method (directly or indirectly) on both pokemon changing positions.
|
||||
*/
|
||||
resetSummonData(): void {
|
||||
console.log(`resetSummonData called on Pokemon ${this.name}`)
|
||||
const illusion: IllusionData | null = this.summonData.illusion;
|
||||
if (this.summonData.speciesForm) {
|
||||
this.summonData.speciesForm = null;
|
||||
@ -6299,13 +6303,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes a Pokemon to leave the field (such as in preparation for a switch out/escape).
|
||||
* @param clearEffects Indicates if effects should be cleared (true) or passed
|
||||
* to the next pokemon, such as during a baton pass (false)
|
||||
* @param hideInfo Indicates if this should also play the animation to hide the Pokemon's
|
||||
* info container.
|
||||
* Cause this {@linkcode Pokemon} to leave the field (such as in preparation for a switch out/escape).
|
||||
* @param clearEffects - Whether to clear (`true`) or transfer (`false`) transient effects upon switching; default `true`
|
||||
* @param hideInfo - Whether to play the animation to hide the Pokemon's info container; default `true`.
|
||||
* @param destroy - Whether to destroy this Pokemon once it leaves the field; default `false`
|
||||
* @remarks
|
||||
* This **SHOULD NOT** be called when a `SummonPhase` or `SwitchSummonPhase` is already being added,
|
||||
* which can lead to erroneous resetting of {@linkcode turnData} or {@linkcode summonData}.
|
||||
*/
|
||||
leaveField(clearEffects = true, hideInfo = true, destroy = false) {
|
||||
console.log(`leaveField called on Pokemon ${this.name}`)
|
||||
this.resetSprite();
|
||||
this.resetTurnData();
|
||||
globalScene
|
||||
|
@ -17,13 +17,12 @@ export class SwitchPhase extends BattlePhase {
|
||||
private readonly doReturn: boolean;
|
||||
|
||||
/**
|
||||
* Creates a new SwitchPhase
|
||||
* @param switchType {@linkcode SwitchType} The type of switch logic this phase implements
|
||||
* @param fieldIndex Field index to switch out
|
||||
* @param isModal Indicates if the switch should be forced (true) or is
|
||||
* optional (false).
|
||||
* @param doReturn Indicates if the party member on the field should be
|
||||
* recalled to ball or has already left the field. Passed to {@linkcode SwitchSummonPhase}.
|
||||
* Creates a new {@linkcode SwitchPhase}, the phase where players select a Pokemon to send into battle.
|
||||
* @param switchType - The {@linkcode SwitchType} dictating this switch's logic.
|
||||
* @param fieldIndex - The 0-indexed field position of the Pokemon being switched out.
|
||||
* @param isModal - Whether the switch should be forced (`true`) or optional (`false`).
|
||||
* @param doReturn - Whether to render the "Come back!" dialogue for recalling player pokemon.
|
||||
* @see {@linkcode SwitchSummonPhase} for the phase which does the actual switching.
|
||||
*/
|
||||
constructor(switchType: SwitchType, fieldIndex: number, isModal: boolean, doReturn: boolean) {
|
||||
super();
|
||||
@ -37,8 +36,8 @@ export class SwitchPhase extends BattlePhase {
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
// Skip modal switch if impossible (no remaining party members that aren't in battle)
|
||||
if (this.isModal && !globalScene.getPlayerParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) {
|
||||
// Failsafe: skip modal switches if impossible (no eligible party members in reserve).
|
||||
if (this.isModal && globalScene.getBackupPartyMembers(true).length === 0) {
|
||||
return super.end();
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
private lastPokemon: Pokemon;
|
||||
|
||||
/**
|
||||
* Constructor for creating a new SwitchSummonPhase
|
||||
* Constructor for creating a new {@linkcode SwitchSummonPhase}, the phase where player and enemy Pokemon are switched out.
|
||||
* @param switchType - The type of switch behavior
|
||||
* @param fieldIndex - Position on the battle field
|
||||
* @param slotIndex - The index of pokemon (in party of 6) to switch into
|
||||
|
@ -32,25 +32,34 @@ describe("Moves - U-turn", () => {
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => {
|
||||
// arrange
|
||||
const playerHp = 1;
|
||||
game.override.ability(Abilities.REGENERATOR);
|
||||
it("should switch the user out upon use", async () => {
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
game.scene.getPlayerPokemon()!.hp = playerHp;
|
||||
const [raichu, shuckle] = game.scene.getPlayerParty();
|
||||
expect(raichu).toBeDefined();
|
||||
expect(shuckle).toBeDefined();
|
||||
|
||||
// act
|
||||
expect(game.scene.getPlayerPokemon()!).toBe(raichu);
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// assert
|
||||
expect(game.scene.getPlayerParty()[1].hp).toEqual(
|
||||
Math.floor(game.scene.getPlayerParty()[1].getMaxHp() * 0.33 + playerHp),
|
||||
);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()!).toBe(shuckle);
|
||||
});
|
||||
|
||||
it("triggers regenerator passive once upon switch", async () => {
|
||||
game.override.ability(Abilities.REGENERATOR);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
game.scene.getPlayerPokemon()!.hp = 1;
|
||||
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.getPlayerParty()[1].hp).toBeGreaterThan(1);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => {
|
||||
// arrange
|
||||
|
Loading…
Reference in New Issue
Block a user