This commit is contained in:
Sirz Benjie 2025-06-19 16:38:10 -06:00 committed by GitHub
commit 52a5e4826e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 571 additions and 480 deletions

View File

@ -23,6 +23,8 @@ export class EvolutionPhase extends Phase {
protected pokemon: PlayerPokemon; protected pokemon: PlayerPokemon;
protected lastLevel: number; protected lastLevel: number;
protected evoChain: Phaser.Tweens.TweenChain | null = null;
private preEvolvedPokemonName: string; private preEvolvedPokemonName: string;
private evolution: SpeciesFormEvolution | null; private evolution: SpeciesFormEvolution | null;
@ -40,13 +42,23 @@ export class EvolutionPhase extends Phase {
protected pokemonEvoSprite: Phaser.GameObjects.Sprite; protected pokemonEvoSprite: Phaser.GameObjects.Sprite;
protected pokemonEvoTintSprite: Phaser.GameObjects.Sprite; protected pokemonEvoTintSprite: Phaser.GameObjects.Sprite;
constructor(pokemon: PlayerPokemon, evolution: SpeciesFormEvolution | null, lastLevel: number) { /** 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(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
this.evolution = evolution; this.evolution = evolution;
this.lastLevel = lastLevel; this.lastLevel = lastLevel;
this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution; this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution;
this.canCancel = canCancel;
} }
validate(): boolean { validate(): boolean {
@ -57,198 +69,227 @@ export class EvolutionPhase extends Phase {
return globalScene.ui.setModeForceTransition(UiMode.EVOLUTION_SCENE); return globalScene.ui.setModeForceTransition(UiMode.EVOLUTION_SCENE);
} }
start() { /**
super.start(); * 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.setMode().then(() => { this.evolutionBg = globalScene.add
if (!this.validate()) { .video(0, 0, "evo_bg")
return this.end(); .stop()
} .setOrigin(0)
.setScale(0.4359673025)
.setVisible(false);
globalScene.fadeOutBgm(undefined, 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.evolutionHandler = globalScene.ui.getHandler() as EvolutionSceneHandler; 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);
}
this.evolutionContainer = this.evolutionHandler.evolutionContainer; /**
* 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);
}
this.evolutionBaseBg = globalScene.add.image(0, 0, "default_bg"); if (setPipeline) {
this.evolutionBaseBg.setOrigin(0, 0); sprite.setPipeline(globalScene.spritePipeline, {
this.evolutionContainer.add(this.evolutionBaseBg); tone: [0.0, 0.0, 0.0, 0.0],
hasShadow: false,
this.evolutionBg = globalScene.add.video(0, 0, "evo_bg").stop(); teraColor: getTypeRgb(pokemon.getTeraType()),
this.evolutionBg.setOrigin(0, 0); isTerastallized: pokemon.isTerastallized,
this.evolutionBg.setScale(0.4359673025); });
this.evolutionBg.setVisible(false); }
this.evolutionContainer.add(this.evolutionBg);
sprite
this.evolutionBgOverlay = globalScene.add.rectangle( .setPipelineData("ignoreTimeTint", true)
0, .setPipelineData("spriteKey", pokemon.getSpriteKey())
0, .setPipelineData("shiny", pokemon.shiny)
globalScene.game.canvas.width / 6, .setPipelineData("variant", pokemon.variant);
globalScene.game.canvas.height / 6,
0x262626, for (let k of ["spriteColors", "fusionSpriteColors"]) {
); if (pokemon.summonData.speciesForm) {
this.evolutionBgOverlay.setOrigin(0, 0); k += "Base";
this.evolutionBgOverlay.setAlpha(0); }
this.evolutionContainer.add(this.evolutionBgOverlay); sprite.pipelineData[k] = pokemon.getSprite().pipelineData[k];
}
const getPokemonSprite = () => {
const ret = globalScene.addPokemonSprite( return sprite;
this.pokemon, }
this.evolutionBaseBg.displayWidth / 2,
this.evolutionBaseBg.displayHeight / 2, private getPokemonSprite(): Phaser.GameObjects.Sprite {
"pkmn__sub", const sprite = globalScene.addPokemonSprite(
); this.pokemon,
ret.setPipeline(globalScene.spritePipeline, { this.evolutionBaseBg.displayWidth / 2,
tone: [0.0, 0.0, 0.0, 0.0], this.evolutionBaseBg.displayHeight / 2,
ignoreTimeTint: true, "pkmn__sub",
}); );
return ret; sprite.setPipeline(globalScene.spritePipeline, {
}; tone: [0.0, 0.0, 0.0, 0.0],
ignoreTimeTint: true,
this.evolutionContainer.add((this.pokemonSprite = getPokemonSprite())); });
this.evolutionContainer.add((this.pokemonTintSprite = getPokemonSprite())); return sprite;
this.evolutionContainer.add((this.pokemonEvoSprite = getPokemonSprite())); }
this.evolutionContainer.add((this.pokemonEvoTintSprite = getPokemonSprite()));
/**
this.pokemonTintSprite.setAlpha(0); * Initialize {@linkcode pokemonSprite}, {@linkcode pokemonTintSprite}, {@linkcode pokemonEvoSprite}, and {@linkcode pokemonEvoTintSprite}
this.pokemonTintSprite.setTintFill(0xffffff); * and add them to the {@linkcode evolutionContainer}
this.pokemonEvoSprite.setVisible(false); */
this.pokemonEvoTintSprite.setVisible(false); private setupPokemonSprites(): void {
this.pokemonEvoTintSprite.setTintFill(0xffffff); this.pokemonSprite = this.configureSprite(this.pokemon, this.getPokemonSprite());
this.pokemonTintSprite = this.configureSprite(
this.evolutionOverlay = globalScene.add.rectangle( this.pokemon,
0, this.getPokemonSprite().setAlpha(0).setTintFill(0xffffff),
-globalScene.game.canvas.height / 6, );
globalScene.game.canvas.width / 6, this.pokemonEvoSprite = this.configureSprite(this.pokemon, this.getPokemonSprite().setVisible(false));
globalScene.game.canvas.height / 6 - 48, this.pokemonEvoTintSprite = this.configureSprite(
0xffffff, this.pokemon,
); this.getPokemonSprite().setVisible(false).setTintFill(0xffffff),
this.evolutionOverlay.setOrigin(0, 0); );
this.evolutionOverlay.setAlpha(0);
globalScene.ui.add(this.evolutionOverlay); this.evolutionContainer.add([
this.pokemonSprite,
[this.pokemonSprite, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => { this.pokemonTintSprite,
const spriteKey = this.pokemon.getSpriteKey(true); this.pokemonEvoSprite,
try { this.pokemonEvoTintSprite,
sprite.play(spriteKey); ]);
} catch (err: unknown) { }
console.error(`Failed to play animation for ${spriteKey}`, err);
} async start() {
super.start();
sprite.setPipeline(globalScene.spritePipeline, { await this.setMode();
tone: [0.0, 0.0, 0.0, 0.0],
hasShadow: false, if (!this.validate()) {
teraColor: getTypeRgb(this.pokemon.getTeraType()), return this.end();
isTerastallized: this.pokemon.isTerastallized, }
}); this.setupEvolutionAssets();
sprite.setPipelineData("ignoreTimeTint", true); this.setupPokemonSprites();
sprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
sprite.setPipelineData("shiny", this.pokemon.shiny); this.doEvolution();
sprite.setPipelineData("variant", this.pokemon.variant); }
["spriteColors", "fusionSpriteColors"].map(k => {
if (this.pokemon.summonData.speciesForm) { /**
k += "Base"; * Update the sprites depicting the evolved Pokemon
} * @param evolvedPokemon - The evolved Pokemon
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]; */
}); 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);
},
}); });
this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
this.doEvolution();
}); });
} }
private fadeOutPokemonSprite(evolvedPokemon: Pokemon): void {
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
},
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 { doEvolution(): void {
globalScene.ui.showText( globalScene.ui.showText(
i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }), i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }),
null, null,
() => { () => {
this.pokemon.cry(); this.pokemon.cry();
this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => { this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => {
[this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => { this.updateEvolvedPokemonSprites(evolvedPokemon);
const spriteKey = evolvedPokemon.getSpriteKey(true); this.playEvolutionAnimation(evolvedPokemon);
try {
sprite.play(spriteKey);
} catch (err: unknown) {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", evolvedPokemon.getSpriteKey());
sprite.setPipelineData("shiny", evolvedPokemon.shiny);
sprite.setPipelineData("variant", evolvedPokemon.variant);
["spriteColors", "fusionSpriteColors"].map(k => {
if (evolvedPokemon.summonData.speciesForm) {
k += "Base";
}
sprite.pipelineData[k] = evolvedPokemon.getSprite().pipelineData[k];
});
});
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, () => {
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
});
this.evolutionBg.setVisible(true);
this.evolutionBg.play();
});
globalScene.playSound("se/charge");
this.doSpiralUpward();
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
},
onComplete: () => {
this.pokemonSprite.setVisible(false);
globalScene.time.delayedCall(1100, () => {
globalScene.playSound("se/beam");
this.doArcDownward();
globalScene.time.delayedCall(1500, () => {
this.pokemonEvoTintSprite.setScale(0.25);
this.pokemonEvoTintSprite.setVisible(true);
this.evolutionHandler.canCancel = true;
this.doCycle(1).then(success => {
if (success) {
this.handleSuccessEvolution(evolvedPokemon);
} else {
this.handleFailedEvolution(evolvedPokemon);
}
});
});
});
},
});
},
});
});
}); });
}, },
1000, 1000,
); );
} }
/** /** Used exclusively by {@linkcode handleFailedEvolution} to fade out the evolution sprites and music */
* Handles a failed/stopped evolution private fadeOutEvolutionAssets(): void {
* @param evolvedPokemon - The evolved Pokemon
*/
private handleFailedEvolution(evolvedPokemon: Pokemon): void {
this.pokemonSprite.setVisible(true);
this.pokemonTintSprite.setScale(1);
globalScene.tweens.add({ globalScene.tweens.add({
targets: [this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite], targets: [this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite],
alpha: 0, alpha: 0,
@ -257,9 +298,40 @@ export class EvolutionPhase extends Phase {
this.evolutionBg.setVisible(false); this.evolutionBg.setVisible(false);
}, },
}); });
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100); 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.phaseManager.unshiftNew("EndEvolutionPhase");
globalScene.ui.showText( globalScene.ui.showText(
@ -280,25 +352,7 @@ export class EvolutionPhase extends Phase {
evolvedPokemon.destroy(); evolvedPokemon.destroy();
this.end(); this.end();
}; };
globalScene.ui.setOverlayMode( this.showPauseEvolutionConfirmation(end);
UiMode.CONFIRM,
() => {
globalScene.ui.revertMode();
this.pokemon.pauseEvolutions = true;
globalScene.ui.showText(
i18next.t("menu:evolutionsPaused", {
pokemonName: this.preEvolvedPokemonName,
}),
null,
end,
3000,
);
},
() => {
globalScene.ui.revertMode();
globalScene.time.delayedCall(3000, end);
},
);
}, },
); );
}, },
@ -307,6 +361,93 @@ export class EvolutionPhase extends Phase {
); );
} }
/**
* 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.species.getExpandedSpeciesName(),
}),
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 * Handles a successful evolution
* @param evolvedPokemon - The evolved Pokemon * @param evolvedPokemon - The evolved Pokemon
@ -316,85 +457,15 @@ export class EvolutionPhase extends Phase {
this.pokemonEvoSprite.setVisible(true); this.pokemonEvoSprite.setVisible(true);
this.doCircleInward(); this.doCircleInward();
const onEvolutionComplete = () => {
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.species.getExpandedSpeciesName(),
}),
null,
() => this.end(),
null,
true,
fixedInt(4000),
);
globalScene.time.delayedCall(fixedInt(4250), () => globalScene.playBgm());
});
});
};
globalScene.time.delayedCall(900, () => { globalScene.time.delayedCall(900, () => {
this.evolutionHandler.canCancel = false; this.evolutionHandler.canCancel = this.canCancel;
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => this.postEvolve(evolvedPokemon));
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.add({
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
globalScene.tweens.add({
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
onComplete: () => {
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: onEvolutionComplete,
});
},
});
},
});
});
}); });
} }
doSpiralUpward() { doSpiralUpward() {
let f = 0; let f = 0;
globalScene.tweens.addCounter({ globalScene.tweens.addCounter({
repeat: 64, repeat: 64,
duration: getFrameMs(1), duration: getFrameMs(1),
@ -430,34 +501,41 @@ export class EvolutionPhase extends Phase {
}); });
} }
doCycle(l: number, lastCycle = 15): Promise<boolean> { /**
return new Promise(resolve => { * Return a tween chain that cycles the evolution sprites
const isLastCycle = l === lastCycle; */
globalScene.tweens.add({ doCycle(cycles: number, lastCycle = 15, onComplete = () => {}): void {
targets: this.pokemonTintSprite, // Make our tween start both at the same time
scale: 0.25, 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", ease: "Cubic.easeInOut",
duration: 500 / l, duration: 500 / i,
yoyo: !isLastCycle, yoyo: i !== lastCycle,
});
globalScene.tweens.add({
targets: this.pokemonEvoTintSprite,
scale: 1,
ease: "Cubic.easeInOut",
duration: 500 / l,
yoyo: !isLastCycle,
onComplete: () => { onComplete: () => {
if (this.evolutionHandler.cancelled) { if (this.evolutionHandler.cancelled) {
return resolve(false); // cause the tween chain to complete instantly, skipping the remaining tweens.
this.pokemonEvoTintSprite.setScale(1);
this.pokemonEvoTintSprite.setVisible(false);
this.evoChain?.complete?.();
return;
} }
if (l < lastCycle) { if (i === lastCycle) {
this.doCycle(l + 0.5, lastCycle).then(success => resolve(success)); this.pokemonEvoTintSprite.setScale(1);
} else {
this.pokemonTintSprite.setVisible(false);
resolve(true);
} }
}, },
}); });
}
this.evoChain = globalScene.tweens.chain({
targets: null,
tweens,
onComplete: () => {
this.evoChain = null;
onComplete();
},
}); });
} }

