pokerogue/src/phases/summon-phase.ts
Dean 0c48fff14b
[Bug] Fix Substitute sprite crash & revived Pokemon softlock (#5829)
Break `resetSummonData` into two methods

Co-authored-by: damocleas <damocleas25@gmail.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: AJ Fontaine <36677462+Fontbane@users.noreply.github.com>
Co-authored-by: lxy-lxy-lxy <55084073+lxy-lxy-lxy@users.noreply.github.com>
Co-authored-by: Xavion3 <xavion333@gmail.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: Lylian BALL <131535108+PyGaVS@users.noreply.github.com>
2025-05-17 10:35:38 -05:00

301 lines
10 KiB
TypeScript

import { BattleType } from "#enums/battle-type";
import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { TrainerSlot } from "#enums/trainer-slot";
import { PlayerGender } from "#app/enums/player-gender";
import { addPokeballOpenParticles } from "#app/field/anims";
import type Pokemon from "#app/field/pokemon";
import { FieldPosition } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import i18next from "i18next";
import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase";
import { PostSummonPhase } from "./post-summon-phase";
import { GameOverPhase } from "./game-over-phase";
import { ShinySparklePhase } from "./shiny-sparkle-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { applyPreSummonAbAttrs, PreSummonAbAttr } from "#app/data/abilities/ability";
import { globalScene } from "#app/global-scene";
export class SummonPhase extends PartyMemberPokemonPhase {
private loaded: boolean;
constructor(fieldIndex: number, player = true, loaded = false) {
super(fieldIndex, player);
this.loaded = loaded;
}
start() {
super.start();
applyPreSummonAbAttrs(PreSummonAbAttr, this.getPokemon());
this.preSummon();
}
/**
* Sends out a Pokemon before the battle begins and shows the appropriate messages
*/
preSummon(): void {
const partyMember = this.getPokemon();
// If the Pokemon about to be sent out is fainted, illegal under a challenge, or no longer in the party for some reason, switch to the first non-fainted legal Pokemon
if (!partyMember.isAllowedInBattle() || (this.player && !this.getParty().some(p => p.id === partyMember.id))) {
console.warn(
"The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve...",
);
// First check if they're somehow still in play, if so remove them.
if (partyMember.isOnField()) {
partyMember.leaveField();
}
const party = this.getParty();
// Find the first non-fainted Pokemon index above the current one
const legalIndex = party.findIndex((p, i) => i > this.partyMemberIndex && p.isAllowedInBattle());
if (legalIndex === -1) {
console.error("Party Details:\n", party);
console.error("All available Pokemon were fainted or illegal!");
globalScene.clearPhaseQueue();
globalScene.unshiftPhase(new GameOverPhase());
this.end();
return;
}
// Swaps the fainted Pokemon and the first non-fainted legal Pokemon in the party
[party[this.partyMemberIndex], party[legalIndex]] = [party[legalIndex], party[this.partyMemberIndex]];
console.warn(
"Swapped %s %O with %s %O",
getPokemonNameWithAffix(partyMember),
partyMember,
getPokemonNameWithAffix(party[0]),
party[0],
);
}
if (this.player) {
globalScene.ui.showText(
i18next.t("battle:playerGo", {
pokemonName: getPokemonNameWithAffix(this.getPokemon()),
}),
);
if (this.player) {
globalScene.pbTray.hide();
}
globalScene.trainer.setTexture(
`trainer_${globalScene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`,
);
globalScene.time.delayedCall(562, () => {
globalScene.trainer.setFrame("2");
globalScene.time.delayedCall(64, () => {
globalScene.trainer.setFrame("3");
});
});
globalScene.tweens.add({
targets: globalScene.trainer,
x: -36,
duration: 1000,
onComplete: () => globalScene.trainer.setVisible(false),
});
globalScene.time.delayedCall(750, () => this.summon());
} else if (
globalScene.currentBattle.battleType === BattleType.TRAINER ||
globalScene.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE
) {
const trainerName = globalScene.currentBattle.trainer?.getName(
!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
);
const pokemonName = this.getPokemon().getNameToRender();
const message = i18next.t("battle:trainerSendOut", {
trainerName,
pokemonName,
});
globalScene.pbTrayEnemy.hide();
globalScene.ui.showText(message, null, () => this.summon());
} else if (globalScene.currentBattle.isBattleMysteryEncounter()) {
globalScene.pbTrayEnemy.hide();
this.summonWild();
}
}
/**
* Enemy trainer or player trainer will do animations to throw Pokeball and summon a Pokemon to the field.
*/
summon(): void {
const pokemon = this.getPokemon();
const pokeball = globalScene.addFieldSprite(
this.player ? 36 : 248,
this.player ? 80 : 44,
"pb",
getPokeballAtlasKey(pokemon.getPokeball(true)),
);
pokeball.setVisible(false);
pokeball.setOrigin(0.5, 0.625);
globalScene.field.add(pokeball);
if (this.fieldIndex === 1) {
pokemon.setFieldPosition(FieldPosition.RIGHT, 0);
} else {
const availablePartyMembers = this.getParty().filter(p => p.isAllowedInBattle()).length;
pokemon.setFieldPosition(
!globalScene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT,
);
}
const fpOffset = pokemon.getFieldPositionOffset();
pokeball.setVisible(true);
globalScene.tweens.add({
targets: pokeball,
duration: 650,
x: (this.player ? 100 : 236) + fpOffset[0],
});
globalScene.tweens.add({
targets: pokeball,
duration: 150,
ease: "Cubic.easeOut",
y: (this.player ? 70 : 34) + fpOffset[1],
onComplete: () => {
globalScene.tweens.add({
targets: pokeball,
duration: 500,
ease: "Cubic.easeIn",
angle: 1440,
y: (this.player ? 132 : 86) + fpOffset[1],
onComplete: () => {
globalScene.playSound("se/pb_rel");
pokeball.destroy();
globalScene.add.existing(pokemon);
globalScene.field.add(pokemon);
if (!this.player) {
const playerPokemon = globalScene.getPlayerPokemon() as Pokemon;
if (playerPokemon?.isOnField()) {
globalScene.field.moveBelow(pokemon, playerPokemon);
}
globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id);
}
addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.getPokeball(true));
globalScene.updateModifiers(this.player);
globalScene.updateFieldScale();
pokemon.showInfo();
pokemon.playAnim();
pokemon.setVisible(true);
pokemon.getSprite().setVisible(true);
pokemon.setScale(0.5);
pokemon.tint(getPokeballTintColor(pokemon.getPokeball(true)));
pokemon.untint(250, "Sine.easeIn");
globalScene.updateFieldScale();
globalScene.tweens.add({
targets: pokemon,
duration: 250,
ease: "Sine.easeIn",
scale: pokemon.getSpriteScale(),
onComplete: () => {
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
pokemon.getSprite().clearTint();
pokemon.fieldSetup();
// necessary to stay transformed during wild waves
if (pokemon.summonData.speciesForm) {
pokemon.loadAssets(false);
}
globalScene.time.delayedCall(1000, () => this.end());
},
});
},
});
},
});
}
/**
* Handles tweening and battle setup for a wild Pokemon that appears outside of the normal screen transition.
* Wild Pokemon will ease and fade in onto the field, then perform standard summon behavior.
* Currently only used by Mystery Encounters, as all other battle types pre-summon wild pokemon before screen transitions.
*/
summonWild(): void {
const pokemon = this.getPokemon();
if (this.fieldIndex === 1) {
pokemon.setFieldPosition(FieldPosition.RIGHT, 0);
} else {
const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length;
pokemon.setFieldPosition(
!globalScene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT,
);
}
globalScene.add.existing(pokemon);
globalScene.field.add(pokemon);
if (!this.player) {
const playerPokemon = globalScene.getPlayerPokemon() as Pokemon;
if (playerPokemon?.isOnField()) {
globalScene.field.moveBelow(pokemon, playerPokemon);
}
globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id);
}
globalScene.updateModifiers(this.player);
globalScene.updateFieldScale();
pokemon.showInfo();
pokemon.playAnim();
pokemon.setVisible(true);
pokemon.getSprite().setVisible(true);
pokemon.setScale(0.75);
pokemon.tint(getPokeballTintColor(pokemon.pokeball));
pokemon.untint(250, "Sine.easeIn");
globalScene.updateFieldScale();
pokemon.x += 16;
pokemon.y -= 20;
pokemon.alpha = 0;
// Ease pokemon in
globalScene.tweens.add({
targets: pokemon,
x: "-=16",
y: "+=16",
alpha: 1,
duration: 1000,
ease: "Sine.easeIn",
scale: pokemon.getSpriteScale(),
onComplete: () => {
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
pokemon.getSprite().clearTint();
pokemon.fieldSetup();
globalScene.updateFieldScale();
globalScene.time.delayedCall(1000, () => this.end());
},
});
}
onEnd(): void {
const pokemon = this.getPokemon();
if (pokemon.isShiny(true)) {
globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex()));
}
pokemon.resetTurnData();
if (
!this.loaded ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) ||
globalScene.currentBattle.waveIndex % 10 === 1
) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
this.queuePostSummon();
}
}
queuePostSummon(): void {
globalScene.pushPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex()));
}
end() {
this.onEnd();
super.end();
}
}