mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-10-14 09:07:14 +02:00
* Rename `Abilities` to `AbilityId` * Rename `abilities.ts` to `ability-id.ts` * Rename `Moves` to `MoveId` * Rename `moves.ts` to `move-id.ts` * Rename `Species` to `SpeciesId` * Rename `species.ts` to `species-id.ts` * Rename `Biome` to `BiomeId` * Rename `biome.ts` to `biome-id.ts` * Replace `Abilities` with `AbilityId` in comments * Replace `Biome` with `BiomeId` in comments * Replace `Moves` with `MoveId` in comments * Replace `Species` with `SpeciesId` in comments
529 lines
16 KiB
TypeScript
529 lines
16 KiB
TypeScript
import type { GameObjects } from "phaser";
|
|
import { globalScene } from "#app/global-scene";
|
|
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
|
|
import type { SpeciesId } from "#enums/species-id";
|
|
import { isNullOrUndefined } from "#app/utils/common";
|
|
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
|
import type { Variant } from "#app/sprites/variant";
|
|
import { doShinySparkleAnim } from "#app/field/anims";
|
|
import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite";
|
|
import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig;
|
|
|
|
type KnownFileRoot =
|
|
| "arenas"
|
|
| "battle_anims"
|
|
| "cg"
|
|
| "character"
|
|
| "effect"
|
|
| "egg"
|
|
| "events"
|
|
| "inputs"
|
|
| "items"
|
|
| "mystery-encounters"
|
|
| "pokeball"
|
|
| "pokemon"
|
|
| "pokemon/back"
|
|
| "pokemon/exp"
|
|
| "pokemon/female"
|
|
| "pokemon/icons"
|
|
| "pokemon/input"
|
|
| "pokemon/shiny"
|
|
| "pokemon/variant"
|
|
| "statuses"
|
|
| "trainer"
|
|
| "ui";
|
|
|
|
export class MysteryEncounterSpriteConfig {
|
|
/** The sprite key (which is the image file name). e.g. "ace_trainer_f" */
|
|
spriteKey: string;
|
|
/** Refer to [/public/images](../../public/images) directorty for all folder names */
|
|
fileRoot: (KnownFileRoot & string) | string;
|
|
/** Optional replacement for `spriteKey`/`fileRoot`. Just know this defaults to male/genderless, form 0, no shiny */
|
|
species?: SpeciesId;
|
|
/** Enable shadow. Defaults to `false` */
|
|
hasShadow?: boolean = false;
|
|
/** Disable animation. Defaults to `false` */
|
|
disableAnimation?: boolean = false;
|
|
/** Repeat the animation. Defaults to `false` */
|
|
repeat?: boolean = false;
|
|
/** What frame of the animation to start on. Defaults to 0 */
|
|
startFrame?: number = 0;
|
|
/** Hidden at start of encounter. Defaults to `false` */
|
|
hidden?: boolean = false;
|
|
/** Tint color. `0` - `1`. Higher means darker tint. */
|
|
tint?: number;
|
|
/** X offset */
|
|
x?: number;
|
|
/** Y offset */
|
|
y?: number;
|
|
/** Y shadow offset */
|
|
yShadow?: number;
|
|
/** Sprite scale. `0` - `n` */
|
|
scale?: number;
|
|
/** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */
|
|
isPokemon?: boolean;
|
|
/** If using a Pokemon shiny sprite, needs to be set to ensure the correct variant assets get loaded and displayed */
|
|
isShiny?: boolean;
|
|
/** If using a Pokemon shiny sprite, needs to be set to ensure the correct variant assets get loaded and displayed */
|
|
variant?: Variant;
|
|
/** If you are using an item sprite, set to `true` */
|
|
isItem?: boolean;
|
|
/** The sprites alpha. `0` - `1` The lower the number, the more transparent */
|
|
alpha?: number;
|
|
}
|
|
|
|
/**
|
|
* When a mystery encounter spawns, there are visuals (mainly sprites) tied to the field for the new encounter to inform the player of the type of encounter
|
|
* These slide in with the field as part of standard field change cycle, and will typically be hidden after the player has selected an option for the encounter
|
|
* Note: intro visuals are not "Trainers" or any other specific game object, though they may contain trainer sprites
|
|
*/
|
|
export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
|
|
public encounter: MysteryEncounter;
|
|
public spriteConfigs: MysteryEncounterSpriteConfig[];
|
|
public enterFromRight: boolean;
|
|
private shinySparkleSprites: {
|
|
sprite: Phaser.GameObjects.Sprite;
|
|
variant: Variant;
|
|
}[];
|
|
|
|
constructor(encounter: MysteryEncounter) {
|
|
super(globalScene, -72, 76);
|
|
this.encounter = encounter;
|
|
this.enterFromRight = encounter.enterIntroVisualsFromRight ?? false;
|
|
// Shallow copy configs to allow visual config updates at runtime without dirtying master copy of Encounter
|
|
this.spriteConfigs = encounter.spriteConfigs.map(config => {
|
|
const result = {
|
|
...config,
|
|
};
|
|
|
|
if (!isNullOrUndefined(result.species)) {
|
|
const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant);
|
|
result.spriteKey = keys.spriteKey;
|
|
result.fileRoot = keys.fileRoot;
|
|
result.isPokemon = true;
|
|
}
|
|
|
|
return result;
|
|
});
|
|
if (!this.spriteConfigs) {
|
|
return;
|
|
}
|
|
|
|
const getSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => {
|
|
const ret = globalScene.addFieldSprite(0, 0, spriteKey);
|
|
ret.setOrigin(0.5, 1);
|
|
ret.setPipeline(globalScene.spritePipeline, {
|
|
tone: [0.0, 0.0, 0.0, 0.0],
|
|
hasShadow: !!hasShadow,
|
|
yShadowOffset: yShadow ?? 0,
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
const getItemSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => {
|
|
const icon = globalScene.add.sprite(-19, 2, "items", spriteKey);
|
|
icon.setOrigin(0.5, 1);
|
|
icon.setPipeline(globalScene.spritePipeline, {
|
|
tone: [0.0, 0.0, 0.0, 0.0],
|
|
hasShadow: !!hasShadow,
|
|
yShadowOffset: yShadow ?? 0,
|
|
});
|
|
return icon;
|
|
};
|
|
|
|
// Depending on number of sprites added, should space them to be on the circular field sprite
|
|
const minX = -40;
|
|
const maxX = 40;
|
|
const origin = 4;
|
|
let n = 0;
|
|
// Sprites with custom X or Y defined will not count for normal spacing requirements
|
|
const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1));
|
|
|
|
this.shinySparkleSprites = [];
|
|
const shinySparkleSprites = globalScene.add.container(0, 0);
|
|
this.spriteConfigs?.forEach(config => {
|
|
const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha, isPokemon, isShiny, variant } = config;
|
|
|
|
let sprite: GameObjects.Sprite;
|
|
let tintSprite: GameObjects.Sprite;
|
|
let pokemonShinySparkle: Phaser.GameObjects.Sprite | undefined;
|
|
|
|
if (isItem) {
|
|
sprite = getItemSprite(spriteKey, hasShadow, yShadow);
|
|
tintSprite = getItemSprite(spriteKey);
|
|
} else {
|
|
sprite = getSprite(spriteKey, hasShadow, yShadow);
|
|
tintSprite = getSprite(spriteKey);
|
|
if (isPokemon && isShiny) {
|
|
// Set Pipeline for shiny variant
|
|
sprite.setPipelineData("spriteKey", spriteKey);
|
|
tintSprite.setPipelineData("spriteKey", spriteKey);
|
|
sprite.setPipelineData("shiny", true);
|
|
sprite.setPipelineData("variant", variant);
|
|
tintSprite.setPipelineData("shiny", true);
|
|
tintSprite.setPipelineData("variant", variant);
|
|
// Create Sprite for shiny Sparkle
|
|
pokemonShinySparkle = globalScene.add.sprite(sprite.x, sprite.y, "shiny");
|
|
pokemonShinySparkle.setOrigin(0.5, 1);
|
|
pokemonShinySparkle.setVisible(false);
|
|
this.shinySparkleSprites.push({
|
|
sprite: pokemonShinySparkle,
|
|
variant: variant ?? 0,
|
|
});
|
|
shinySparkleSprites.add(pokemonShinySparkle);
|
|
}
|
|
}
|
|
|
|
sprite.setVisible(!config.hidden);
|
|
tintSprite.setVisible(false);
|
|
|
|
if (scale) {
|
|
sprite.setScale(scale);
|
|
tintSprite.setScale(scale);
|
|
}
|
|
|
|
// Sprite offset from origin
|
|
if (x || y) {
|
|
if (x) {
|
|
sprite.setPosition(origin + x, sprite.y);
|
|
tintSprite.setPosition(origin + x, tintSprite.y);
|
|
}
|
|
if (y) {
|
|
sprite.setPosition(sprite.x, sprite.y + y);
|
|
tintSprite.setPosition(tintSprite.x, tintSprite.y + y);
|
|
}
|
|
} else {
|
|
// Single sprite
|
|
if (this.spriteConfigs.length === 1) {
|
|
sprite.x = origin;
|
|
tintSprite.x = origin;
|
|
} else {
|
|
// Do standard sprite spacing (not including offset sprites)
|
|
sprite.x = minX + (n + 0.5) * spacingValue + origin;
|
|
tintSprite.x = minX + (n + 0.5) * spacingValue + origin;
|
|
n++;
|
|
}
|
|
}
|
|
|
|
if (!isNullOrUndefined(pokemonShinySparkle)) {
|
|
// Offset the sparkle to match the Pokemon's position
|
|
pokemonShinySparkle.setPosition(sprite.x, sprite.y);
|
|
}
|
|
|
|
if (!isNullOrUndefined(alpha)) {
|
|
sprite.setAlpha(alpha);
|
|
tintSprite.setAlpha(alpha);
|
|
}
|
|
|
|
this.add(sprite);
|
|
this.add(tintSprite);
|
|
});
|
|
this.add(shinySparkleSprites);
|
|
}
|
|
|
|
/**
|
|
* Loads the assets that were defined on construction (async)
|
|
*/
|
|
loadAssets(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
if (!this.spriteConfigs) {
|
|
resolve();
|
|
}
|
|
|
|
const shinyPromises: Promise<void>[] = [];
|
|
this.spriteConfigs.forEach(config => {
|
|
if (config.isPokemon) {
|
|
globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
|
|
if (config.isShiny && !isNullOrUndefined(config.variant)) {
|
|
shinyPromises.push(loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant));
|
|
}
|
|
} else if (config.isItem) {
|
|
globalScene.loadAtlas("items", "");
|
|
} else {
|
|
globalScene.loadAtlas(config.spriteKey, config.fileRoot);
|
|
}
|
|
});
|
|
|
|
globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => {
|
|
this.spriteConfigs.every(config => {
|
|
if (config.isItem) {
|
|
return true;
|
|
}
|
|
|
|
const originalWarn = console.warn;
|
|
|
|
// Ignore warnings for missing frames, because there will be a lot
|
|
console.warn = () => {};
|
|
const frameNames = globalScene.anims.generateFrameNames(config.spriteKey, {
|
|
zeroPad: 4,
|
|
suffix: ".png",
|
|
start: 1,
|
|
end: 128,
|
|
});
|
|
|
|
console.warn = originalWarn;
|
|
if (!globalScene.anims.exists(config.spriteKey)) {
|
|
globalScene.anims.create({
|
|
key: config.spriteKey,
|
|
frames: frameNames,
|
|
frameRate: 10,
|
|
repeat: -1,
|
|
});
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
Promise.all(shinyPromises).then(() => resolve());
|
|
});
|
|
|
|
if (!globalScene.load.isLoading()) {
|
|
globalScene.load.start();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets the initial frames and tint of sprites after load
|
|
*/
|
|
initSprite(): void {
|
|
if (!this.spriteConfigs) {
|
|
return;
|
|
}
|
|
|
|
this.getSprites().map((sprite, i) => {
|
|
if (!this.spriteConfigs[i].isItem) {
|
|
sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
|
|
if (sprite.texture.frameTotal > 1) {
|
|
// Show the first animation frame for a smooth transition when the animation starts.
|
|
const firstFrame = sprite.texture.frames["0001.png"];
|
|
sprite.setFrame(firstFrame ?? 0);
|
|
}
|
|
}
|
|
});
|
|
this.getTintSprites().map((tintSprite, i) => {
|
|
if (!this.spriteConfigs[i].isItem) {
|
|
tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
|
|
if (tintSprite.texture.frameTotal > 1) {
|
|
// Show the first frame for a smooth transition when the animation starts.
|
|
const firstFrame = tintSprite.texture.frames["0001.png"];
|
|
tintSprite.setFrame(firstFrame ?? 0);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.spriteConfigs.every((config, i) => {
|
|
if (!config.tint) {
|
|
return true;
|
|
}
|
|
|
|
const tintSprite = this.getAt(i * 2 + 1);
|
|
this.tint(tintSprite, 0, config.tint);
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Attempts to animate a given set of {@linkcode Phaser.GameObjects.Sprite}
|
|
* @see {@linkcode Phaser.GameObjects.Sprite.play}
|
|
* @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate
|
|
* @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint
|
|
* @param animConfig {@linkcode Phaser.Types.Animations.PlayAnimationConfig} to pass to {@linkcode Phaser.GameObjects.Sprite.play}
|
|
* @returns true if the sprite was able to be animated
|
|
*/
|
|
tryPlaySprite(
|
|
sprite: Phaser.GameObjects.Sprite,
|
|
tintSprite: Phaser.GameObjects.Sprite,
|
|
animConfig: Phaser.Types.Animations.PlayAnimationConfig,
|
|
): boolean {
|
|
// Show an error in the console if there isn't a texture loaded
|
|
if (sprite.texture.key === "__MISSING") {
|
|
console.error(`No texture found for '${animConfig.key}'!`);
|
|
|
|
return false;
|
|
}
|
|
// Don't try to play an animation when there isn't one
|
|
if (sprite.texture.frameTotal <= 1) {
|
|
console.warn(`No animation found for '${animConfig.key}'. Is this intentional?`);
|
|
|
|
return false;
|
|
}
|
|
|
|
sprite.play(animConfig);
|
|
tintSprite.play(animConfig);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Play shiny sparkle animations if there are shiny Pokemon
|
|
*/
|
|
playShinySparkles() {
|
|
for (const sparkleConfig of this.shinySparkleSprites) {
|
|
globalScene.time.delayedCall(500, () => {
|
|
doShinySparkleAnim(sparkleConfig.sprite, sparkleConfig.variant);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For sprites with animation and that do not have animation disabled, will begin frame animation
|
|
*/
|
|
playAnim(): void {
|
|
if (!this.spriteConfigs) {
|
|
return;
|
|
}
|
|
|
|
const sprites = this.getSprites();
|
|
const tintSprites = this.getTintSprites();
|
|
this.spriteConfigs.forEach((config, i) => {
|
|
if (!config.disableAnimation) {
|
|
const trainerAnimConfig: PlayAnimationConfig = {
|
|
key: config.spriteKey,
|
|
repeat: config?.repeat ? -1 : 0,
|
|
startFrame: config?.startFrame ?? 0,
|
|
};
|
|
|
|
this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns a Sprite/TintSprite pair
|
|
* @param index
|
|
*/
|
|
getSpriteAtIndex(index: number): Phaser.GameObjects.Sprite[] {
|
|
if (!this.spriteConfigs) {
|
|
return [];
|
|
}
|
|
|
|
const ret: Phaser.GameObjects.Sprite[] = [];
|
|
ret.push(this.getAt(index * 2)); // Sprite
|
|
ret.push(this.getAt(index * 2 + 1)); // Tint Sprite
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Gets all non-tint sprites (these are the "real" unmodified sprites)
|
|
*/
|
|
getSprites(): Phaser.GameObjects.Sprite[] {
|
|
if (!this.spriteConfigs) {
|
|
return [];
|
|
}
|
|
|
|
const ret: Phaser.GameObjects.Sprite[] = [];
|
|
this.spriteConfigs.forEach((_, i) => {
|
|
ret.push(this.getAt(i * 2));
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Gets all tint sprites (duplicate sprites that have different alpha and fill values)
|
|
*/
|
|
getTintSprites(): Phaser.GameObjects.Sprite[] {
|
|
if (!this.spriteConfigs) {
|
|
return [];
|
|
}
|
|
|
|
const ret: Phaser.GameObjects.Sprite[] = [];
|
|
this.spriteConfigs.forEach((_, i) => {
|
|
ret.push(this.getAt(i * 2 + 1));
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Tints a single sprite
|
|
* @param sprite
|
|
* @param color
|
|
* @param alpha
|
|
* @param duration
|
|
* @param ease
|
|
*/
|
|
private tint(sprite, color: number, alpha?: number, duration?: number, ease?: string): void {
|
|
// const tintSprites = this.getTintSprites();
|
|
sprite.setTintFill(color);
|
|
sprite.setVisible(true);
|
|
|
|
if (duration) {
|
|
sprite.setAlpha(0);
|
|
|
|
globalScene.tweens.add({
|
|
targets: sprite,
|
|
alpha: alpha || 1,
|
|
duration: duration,
|
|
ease: ease || "Linear",
|
|
});
|
|
} else {
|
|
sprite.setAlpha(alpha);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tints all sprites
|
|
* @param color
|
|
* @param alpha
|
|
* @param duration
|
|
* @param ease
|
|
*/
|
|
tintAll(color: number, alpha?: number, duration?: number, ease?: string): void {
|
|
const tintSprites = this.getTintSprites();
|
|
tintSprites.map(tintSprite => {
|
|
this.tint(tintSprite, color, alpha, duration, ease);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Untints a single sprite over a duration
|
|
* @param sprite
|
|
* @param duration
|
|
* @param ease
|
|
*/
|
|
private untint(sprite, duration: number, ease?: string): void {
|
|
if (duration) {
|
|
globalScene.tweens.add({
|
|
targets: sprite,
|
|
alpha: 0,
|
|
duration: duration,
|
|
ease: ease || "Linear",
|
|
onComplete: () => {
|
|
sprite.setVisible(false);
|
|
sprite.setAlpha(1);
|
|
},
|
|
});
|
|
} else {
|
|
sprite.setVisible(false);
|
|
sprite.setAlpha(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Untints all sprites
|
|
* @param sprite
|
|
* @param duration
|
|
* @param ease
|
|
*/
|
|
untintAll(duration: number, ease?: string): void {
|
|
const tintSprites = this.getTintSprites();
|
|
tintSprites.map(tintSprite => {
|
|
this.untint(tintSprite, duration, ease);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets container and all child sprites to visible
|
|
* @param value - true for visible, false for hidden
|
|
*/
|
|
setVisible(value: boolean): this {
|
|
for (const sprite of this.getSprites()) {
|
|
sprite.setVisible(value);
|
|
}
|
|
return super.setVisible(value);
|
|
}
|
|
}
|