pokerogue/src/phases/evolution-phase.ts

722 lines
21 KiB
TypeScript

import type { AnySound } from "#app/battle-scene";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import { Phase } from "#app/phase";
import type { SpeciesFormEvolution } from "#balance/pokemon-evolutions";
import { FusionSpeciesFormEvolution } from "#balance/pokemon-evolutions";
import { EVOLVE_MOVE } from "#balance/pokemon-level-moves";
import { getTypeRgb } from "#data/type";
import { LearnMoveSituation } from "#enums/learn-move-situation";
import { UiMode } from "#enums/ui-mode";
import { cos, sin } from "#field/anims";
import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type { EvolutionSceneHandler } from "#ui/evolution-scene-handler";
import { fixedInt, getFrameMs, randInt } from "#utils/common";
import i18next from "i18next";
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
export class EvolutionPhase extends Phase {
// FormChangePhase inherits from this, but EvolutionPhase is not abstract.
// We have to use the union here
public readonly phaseName: "EvolutionPhase" | "FormChangePhase" = "EvolutionPhase";
protected pokemon: PlayerPokemon;
protected lastLevel: number;
protected evoChain: Phaser.Tweens.TweenChain | null = null;
private preEvolvedPokemonName: string;
private evolution: SpeciesFormEvolution | null;
private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species
private evolutionBgm: AnySound;
private evolutionHandler: EvolutionSceneHandler;
protected evolutionContainer: Phaser.GameObjects.Container;
protected evolutionBaseBg: Phaser.GameObjects.Image;
protected evolutionBg: Phaser.GameObjects.Video;
protected evolutionBgOverlay: Phaser.GameObjects.Rectangle;
protected evolutionOverlay: Phaser.GameObjects.Rectangle;
protected pokemonSprite: Phaser.GameObjects.Sprite;
protected pokemonTintSprite: Phaser.GameObjects.Sprite;
protected pokemonEvoSprite: Phaser.GameObjects.Sprite;
protected pokemonEvoTintSprite: Phaser.GameObjects.Sprite;
/** Whether the evolution can be cancelled by the player */
protected canCancel: boolean;
/**
* @param pokemon - The Pokemon that is evolving
* @param evolution - The form being evolved into
* @param lastLevel - The level at which the Pokemon is evolving
* @param canCancel - Whether the evolution can be cancelled by the player
*/
constructor(pokemon: PlayerPokemon, evolution: SpeciesFormEvolution | null, lastLevel: number, canCancel = true) {
super();
this.pokemon = pokemon;
this.evolution = evolution;
this.lastLevel = lastLevel;
this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution;
this.canCancel = canCancel;
}
validate(): boolean {
return !!this.evolution;
}
setMode(): Promise<void> {
return globalScene.ui.setModeForceTransition(UiMode.EVOLUTION_SCENE);
}
/**
* Set up the following evolution assets
* - {@linkcode evolutionContainer}
* - {@linkcode evolutionBaseBg}
* - {@linkcode evolutionBg}
* - {@linkcode evolutionBgOverlay}
* - {@linkcode evolutionOverlay}
*
*/
private setupEvolutionAssets(): void {
this.evolutionHandler = globalScene.ui.getHandler() as EvolutionSceneHandler;
this.evolutionContainer = this.evolutionHandler.evolutionContainer;
this.evolutionBaseBg = globalScene.add.image(0, 0, "default_bg").setOrigin(0);
this.evolutionBg = globalScene.add
.video(0, 0, "evo_bg")
.stop()
.setOrigin(0)
.setScale(0.4359673025)
.setVisible(false);
this.evolutionBgOverlay = globalScene.add
.rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0x262626)
.setOrigin(0)
.setAlpha(0);
this.evolutionContainer.add([this.evolutionBaseBg, this.evolutionBgOverlay, this.evolutionBg]);
this.evolutionOverlay = globalScene.add.rectangle(
0,
-globalScene.game.canvas.height / 6,
globalScene.game.canvas.width / 6,
globalScene.game.canvas.height / 6 - 48,
0xffffff,
);
this.evolutionOverlay.setOrigin(0).setAlpha(0);
globalScene.ui.add(this.evolutionOverlay);
}
/**
* Configure the sprite, setting its pipeline data
* @param pokemon - The pokemon object that the sprite information is configured from
* @param sprite - The sprite object to configure
* @param setPipeline - Whether to also set the pipeline; should be false
* if the sprite is only being updated with new sprite assets
*
*
* @returns The sprite object that was passed in
*/
protected configureSprite(pokemon: Pokemon, sprite: Phaser.GameObjects.Sprite, setPipeline = true): typeof sprite {
const spriteKey = pokemon.getSpriteKey(true);
try {
sprite.play(spriteKey);
} catch (err: unknown) {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
if (setPipeline) {
sprite.setPipeline(globalScene.spritePipeline, {
tone: [0.0, 0.0, 0.0, 0.0],
hasShadow: false,
teraColor: getTypeRgb(pokemon.getTeraType()),
isTerastallized: pokemon.isTerastallized,
});
}
sprite
.setPipelineData("ignoreTimeTint", true)
.setPipelineData("spriteKey", spriteKey)
.setPipelineData("shiny", pokemon.shiny)
.setPipelineData("variant", pokemon.variant);
for (let k of ["spriteColors", "fusionSpriteColors"]) {
if (pokemon.summonData.speciesForm) {
k += "Base";
}
sprite.pipelineData[k] = pokemon.getSprite().pipelineData[k];
}
return sprite;
}
private getPokemonSprite(): Phaser.GameObjects.Sprite {
const sprite = globalScene.addPokemonSprite(
this.pokemon,
this.evolutionBaseBg.displayWidth / 2,
this.evolutionBaseBg.displayHeight / 2,
"pkmn__sub",
);
sprite.setPipeline(globalScene.spritePipeline, {
tone: [0.0, 0.0, 0.0, 0.0],
ignoreTimeTint: true,
});
return sprite;
}
/**
* Initialize {@linkcode pokemonSprite}, {@linkcode pokemonTintSprite}, {@linkcode pokemonEvoSprite}, and {@linkcode pokemonEvoTintSprite}
* and add them to the {@linkcode evolutionContainer}
*/
private setupPokemonSprites(): void {
this.pokemonSprite = this.configureSprite(this.pokemon, this.getPokemonSprite());
this.pokemonTintSprite = this.configureSprite(
this.pokemon,
this.getPokemonSprite().setAlpha(0).setTintFill(0xffffff),
);
this.pokemonEvoSprite = this.configureSprite(this.pokemon, this.getPokemonSprite().setVisible(false));
this.pokemonEvoTintSprite = this.configureSprite(
this.pokemon,
this.getPokemonSprite().setVisible(false).setTintFill(0xffffff),
);
this.evolutionContainer.add([
this.pokemonSprite,
this.pokemonTintSprite,
this.pokemonEvoSprite,
this.pokemonEvoTintSprite,
]);
}
async start() {
super.start();
await this.setMode();
if (!this.validate()) {
return this.end();
}
this.setupEvolutionAssets();
this.setupPokemonSprites();
this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
this.doEvolution();
}
/**
* Update the sprites depicting the evolved Pokemon
* @param evolvedPokemon - The evolved Pokemon
*/
private updateEvolvedPokemonSprites(evolvedPokemon: Pokemon): void {
this.configureSprite(evolvedPokemon, this.pokemonEvoSprite, false);
this.configureSprite(evolvedPokemon, this.pokemonEvoTintSprite, false);
}
/**
* Adds the evolution tween and begins playing it
*/
private playEvolutionAnimation(evolvedPokemon: Pokemon): void {
globalScene.time.delayedCall(1000, () => {
this.evolutionBgm = globalScene.playSoundWithoutBgm("evolution");
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 1,
delay: 500,
duration: 1500,
ease: "Sine.easeOut",
onComplete: () => {
globalScene.time.delayedCall(1000, () => {
this.evolutionBg.setVisible(true).play();
});
globalScene.playSound("se/charge");
this.doSpiralUpward();
this.fadeOutPokemonSprite(evolvedPokemon);
},
});
});
}
private fadeOutPokemonSprite(evolvedPokemon: Pokemon): void {
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue() ?? 1);
},
onComplete: () => {
this.pokemonSprite.setVisible(false);
globalScene.time.delayedCall(1100, () => {
globalScene.playSound("se/beam");
this.doArcDownward();
this.prepareForCycle(evolvedPokemon);
});
},
});
}
/**
* Prepares the evolution cycle by setting up the tint sprites and starting the cycle
*/
private prepareForCycle(evolvedPokemon: Pokemon): void {
globalScene.time.delayedCall(1500, () => {
this.pokemonEvoTintSprite.setScale(0.25).setVisible(true);
this.evolutionHandler.canCancel = this.canCancel;
this.doCycle(1, undefined, () => {
if (this.evolutionHandler.cancelled) {
this.handleFailedEvolution(evolvedPokemon);
} else {
this.handleSuccessEvolution(evolvedPokemon);
}
});
});
}
/**
* Show the evolution text and then commence the evolution animation
*/
doEvolution(): void {
globalScene.ui.showText(
i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }),
null,
() => {
this.pokemon.cry();
this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => {
this.updateEvolvedPokemonSprites(evolvedPokemon);
this.playEvolutionAnimation(evolvedPokemon);
});
},
1000,
);
}
/** Used exclusively by {@linkcode handleFailedEvolution} to fade out the evolution sprites and music */
private fadeOutEvolutionAssets(): void {
globalScene.tweens.add({
targets: [this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite],
alpha: 0,
duration: 250,
onComplete: () => {
this.evolutionBg.setVisible(false);
},
});
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
}
/**
* Show the confirmation prompt for pausing evolutions
* @param endCallback - The callback to call after either option is selected.
* This should end the evolution phase
*/
private showPauseEvolutionConfirmation(endCallback: () => void): void {
globalScene.ui.setOverlayMode(
UiMode.CONFIRM,
() => {
globalScene.ui.revertMode();
this.pokemon.pauseEvolutions = true;
globalScene.ui.showText(
i18next.t("menu:evolutionsPaused", {
pokemonName: this.preEvolvedPokemonName,
}),
null,
endCallback,
3000,
);
},
() => {
globalScene.ui.revertMode();
globalScene.time.delayedCall(3000, endCallback);
},
);
}
/**
* Used exclusively by {@linkcode handleFailedEvolution} to show the failed evolution UI messages
*/
private showFailedEvolutionUI(evolvedPokemon: Pokemon): void {
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
globalScene.ui.showText(
i18next.t("menu:stoppedEvolving", {
pokemonName: this.preEvolvedPokemonName,
}),
null,
() => {
globalScene.ui.showText(
i18next.t("menu:pauseEvolutionsQuestion", {
pokemonName: this.preEvolvedPokemonName,
}),
null,
() => {
const end = () => {
globalScene.ui.showText("", 0);
globalScene.playBgm();
evolvedPokemon.destroy();
this.end();
};
this.showPauseEvolutionConfirmation(end);
},
);
},
null,
true,
);
}
/**
* Fade out the evolution assets, show the failed evolution UI messages, and enqueue the EndEvolutionPhase
* @param evolvedPokemon - The evolved Pokemon
*/
private handleFailedEvolution(evolvedPokemon: Pokemon): void {
this.pokemonSprite.setVisible(true);
this.pokemonTintSprite.setScale(1);
this.fadeOutEvolutionAssets();
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
this.showFailedEvolutionUI(evolvedPokemon);
}
/**
* Fadeout evolution music, play the cry, show the evolution completed text, and end the phase
*/
private onEvolutionComplete(evolvedPokemon: Pokemon) {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
globalScene.time.delayedCall(250, () => {
this.pokemon.cry();
globalScene.time.delayedCall(1250, () => {
globalScene.playSoundWithoutBgm("evolution_fanfare");
evolvedPokemon.destroy();
globalScene.ui.showText(
i18next.t("menu:evolutionDone", {
pokemonName: this.preEvolvedPokemonName,
evolvedPokemonName: this.pokemon.name,
}),
null,
() => this.end(),
null,
true,
fixedInt(4000),
);
globalScene.time.delayedCall(fixedInt(4250), () => globalScene.playBgm());
});
});
}
private postEvolve(evolvedPokemon: Pokemon): void {
const learnSituation: LearnMoveSituation = this.fusionSpeciesEvolved
? LearnMoveSituation.EVOLUTION_FUSED
: this.pokemon.fusionSpecies
? LearnMoveSituation.EVOLUTION_FUSED_BASE
: LearnMoveSituation.EVOLUTION;
const levelMoves = this.pokemon
.getLevelMoves(this.lastLevel + 1, true, false, false, learnSituation)
.filter(lm => lm[0] === EVOLVE_MOVE);
for (const lm of levelMoves) {
globalScene.phaseManager.unshiftNew("LearnMovePhase", globalScene.getPlayerParty().indexOf(this.pokemon), lm[1]);
}
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
globalScene.playSound("se/shine");
this.doSpray();
globalScene.tweens.chain({
targets: null,
tweens: [
{
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
},
},
{
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
},
{
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: () => this.onEvolutionComplete(evolvedPokemon),
},
],
});
}
/**
* Handles a successful evolution
* @param evolvedPokemon - The evolved Pokemon
*/
private handleSuccessEvolution(evolvedPokemon: Pokemon): void {
globalScene.playSound("se/sparkle");
this.pokemonEvoSprite.setVisible(true);
this.doCircleInward();
globalScene.time.delayedCall(900, () => {
this.evolutionHandler.canCancel = this.canCancel;
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => this.postEvolve(evolvedPokemon));
});
}
doSpiralUpward() {
let f = 0;
globalScene.tweens.addCounter({
repeat: 64,
duration: getFrameMs(1),
onRepeat: () => {
if (f < 64) {
if (!(f & 7)) {
for (let i = 0; i < 4; i++) {
this.doSpiralUpwardParticle((f & 120) * 2 + i * 64);
}
}
f++;
}
},
});
}
doArcDownward() {
let f = 0;
globalScene.tweens.addCounter({
repeat: 96,
duration: getFrameMs(1),
onRepeat: () => {
if (f < 96) {
if (f < 6) {
for (let i = 0; i < 9; i++) {
this.doArcDownParticle(i * 16);
}
}
f++;
}
},
});
}
/**
* Return a tween chain that cycles the evolution sprites
*/
doCycle(cycles: number, lastCycle = 15, onComplete = () => {}): void {
// Make our tween start both at the same time
const tweens: Phaser.Types.Tweens.TweenBuilderConfig[] = [];
for (let i = cycles; i <= lastCycle; i += 0.5) {
tweens.push({
targets: [this.pokemonTintSprite, this.pokemonEvoTintSprite],
scale: (_target, _key, _value, targetIndex: number, _totalTargets, _tween) => (targetIndex === 0 ? 0.25 : 1),
ease: "Cubic.easeInOut",
duration: 500 / i,
yoyo: i !== lastCycle,
onComplete: () => {
if (this.evolutionHandler.cancelled) {
// cause the tween chain to complete instantly, skipping the remaining tweens.
this.pokemonEvoTintSprite.setScale(1);
this.pokemonEvoTintSprite.setVisible(false);
this.evoChain?.complete?.();
return;
}
if (i === lastCycle) {
this.pokemonEvoTintSprite.setScale(1);
}
},
});
}
this.evoChain = globalScene.tweens.chain({
targets: null,
tweens,
onComplete: () => {
this.evoChain = null;
onComplete();
},
});
}
doCircleInward() {
let f = 0;
globalScene.tweens.addCounter({
repeat: 48,
duration: getFrameMs(1),
onRepeat: () => {
if (!f) {
for (let i = 0; i < 16; i++) {
this.doCircleInwardParticle(i * 16, 4);
}
} else if (f === 32) {
for (let i = 0; i < 16; i++) {
this.doCircleInwardParticle(i * 16, 8);
}
}
f++;
},
});
}
doSpray() {
let f = 0;
globalScene.tweens.addCounter({
repeat: 48,
duration: getFrameMs(1),
onRepeat: () => {
if (!f) {
for (let i = 0; i < 8; i++) {
this.doSprayParticle(i);
}
} else if (f < 50) {
this.doSprayParticle(randInt(8));
}
f++;
},
});
}
doSpiralUpwardParticle(trigIndex: number) {
const initialX = this.evolutionBaseBg.displayWidth / 2;
const particle = globalScene.add.image(initialX, 0, "evo_sparkle");
this.evolutionContainer.add(particle);
let f = 0;
let amp = 48;
const particleTimer = globalScene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
},
});
const updateParticle = () => {
if (!f || particle.y > 8) {
particle.setPosition(initialX, 88 - (f * f) / 80);
particle.y += sin(trigIndex, amp) / 4;
particle.x += cos(trigIndex, amp);
particle.setScale(1 - f / 80);
trigIndex += 4;
if (f & 1) {
amp--;
}
f++;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}
doArcDownParticle(trigIndex: number) {
const initialX = this.evolutionBaseBg.displayWidth / 2;
const particle = globalScene.add.image(initialX, 0, "evo_sparkle");
particle.setScale(0.5);
this.evolutionContainer.add(particle);
let f = 0;
let amp = 8;
const particleTimer = globalScene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
},
});
const updateParticle = () => {
if (!f || particle.y < 88) {
particle.setPosition(initialX, 8 + (f * f) / 5);
particle.y += sin(trigIndex, amp) / 4;
particle.x += cos(trigIndex, amp);
amp = 8 + sin(f * 4, 40);
f++;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}
doCircleInwardParticle(trigIndex: number, speed: number) {
const initialX = this.evolutionBaseBg.displayWidth / 2;
const initialY = this.evolutionBaseBg.displayHeight / 2;
const particle = globalScene.add.image(initialX, initialY, "evo_sparkle");
this.evolutionContainer.add(particle);
let amp = 120;
const particleTimer = globalScene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
},
});
const updateParticle = () => {
if (amp > 8) {
particle.setPosition(initialX, initialY);
particle.y += sin(trigIndex, amp);
particle.x += cos(trigIndex, amp);
amp -= speed;
trigIndex += 4;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}
doSprayParticle(trigIndex: number) {
const initialX = this.evolutionBaseBg.displayWidth / 2;
const initialY = this.evolutionBaseBg.displayHeight / 2;
const particle = globalScene.add.image(initialX, initialY, "evo_sparkle");
this.evolutionContainer.add(particle);
let f = 0;
let yOffset = 0;
const speed = 3 - randInt(8);
const amp = 48 + randInt(64);
const particleTimer = globalScene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
},
});
const updateParticle = () => {
if (!(f & 3)) {
yOffset++;
}
if (trigIndex < 128) {
particle.setPosition(initialX + (speed * f) / 3, initialY + yOffset);
particle.y += -sin(trigIndex, amp);
if (f > 108) {
particle.setScale(1 - (f - 108) / 20);
}
trigIndex++;
f++;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}
}