View File

@ -3,7 +3,7 @@ import { fixedInt } from "#app/utils/common";
import { achvs } from "../system/achv"; import { achvs } from "../system/achv";
import type { SpeciesFormChange } from "../data/pokemon-forms"; import type { SpeciesFormChange } from "../data/pokemon-forms";
import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms/form-change-triggers"; import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms/form-change-triggers";
import type { PlayerPokemon } from "../field/pokemon"; import type { default as Pokemon, PlayerPokemon } from "../field/pokemon";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import type PartyUiHandler from "../ui/party-ui-handler"; import type PartyUiHandler from "../ui/party-ui-handler";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
@ -34,146 +34,158 @@ export class FormChangePhase extends EvolutionPhase {
return globalScene.ui.setOverlayMode(UiMode.EVOLUTION_SCENE); return globalScene.ui.setOverlayMode(UiMode.EVOLUTION_SCENE);
} }
doEvolution(): void { /**
const preName = getPokemonNameWithAffix(this.pokemon); * Commence the tweens that play after the form change animation finishes
* @param transformedPokemon - The Pokemon after the evolution
this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => { * @param preName - The name of the Pokemon before the evolution
[this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => { */
const spriteKey = transformedPokemon.getSpriteKey(true); private postFormChangeTweens(transformedPokemon: Pokemon, preName: string): void {
try { globalScene.tweens.chain({
sprite.play(spriteKey); targets: null,
} catch (err: unknown) { tweens: [
console.error(`Failed to play animation for ${spriteKey}`, err); {
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,
completeDelay: 250,
onComplete: () => this.pokemon.cry(),
},
],
// 1.25 seconds after the pokemon cry
completeDelay: 1250,
onComplete: () => {
let playEvolutionFanfare = false;
if (this.formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1) {
globalScene.validateAchv(achvs.MEGA_EVOLVE);
playEvolutionFanfare = true;
} else if (
this.formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 ||
this.formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1
) {
globalScene.validateAchv(achvs.GIGANTAMAX);
playEvolutionFanfare = true;
} }
sprite.setPipelineData("ignoreTimeTint", true); const delay = playEvolutionFanfare ? 4000 : 1750;
sprite.setPipelineData("spriteKey", transformedPokemon.getSpriteKey()); globalScene.playSoundWithoutBgm(playEvolutionFanfare ? "evolution_fanfare" : "minor_fanfare");
sprite.setPipelineData("shiny", transformedPokemon.shiny); transformedPokemon.destroy();
sprite.setPipelineData("variant", transformedPokemon.variant); globalScene.ui.showText(
["spriteColors", "fusionSpriteColors"].map(k => { getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName),
if (transformedPokemon.summonData.speciesForm) { null,
k += "Base"; () => this.end(),
} null,
sprite.pipelineData[k] = transformedPokemon.getSprite().pipelineData[k]; true,
}); fixedInt(delay),
}); );
globalScene.time.delayedCall(fixedInt(delay + 250), () => globalScene.playBgm());
},
});
}
globalScene.time.delayedCall(250, () => { /**
globalScene.tweens.add({ * Commence the animations that occur once the form change evolution cycle ({@linkcode doCycle}) is complete
*
* @privateRemarks
* This would prefer {@linkcode doCycle} to be refactored and de-promisified so this can be moved into {@linkcode beginTweens}
* @param preName - The name of the Pokemon before the evolution
* @param transformedPokemon - The Pokemon being transformed into
*/
private afterCycle(preName: string, transformedPokemon: Pokemon): void {
globalScene.playSound("se/sparkle");
this.pokemonEvoSprite.setVisible(true);
this.doCircleInward();
globalScene.time.delayedCall(900, () => {
this.pokemon.changeForm(this.formChange).then(() => {
if (!this.modal) {
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
}
globalScene.playSound("se/shine");
this.doSpray();
this.postFormChangeTweens(transformedPokemon, preName);
});
});
}
/**
* Commence the sequence of tweens and events that occur during the evolution animation
* @param preName The name of the Pokemon before the evolution
* @param transformedPokemon The Pokemon after the evolution
*/
private beginTweens(preName: string, transformedPokemon: Pokemon): void {
globalScene.tweens.chain({
// Starts 250ms after sprites have been configured
targets: null,
tweens: [
// Step 1: Fade in the background overlay
{
delay: 250,
targets: this.evolutionBgOverlay, targets: this.evolutionBgOverlay,
alpha: 1, alpha: 1,
delay: 500,
duration: 1500, duration: 1500,
ease: "Sine.easeOut", ease: "Sine.easeOut",
// We want the backkground overlay to fade out after it fades in
onComplete: () => { onComplete: () => {
globalScene.time.delayedCall(1000, () => { globalScene.tweens.add({
globalScene.tweens.add({ targets: this.evolutionBgOverlay,
targets: this.evolutionBgOverlay, alpha: 0,
alpha: 0, duration: 250,
duration: 250, delay: 1000,
});
this.evolutionBg.setVisible(true);
this.evolutionBg.play();
}); });
this.evolutionBg.setVisible(true).play();
},
},
// Step 2: Play the sounds and fade in the tint sprite
{
targets: this.pokemonTintSprite,
alpha: { from: 0, to: 1 },
duration: 2000,
onStart: () => {
globalScene.playSound("se/charge"); globalScene.playSound("se/charge");
this.doSpiralUpward(); this.doSpiralUpward();
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
},
onComplete: () => {
this.pokemonSprite.setVisible(false);
globalScene.time.delayedCall(1100, () => {
globalScene.playSound("se/beam");
this.doArcDownward();
globalScene.time.delayedCall(1000, () => {
this.pokemonEvoTintSprite.setScale(0.25);
this.pokemonEvoTintSprite.setVisible(true);
this.doCycle(1, 1).then(_success => {
globalScene.playSound("se/sparkle");
this.pokemonEvoSprite.setVisible(true);
this.doCircleInward();
globalScene.time.delayedCall(900, () => {
this.pokemon.changeForm(this.formChange).then(() => {
if (!this.modal) {
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
}
globalScene.playSound("se/shine");
this.doSpray();
globalScene.tweens.add({
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
globalScene.tweens.add({
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
onComplete: () => {
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: () => {
globalScene.time.delayedCall(250, () => {
this.pokemon.cry();
globalScene.time.delayedCall(1250, () => {
let playEvolutionFanfare = false;
if (this.formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1) {
globalScene.validateAchv(achvs.MEGA_EVOLVE);
playEvolutionFanfare = true;
} else if (
this.formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 ||
this.formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1
) {
globalScene.validateAchv(achvs.GIGANTAMAX);
playEvolutionFanfare = true;
}
const delay = playEvolutionFanfare ? 4000 : 1750;
globalScene.playSoundWithoutBgm(
playEvolutionFanfare ? "evolution_fanfare" : "minor_fanfare",
);
transformedPokemon.destroy();
globalScene.ui.showText(
getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName),
null,
() => this.end(),
null,
true,
fixedInt(delay),
);
globalScene.time.delayedCall(fixedInt(delay + 250), () =>
globalScene.playBgm(),
);
});
});
},
});
},
});
},
});
});
});
});
});
});
},
});
}, },
onComplete: () => {
this.pokemonSprite.setVisible(false);
},
},
],
// Step 3: Commence the form change animation via doCycle then continue the animation chain with afterCycle
completeDelay: 1100,
onComplete: () => {
globalScene.playSound("se/beam");
this.doArcDownward();
globalScene.time.delayedCall(1000, () => {
this.pokemonEvoTintSprite.setScale(0.25).setVisible(true);
this.doCycle(1, 1, () => this.afterCycle(preName, transformedPokemon));
}); });
}); },
});
}
doEvolution(): void {
const preName = getPokemonNameWithAffix(this.pokemon, false);
this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => {
this.configureSprite(transformedPokemon, this.pokemonEvoSprite, false);
this.configureSprite(transformedPokemon, this.pokemonEvoTintSprite, false);
this.beginTweens(preName, transformedPokemon);
}); });
} }

