import { globalScene } from "#app/global-scene"; import { applyPreSummonAbAttrs, applyPreSwitchOutAbAttrs, PostDamageForceSwitchAbAttr, PreSummonAbAttr, PreSwitchOutAbAttr, } from "#app/data/abilities/ability"; import { ForceSwitchOutAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { getPokeballTintColor } from "#app/data/pokeball"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { TrainerSlot } from "#enums/trainer-slot"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { SwitchEffectTransferModifier } from "#app/modifier/modifier"; import { Command } from "#app/ui/command-ui-handler"; import i18next from "i18next"; import { SummonPhase } from "./summon-phase"; import { SubstituteTag } from "#app/data/battler-tags"; import { SwitchType } from "#enums/switch-type"; export class SwitchSummonPhase extends SummonPhase { public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" = "SwitchSummonPhase"; private readonly switchType: SwitchType; private readonly slotIndex: number; private readonly doReturn: boolean; private lastPokemon: Pokemon; /** * Constructor for creating a new SwitchSummonPhase * @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 * @param doReturn - Whether to render "comeback" dialogue * @param player - Whether the switch came from the player or enemy; default `true` */ constructor(switchType: SwitchType, fieldIndex: number, slotIndex: number, doReturn: boolean, player = true) { super(fieldIndex, player); this.switchType = switchType; this.slotIndex = slotIndex; this.doReturn = doReturn; } start(): void { super.start(); } preSummon(): void { if (!this.player) { if (this.slotIndex === -1) { //@ts-ignore this.slotIndex = globalScene.currentBattle.trainer?.getNextSummonIndex( !this.fieldIndex ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, ); // TODO: what would be the default trainer-slot fallback? } if (this.slotIndex > -1) { this.showEnemyTrainer(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); globalScene.pbTrayEnemy.showPbTray(globalScene.getEnemyParty()); } } if ( !this.doReturn || (this.slotIndex !== -1 && !(this.player ? globalScene.getPlayerParty() : globalScene.getEnemyParty())[this.slotIndex]) ) { if (this.player) { this.switchAndSummon(); return; } globalScene.time.delayedCall(750, () => this.switchAndSummon()); return; } const pokemon = this.getPokemon(); (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id), ); if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { const substitute = pokemon.getTag(SubstituteTag); if (substitute) { globalScene.tweens.add({ targets: substitute.sprite, duration: 250, scale: substitute.sprite.scale * 0.5, ease: "Sine.easeIn", onComplete: () => substitute.sprite.destroy(), }); } } globalScene.ui.showText( this.player ? i18next.t("battle:playerComeBack", { pokemonName: getPokemonNameWithAffix(pokemon), }) : i18next.t("battle:trainerComeBack", { trainerName: globalScene.currentBattle.trainer?.getName( !(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, ), pokemonName: pokemon.getNameToRender(), }), ); globalScene.playSound("se/pb_rel"); pokemon.hideInfo(); pokemon.tint(getPokeballTintColor(pokemon.getPokeball(true)), 1, 250, "Sine.easeIn"); globalScene.tweens.add({ targets: pokemon, duration: 250, ease: "Sine.easeIn", scale: 0.5, onComplete: () => { globalScene.time.delayedCall(750, () => this.switchAndSummon()); pokemon.leaveField(this.switchType === SwitchType.SWITCH, false); }, }); } switchAndSummon() { const party = this.player ? this.getParty() : globalScene.getEnemyParty(); const switchedInPokemon: Pokemon | undefined = party[this.slotIndex]; this.lastPokemon = this.getPokemon(); // Defensive programming: Overcome the bug where the summon data has somehow not been reset // prior to switching in a new Pokemon. // Force the switch to occur and load the assets for the new pokemon, ignoring override. switchedInPokemon.resetSummonData(); switchedInPokemon.loadAssets(true); applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); if (!switchedInPokemon) { this.end(); return; } if (this.switchType === SwitchType.BATON_PASS) { // If switching via baton pass, update opposing tags coming from the prior pokemon (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id), ); // If the recipient pokemon lacks a baton, give our baton to it during the swap if ( !globalScene.findModifier( m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id, ) ) { const batonPassModifier = globalScene.findModifier( m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id, ) as SwitchEffectTransferModifier; if (batonPassModifier) { globalScene.tryTransferHeldItemModifier( batonPassModifier, switchedInPokemon, false, undefined, undefined, undefined, false, ); } } } party[this.slotIndex] = this.lastPokemon; party[this.fieldIndex] = switchedInPokemon; const showTextAndSummon = () => { globalScene.ui.showText( this.player ? i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(switchedInPokemon), }) : i18next.t("battle:trainerGo", { trainerName: globalScene.currentBattle.trainer?.getName( !(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, ), pokemonName: this.getPokemon().getNameToRender(), }), ); /** * If this switch is passing a Substitute, make the switched Pokemon matches the returned Pokemon's state as it left. * Otherwise, clear any persisting tags on the returned Pokemon. */ if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { const substitute = this.lastPokemon.getTag(SubstituteTag); if (substitute) { switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; switchedInPokemon.setAlpha(0.5); } } else { switchedInPokemon.fieldSetup(true); } this.summon(); }; if (this.player) { showTextAndSummon(); } else { globalScene.time.delayedCall(1500, () => { this.hideEnemyTrainer(); globalScene.pbTrayEnemy.hide(); showTextAndSummon(); }); } } onEnd(): void { super.onEnd(); const pokemon = this.getPokemon(); const moveId = globalScene.currentBattle.lastMove; const lastUsedMove = moveId ? allMoves[moveId] : undefined; const currentCommand = globalScene.currentBattle.turnCommands[this.fieldIndex]?.command; const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted(); const lastPokemonHasForceSwitchAbAttr = this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted(); // Compensate for turn spent summoning/forced switch if switched out pokemon is not fainted. // Needed as we increment turn counters in `TurnEndPhase`. if ( currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted || lastPokemonHasForceSwitchAbAttr ) { pokemon.tempSummonData.turnCount--; pokemon.tempSummonData.waveTurnCount--; } if (this.switchType === SwitchType.BATON_PASS && pokemon) { pokemon.transferSummon(this.lastPokemon); } else if (this.switchType === SwitchType.SHED_TAIL && pokemon) { const subTag = this.lastPokemon.getTag(SubstituteTag); if (subTag) { pokemon.summonData.tags.push(subTag); } } // Reset turn data if not initial switch (since it gets initialized to an empty object on turn start) if (this.switchType !== SwitchType.INITIAL_SWITCH) { pokemon.resetTurnData(); pokemon.turnData.switchedInThisTurn = true; } this.lastPokemon.resetSummonData(); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out globalScene.arena.triggerWeatherBasedFormChanges(); } queuePostSummon(): void { globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); } }