Added mixin for ForceSwitch to handle shared logic among move/ability classes

This commit is contained in:
Bertie690 2025-05-14 18:23:16 -04:00
parent 02cac77853
commit 37ad950093
10 changed files with 308 additions and 373 deletions

View File

@ -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.

View File

@ -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() {
@ -1246,7 +1248,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
/**
* Determine if the move type change attribute can be applied
*
*
* Can be applied if:
* - The ability's condition is met, e.g. pixilate only boosts normal moves,
* - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode Moves.MULTI_ATTACK}
@ -1262,7 +1264,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
*/
override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean {
return (!this.condition || this.condition(pokemon, _defender, move)) &&
!noAbilityTypeOverrideMoves.has(move.id) &&
!noAbilityTypeOverrideMoves.has(move.id) &&
(!pokemon.isTerastallized ||
(move.id !== Moves.TERA_BLAST &&
(move.id !== Moves.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(Species.TERAPAGOS))));
@ -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,

View 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;
}
};
}

View File

@ -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());
}
}
return true;
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;
}
// Don't allow wild mons to flee with U-turn et al.
if (switchOutTarget instanceof EnemyPokemon && globalScene.currentBattle.battleType === BattleType.WILD && !(this.selfSwitch && move.category !== MoveCategory.STATUS)) {
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);
}
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);
}
}

View File

@ -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));

View File

@ -14,3 +14,5 @@ export enum SwitchType {
/** Force switchout to a random party member */
FORCE_SWITCH,
}
export type NormalSwitchType = Exclude<SwitchType, SwitchType.INITIAL_SWITCH>

View File

@ -5678,7 +5678,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Performs the action of clearing a Pokemon's status
*
*
* This is a helper to {@linkcode resetStatus}, which should be called directly instead of this method
*/
public clearStatus(confusion: boolean, reloadAssets: boolean) {
@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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