View File

@ -5,9 +5,13 @@ import type BattleScene from "#app/battle-scene";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { FixedInt } from "#app/utils/common"; import { FixedInt } from "#app/utils/common";
type TweenManager = typeof Phaser.Tweens.TweenManager.prototype;
/** The set of properties to mutate */
const PROPERTIES = ["delay", "completeDelay", "loopDelay", "duration", "repeatDelay", "hold", "startDelay"];
type FadeInType = typeof FadeIn; type FadeInType = typeof FadeIn;
type FadeOutType = typeof FadeOut; type FadeOutType = typeof FadeOut;
export function initGameSpeed() { export function initGameSpeed() {
const thisArg = this as BattleScene; const thisArg = this as BattleScene;
@ -18,14 +22,44 @@ export function initGameSpeed() {
return thisArg.gameSpeed === 1 ? value : Math.ceil((value /= thisArg.gameSpeed)); return thisArg.gameSpeed === 1 ? value : Math.ceil((value /= thisArg.gameSpeed));
}; };
const originalAddEvent = this.time.addEvent; // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complexity is necessary here
const mutateProperties = (obj: any, allowArray = false) => {
// We do not mutate Tweens or TweenChain objects themselves.
if (obj instanceof Phaser.Tweens.Tween || obj instanceof Phaser.Tweens.TweenChain) {
return;
}
// If allowArray is true then check if first obj is an array and if so, mutate the tweens inside
if (allowArray && Array.isArray(obj)) {
for (const tween of obj) {
mutateProperties(tween);
}
return;
}
for (const prop of PROPERTIES) {
const objProp = obj[prop];
if (typeof objProp === "number" || objProp instanceof FixedInt) {
obj[prop] = transformValue(objProp);
}
}
// If the object has a 'tweens' property that is an array, then it is a tween chain
// and we need to mutate its properties as well
if (obj.tweens && Array.isArray(obj.tweens)) {
for (const tween of obj.tweens) {
mutateProperties(tween);
}
}
};
const originalAddEvent: typeof Phaser.Time.Clock.prototype.addEvent = this.time.addEvent;
this.time.addEvent = function (config: Phaser.Time.TimerEvent | Phaser.Types.Time.TimerEventConfig) { this.time.addEvent = function (config: Phaser.Time.TimerEvent | Phaser.Types.Time.TimerEventConfig) {
if (!(config instanceof Phaser.Time.TimerEvent) && config.delay) { if (!(config instanceof Phaser.Time.TimerEvent) && config.delay) {
config.delay = transformValue(config.delay); config.delay = transformValue(config.delay);
} }
return originalAddEvent.apply(this, [config]); return originalAddEvent.apply(this, [config]);
}; };
const originalTweensAdd = this.tweens.add; const originalTweensAdd: TweenManager["add"] = this.tweens.add;
this.tweens.add = function ( this.tweens.add = function (
config: config:
| Phaser.Types.Tweens.TweenBuilderConfig | Phaser.Types.Tweens.TweenBuilderConfig
@ -33,71 +67,33 @@ export function initGameSpeed() {
| Phaser.Tweens.Tween | Phaser.Tweens.Tween
| Phaser.Tweens.TweenChain, | Phaser.Tweens.TweenChain,
) { ) {
if (config.loopDelay) { mutateProperties(config);
config.loopDelay = transformValue(config.loopDelay as number);
}
if (!(config instanceof Phaser.Tweens.TweenChain)) {
if (config.duration) {
config.duration = transformValue(config.duration);
}
if (!(config instanceof Phaser.Tweens.Tween)) {
if (config.delay) {
config.delay = transformValue(config.delay as number);
}
if (config.repeatDelay) {
config.repeatDelay = transformValue(config.repeatDelay);
}
if (config.hold) {
config.hold = transformValue(config.hold);
}
}
}
return originalTweensAdd.apply(this, [config]); return originalTweensAdd.apply(this, [config]);
}; } as typeof originalTweensAdd;
const originalTweensChain = this.tweens.chain;
const originalTweensChain: TweenManager["chain"] = this.tweens.chain;
this.tweens.chain = function (config: Phaser.Types.Tweens.TweenChainBuilderConfig): Phaser.Tweens.TweenChain { this.tweens.chain = function (config: Phaser.Types.Tweens.TweenChainBuilderConfig): Phaser.Tweens.TweenChain {
if (config.tweens) { mutateProperties(config);
for (const t of config.tweens) {
if (t.duration) {
t.duration = transformValue(t.duration);
}
if (t.delay) {
t.delay = transformValue(t.delay as number);
}
if (t.repeatDelay) {
t.repeatDelay = transformValue(t.repeatDelay);
}
if (t.loopDelay) {
t.loopDelay = transformValue(t.loopDelay as number);
}
if (t.hold) {
t.hold = transformValue(t.hold);
}
}
}
return originalTweensChain.apply(this, [config]); return originalTweensChain.apply(this, [config]);
}; } as typeof originalTweensChain;
const originalAddCounter = this.tweens.addCounter; const originalAddCounter: TweenManager["addCounter"] = this.tweens.addCounter;
this.tweens.addCounter = function (config: Phaser.Types.Tweens.NumberTweenBuilderConfig) { this.tweens.addCounter = function (config: Phaser.Types.Tweens.NumberTweenBuilderConfig) {
if (config.duration) { mutateProperties(config);
config.duration = transformValue(config.duration);
}
if (config.delay) {
config.delay = transformValue(config.delay);
}
if (config.repeatDelay) {
config.repeatDelay = transformValue(config.repeatDelay);
}
if (config.loopDelay) {
config.loopDelay = transformValue(config.loopDelay as number);
}
if (config.hold) {
config.hold = transformValue(config.hold);
}
return originalAddCounter.apply(this, [config]); return originalAddCounter.apply(this, [config]);
}; } as typeof originalAddCounter;
const originalCreate: TweenManager["create"] = this.tweens.create;
this.tweens.create = function (config: Phaser.Types.Tweens.TweenBuilderConfig) {
mutateProperties(config, true);
return originalCreate.apply(this, [config]);
} as typeof originalCreate;
const originalAddMultiple: TweenManager["addMultiple"] = this.tweens.addMultiple;
this.tweens.addMultiple = function (config: Phaser.Types.Tweens.TweenBuilderConfig[]) {
mutateProperties(config, true);
return originalAddMultiple.apply(this, [config]);
} as typeof originalAddMultiple;
const originalFadeOut = SoundFade.fadeOut; const originalFadeOut = SoundFade.fadeOut;
SoundFade.fadeOut = ((_scene: Phaser.Scene, sound: Phaser.Sound.BaseSound, duration: number, destroy?: boolean) => SoundFade.fadeOut = ((_scene: Phaser.Scene, sound: Phaser.Sound.BaseSound, duration: number, destroy?: boolean) =>

View File

@ -122,15 +122,20 @@ export default class GameWrapper {
}, },
}; };
// TODO: Replace this with a proper mock of phaser's TweenManager.
this.scene.tweens = { this.scene.tweens = {
add: data => { add: data => {
if (data.onComplete) { // TODO: our mock of `add` should have the same signature as the real one, which returns the tween
data.onComplete(); data.onComplete?.();
}
}, },
getTweensOf: () => [], getTweensOf: () => [],
killTweensOf: () => [], killTweensOf: () => [],
chain: () => null,
chain: data => {
// TODO: our mock of `chain` should have the same signature as the real one, which returns the chain
data?.tweens?.forEach(tween => tween.onComplete?.());
data.onComplete?.();
},
addCounter: data => { addCounter: data => {
if (data.onComplete) { if (data.onComplete) {
data.onComplete(); data.onComplete();

View File

@ -5,7 +5,7 @@ export class MockVideoGameObject implements MockGameObject {
public name: string; public name: string;
public active = true; public active = true;
public play = () => null; public play = () => this;
public stop = () => this; public stop = () => this;
public setOrigin = () => this; public setOrigin = () => this;
public setScale = () => this; public setScale = () => this;