[Sprite][Bug][ME] Fix ME Intro visuals for shinies and other shiny related fixes (#4827)

* [ME] Fix GTS Wonder Trade shiny not giving luck

* [ME] Shiny Magikarp from Pokemon Salesman can have any variant

* [ME] Shiny lock MEs with custom or special sprites

* [ME] GTS shows shiny sparkle for received Pokemon

* [ME] Shiny lock 'Slumbering Snorlax' and 'The Strong Stuff'

* [ME] Dancing Lessson: show shiny sparkle for Oricorio in intro

* [ME] Show shiny sparkles for Pokemon in ME intro

* fix tests

* Ensure shiny sparkle animation is initialized before playing it (Fixes #3924)

* make loading variant assets cleaner

* cleanup EnemyPokemon shiny initialization

* test fixes and final cleanup

* Make 'getSpeciesFilterRandomPartyMemberFunc' more readable

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Moka 2024-12-01 01:08:53 +01:00 committed by GitHub
parent 80555be22c
commit 38d7a26053
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 260 additions and 99 deletions

View File

@ -47,7 +47,7 @@ import PokemonInfoContainer from "#app/ui/pokemon-info-container";
import { biomeDepths, getBiomeName } from "#app/data/balance/biomes";
import { SceneBase } from "#app/scene-base";
import CandyBar from "#app/ui/candy-bar";
import { Variant, variantData } from "#app/data/variant";
import { Variant, variantColorCache, variantData, VariantSet } from "#app/data/variant";
import { Localizable } from "#app/interfaces/locales";
import Overrides from "#app/overrides";
import { InputsController } from "#app/inputs-controller";
@ -345,6 +345,33 @@ export default class BattleScene extends SceneBase {
this.load.atlas(key, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.png`, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.json`);
}
/**
* Load the variant assets for the given sprite and stores them in {@linkcode variantColorCache}
*/
loadPokemonVariantAssets(spriteKey: string, fileRoot: string, variant?: Variant) {
const useExpSprite = this.experimentalSprites && this.hasExpSprite(spriteKey);
if (useExpSprite) {
fileRoot = `exp/${fileRoot}`;
}
let variantConfig = variantData;
fileRoot.split("/").map(p => variantConfig ? variantConfig = variantConfig[p] : null);
const variantSet = variantConfig as VariantSet;
if (variantSet && (variant !== undefined && variantSet[variant] === 1)) {
const populateVariantColors = (key: string): Promise<void> => {
return new Promise(resolve => {
if (variantColorCache.hasOwnProperty(key)) {
return resolve();
}
this.cachedFetch(`./images/pokemon/variant/${fileRoot}.json`).then(res => res.json()).then(c => {
variantColorCache[key] = c;
resolve();
});
});
};
populateVariantColors(spriteKey);
}
}
async preload() {
if (DEBUG_RNG) {
const scene = this;
@ -891,7 +918,7 @@ export default class BattleScene extends SceneBase {
return pokemon;
}
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, shinyLock: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
level = Overrides.OPP_LEVEL_OVERRIDE;
}
@ -901,7 +928,7 @@ export default class BattleScene extends SceneBase {
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
}
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource);
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, shinyLock, dataSource);
if (Overrides.OPP_FUSION_OVERRIDE) {
pokemon.generateFusionSpecies();
}

View File

@ -216,6 +216,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.GREEDENT),
isBoss: true,
bossSegments: 3,
shiny: false, // Shiny lock because of consistency issues between the different options
moveSet: [ Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.CRUNCH ],
modifierConfigs: bossModifierConfigs,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
@ -353,9 +354,9 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
})
.withOptionPhase(async (scene: BattleScene) => {
// Let it have the food
// Greedent joins the team, level equal to 2 below highest party member
// Greedent joins the team, level equal to 2 below highest party member (shiny locked)
const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2;
const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false);
const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false, true);
greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ];
greedent.passive = true;

View File

@ -98,7 +98,9 @@ export const BerriesAboundEncounter: MysteryEncounter =
tint: 0.25,
x: -5,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: bossPokemon.shiny,
variant: bossPokemon.variant
}
];

View File

@ -92,9 +92,13 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withCatchAllowed(true)
.withFleeAllowed(false)
.withOnVisualsStart((scene: BattleScene) => {
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()!);
danceAnim.play(scene);
const oricorio = scene.getEnemyPokemon()!;
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, oricorio, scene.getPlayerPokemon()!);
danceAnim.play(scene, false, () => {
if (oricorio.shiny) {
oricorio.sparkle();
}
});
return true;
})
.withIntroDialogue([
@ -136,7 +140,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
}
const oricorioData = new PokemonData(enemyPokemon);
const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, oricorioData);
const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation)
scene.getEnemyParty().forEach(enemyPokemon => {

View File

@ -114,7 +114,9 @@ export const FightOrFlightEncounter: MysteryEncounter =
tint: 0.25,
x: -5,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: bossPokemon.shiny,
variant: bossPokemon.variant
},
];

View File

@ -194,10 +194,10 @@ async function summonPlayerPokemon(scene: BattleScene) {
playerAnimationPromise = summonPlayerPokemonAnimation(scene, playerPokemon);
});
// Also loads Wobbuffet data
// Also loads Wobbuffet data (cannot be shiny)
const enemySpecies = getPokemonSpecies(Species.WOBBUFFET);
scene.currentBattle.enemyParty = [];
const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false);
const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false, true);
wobbuffet.ivs = [ 0, 0, 0, 0, 0, 0 ];
wobbuffet.setNature(Nature.MILD);
wobbuffet.setAlpha(0);

View File

@ -12,8 +12,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon
import { getTypeRgb } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import * as Utils from "#app/utils";
import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils";
import { NumberHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, ShinyRateBoosterModifier, SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
@ -27,6 +26,7 @@ import { trainerNamePools } from "#app/data/trainer-names";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem";
@ -230,7 +230,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false);
// Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) {
const shinyThreshold = new Utils.IntegerHolder(WONDER_TRADE_SHINY_CHANCE);
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
if (scene.eventManager.isEventActive()) {
shinyThreshold.value *= scene.eventManager.getShinyMultiplier();
}
@ -247,7 +247,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
const hiddenIndex = tradePokemon.species.ability2 ? 2 : 1;
if (tradePokemon.species.abilityHidden) {
if (tradePokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new IntegerHolder(64);
const hiddenAbilityChance = new NumberHolder(64);
scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -797,6 +797,14 @@ function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPoke
receivedPokeballSprite.x = tradeBaseBg.displayWidth / 2;
receivedPokeballSprite.y = tradeBaseBg.displayHeight / 2 - 100;
// Received pokemon sparkles
let pokemonShinySparkle: Phaser.GameObjects.Sprite;
if (receivedPokemon.shiny) {
pokemonShinySparkle = scene.add.sprite(receivedPokemonSprite.x, receivedPokemonSprite.y, "shiny");
pokemonShinySparkle.setVisible(false);
tradeContainer.add(pokemonShinySparkle);
}
const BASE_ANIM_DURATION = 1000;
// Pokeball falls to the screen
@ -835,6 +843,11 @@ function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPoke
scale: 1,
alpha: 0,
onComplete: () => {
if (receivedPokemon.shiny) {
scene.time.delayedCall(500, () => {
doShinySparkleAnim(scene, pokemonShinySparkle, receivedPokemon.variant);
});
}
receivedPokeballSprite.destroy();
scene.time.delayedCall(2000, () => resolve());
}

View File

@ -60,6 +60,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies,
isBoss: true,
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
status: [ StatusEffect.SLEEP, 5 ], // Extra turns on timer for Snorlax's start of fight moves
moveSet: [ Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT ],
modifierConfigs: [

View File

@ -72,13 +72,11 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
let pokemon: PlayerPokemon;
if (randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) {
// If no HA mon found or you roll 1%, give shiny Magikarp
// If no HA mon found or you roll 1%, give shiny Magikarp with random variant
species = getPokemonSpecies(Species.MAGIKARP);
const hiddenIndex = species.ability2 ? 2 : 1;
pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex, undefined, true, 0);
pokemon = new PlayerPokemon(scene, species, 5, 2, species.formIndex, undefined, true);
} else {
const hiddenIndex = species.ability2 ? 2 : 1;
pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex);
pokemon = new PlayerPokemon(scene, species, 5, 2, species.formIndex);
}
pokemon.generateAndPopulateMoveset();
@ -88,7 +86,9 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
fileRoot: fileRoot,
hasShadow: true,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: pokemon.shiny,
variant: pokemon.variant
});
const starterTier = speciesStarterCosts[species.speciesId];

View File

@ -79,6 +79,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],

View File

@ -61,11 +61,12 @@ export const TrashToTreasureEncounter: MysteryEncounter =
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Calculate boss mon
// Calculate boss mon (shiny locked)
const bossSpecies = getPokemonSpecies(Species.GARBODOR);
const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies,
isBoss: true,
shiny: false, // Shiny lock because of custom intro sprite
formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ]

View File

@ -100,7 +100,9 @@ export const UncommonBreedEncounter: MysteryEncounter =
hasShadow: true,
x: -5,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: pokemon.shiny,
variant: pokemon.variant
},
];
@ -113,13 +115,15 @@ export const UncommonBreedEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
const pokemonSprite = encounter.introVisuals!.getSprites();
scene.tweens.add({ // Bounce at the end
// Bounce at the end, then shiny sparkle if the Pokemon is shiny
scene.tweens.add({
targets: pokemonSprite,
duration: 300,
ease: "Cubic.easeOut",
yoyo: true,
y: "-=20",
loop: 1,
onComplete: () => encounter.introVisuals?.playShinySparkles()
});
scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2"));

View File

@ -184,7 +184,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
dataSource = config.dataSource;
enemySpecies = config.species;
isBoss = config.isBoss;
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource);
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, false, dataSource);
} else {
battle.enemyParty[e] = battle.trainer.genPartyMember(e);
}
@ -202,7 +202,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
enemySpecies = scene.randomSpecies(battle.waveIndex, level, true);
}
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, dataSource);
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, false, dataSource);
}
}

View File

@ -15,7 +15,7 @@ import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPr
import { Type } from "#enums/type";
import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import { Stat } from "#enums/stat";
import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant";
import { Variant, VariantSet, variantData } from "#app/data/variant";
import { speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
import { SpeciesFormKey } from "#enums/species-form-key";
@ -511,29 +511,8 @@ export abstract class PokemonSpeciesForm {
} else {
scene.anims.get(spriteKey).frameRate = 10;
}
let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey);
if (useExpSprite) {
spritePath = `exp/${spritePath}`;
}
let config = variantData;
spritePath.split("/").map(p => config ? config = config[p] : null);
const variantSet = config as VariantSet;
if (variantSet && (variant !== undefined && variantSet[variant] === 1)) {
const populateVariantColors = (key: string): Promise<void> => {
return new Promise(resolve => {
if (variantColorCache.hasOwnProperty(key)) {
return resolve();
}
scene.cachedFetch(`./images/pokemon/variant/${spritePath}.json`).then(res => res.json()).then(c => {
variantColorCache[key] = c;
resolve();
});
});
};
populateVariantColors(spriteKey).then(() => resolve());
return;
}
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
scene.loadPokemonVariantAssets(spriteKey, spritePath, variant);
resolve();
});
if (startLoad) {

View File

@ -1173,16 +1173,28 @@ export function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: Tr
if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex);
}
return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, undefined, postProcess);
return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, false, undefined, postProcess);
};
}
function getSpeciesFilterRandomPartyMemberFunc(speciesFilter: PokemonSpeciesFilter, trainerSlot: TrainerSlot = TrainerSlot.TRAINER, allowLegendaries?: boolean, postProcess?: (EnemyPokemon: EnemyPokemon) => void): PartyMemberFunc {
const originalSpeciesFilter = speciesFilter;
speciesFilter = (species: PokemonSpecies) => (allowLegendaries || (!species.legendary && !species.subLegendary && !species.mythical)) && !species.isTrainerForbidden() && originalSpeciesFilter(species);
return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => {
const ret = scene.addEnemyPokemon(getPokemonSpecies(scene.randomSpecies(scene.currentBattle.waveIndex, level, false, speciesFilter).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex)), level, trainerSlot, undefined, undefined, postProcess);
return ret;
function getSpeciesFilterRandomPartyMemberFunc(
originalSpeciesFilter: PokemonSpeciesFilter,
trainerSlot: TrainerSlot = TrainerSlot.TRAINER,
allowLegendaries?: boolean,
postProcess?: (EnemyPokemon: EnemyPokemon) => void
): PartyMemberFunc {
const speciesFilter = (species: PokemonSpecies): boolean => {
const notLegendary = !species.legendary && !species.subLegendary && !species.mythical;
return (allowLegendaries || notLegendary) && !species.isTrainerForbidden() && originalSpeciesFilter(species);
};
return (scene: BattleScene, level: number, strength: PartyMemberStrength) => {
const waveIndex = scene.currentBattle.waveIndex;
const species = getPokemonSpecies(scene.randomSpecies(waveIndex, level, false, speciesFilter)
.getTrainerSpeciesForLevel(level, true, strength, waveIndex));
return scene.addEnemyPokemon(species, level, trainerSlot, undefined, false, undefined, postProcess);
};
}

View File

@ -1,6 +1,7 @@
import BattleScene from "../battle-scene";
import BattleScene from "#app/battle-scene";
import { PokeballType } from "#enums/pokeball";
import * as Utils from "../utils";
import { Variant } from "#app/data/variant";
import { getFrameMs, randGauss } from "#app/utils";
export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void {
switch (pokeballType) {
@ -127,7 +128,7 @@ function doFanOutParticle(scene: BattleScene, trigIndex: integer, x: integer, y:
const particleTimer = scene.tweens.addCounter({
repeat: -1,
duration: Utils.getFrameMs(1),
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
}
@ -159,7 +160,7 @@ export function addPokeballCaptureStars(scene: BattleScene, pokeball: Phaser.Gam
}
});
const dist = Utils.randGauss(25);
const dist = randGauss(25);
scene.tweens.add({
targets: particle,
x: pokeball.x + dist,
@ -185,3 +186,31 @@ export function sin(index: integer, amplitude: integer): number {
export function cos(index: integer, amplitude: integer): number {
return amplitude * Math.cos(index * (Math.PI / 128));
}
/**
* Play the shiny sparkle animation and sound effect for the given sprite
* First ensures that the animation has been properly initialized
* @param sparkleSprite the Sprite to play the animation on
* @param variant which shiny {@linkcode variant} to play the animation for
*/
export function doShinySparkleAnim(scene: BattleScene, sparkleSprite: Phaser.GameObjects.Sprite, variant: Variant) {
const keySuffix = variant ? `_${variant + 1}` : "";
const spriteKey = `shiny${keySuffix}`;
const animationKey = `sparkle${keySuffix}`;
// Make sure the animation exists, and create it if not
if (!scene.anims.exists(animationKey)) {
const frameNames = scene.anims.generateFrameNames(spriteKey, { suffix: ".png", end: 34 });
scene.anims.create({
key: `sparkle${keySuffix}`,
frames: frameNames,
frameRate: 32,
showOnStart: true,
hideOnComplete: true,
});
}
// Play the animation
sparkleSprite.play(animationKey);
scene.playSound("se/sparkle");
}

View File

@ -1,10 +1,12 @@
import { GameObjects } from "phaser";
import BattleScene from "../battle-scene";
import MysteryEncounter from "../data/mystery-encounters/mystery-encounter";
import BattleScene from "#app/battle-scene";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { Species } from "#enums/species";
import { isNullOrUndefined } from "#app/utils";
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig;
import { Variant } from "#app/data/variant";
import { doShinySparkleAnim } from "#app/field/anims";
type KnownFileRoot =
| "arenas"
@ -59,6 +61,10 @@ export class MysteryEncounterSpriteConfig {
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 */
@ -74,6 +80,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
public encounter: MysteryEncounter;
public spriteConfigs: MysteryEncounterSpriteConfig[];
public enterFromRight: boolean;
private shinySparkleSprites: { sprite: Phaser.GameObjects.Sprite, variant: Variant }[];
constructor(scene: BattleScene, encounter: MysteryEncounter) {
super(scene, -72, 76);
@ -86,7 +93,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
};
if (!isNullOrUndefined(result.species)) {
const keys = getSpriteKeysFromSpecies(result.species);
const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant);
result.spriteKey = keys.spriteKey;
result.fileRoot = keys.fileRoot;
result.isPokemon = true;
@ -120,18 +127,36 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
// 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 = scene.add.container(0, 0);
this.spriteConfigs?.forEach((config) => {
const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = 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 = getSprite(spriteKey, hasShadow, yShadow);
tintSprite = getSprite(spriteKey);
} else {
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 = scene.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);
@ -165,6 +190,11 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
}
}
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);
@ -173,6 +203,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
this.add(sprite);
this.add(tintSprite);
});
this.add(shinySparkleSprites);
}
/**
@ -187,6 +218,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
this.spriteConfigs.forEach((config) => {
if (config.isPokemon) {
this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
if (config.isShiny) {
this.scene.loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant);
}
} else if (config.isItem) {
this.scene.loadAtlas("items", "");
} else {
@ -240,11 +274,21 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
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);
}
}
});
@ -288,6 +332,17 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
return true;
}
/**
* Play shiny sparkle animations if there are shiny Pokemon
*/
playShinySparkles() {
for (const sparkleConfig of this.shinySparkleSprites) {
this.scene.time.delayedCall(500, () => {
doShinySparkleAnim(this.scene, sparkleConfig.sprite, sparkleConfig.variant);
});
}
}
/**
* For sprites with animation and that do not have animation disabled, will begin frame animation
*/

View File

@ -69,6 +69,7 @@ import { SpeciesFormKey } from "#enums/species-form-key";
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates";
import { Nature } from "#enums/nature";
import { StatusEffect } from "#enums/status-effect";
import { doShinySparkleAnim } from "#app/field/anims";
export enum FieldPosition {
CENTER,
@ -673,21 +674,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
initShinySparkle(): void {
const keySuffix = this.variant ? `_${this.variant + 1}` : "";
const key = `shiny${keySuffix}`;
const shinySparkle = this.scene.addFieldSprite(0, 0, key);
const shinySparkle = this.scene.addFieldSprite(0, 0, "shiny");
shinySparkle.setVisible(false);
shinySparkle.setOrigin(0.5, 1);
const frameNames = this.scene.anims.generateFrameNames(key, { suffix: ".png", end: 34 });
if (!(this.scene.anims.exists(`sparkle${keySuffix}`))) {
this.scene.anims.create({
key: `sparkle${keySuffix}`,
frames: frameNames,
frameRate: 32,
showOnStart: true,
hideOnComplete: true,
});
}
this.add(shinySparkle);
this.shinySparkle = shinySparkle;
@ -1976,6 +1965,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Function that tries to set a Pokemon shiny based on seed.
* For manual use only, usually to roll a Pokemon's shiny chance a second time.
* If it rolls shiny, also sets a random variant and give the Pokemon the associated luck.
*
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
@ -2001,6 +1991,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.shiny = randSeedInt(65536) < shinyThreshold.value;
if (this.shiny) {
this.variant = this.generateShinyVariant();
this.luck = this.variant + 1 + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.initShinySparkle();
}
@ -3802,8 +3794,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
sparkle(): void {
if (this.shinySparkle) {
this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ""}`);
this.scene.playSound("se/sparkle");
doShinySparkleAnim(this.scene, this.shinySparkle, this.variant);
}
}
@ -4646,12 +4637,13 @@ export class EnemyPokemon extends Pokemon {
public aiType: AiType;
public bossSegments: integer;
public bossSegmentIndex: integer;
/** To indicate of the instance was populated with a dataSource -> e.g. loaded & populated from session data */
/** To indicate if the instance was populated with a dataSource -> e.g. loaded & populated from session data */
public readonly isPopulatedFromDataSource: boolean;
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource?: PokemonData) {
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex,
dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, undefined, dataSource ? dataSource.nature : undefined, dataSource);
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, shinyLock: boolean = false, dataSource?: PokemonData) {
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex, dataSource?.gender,
(!shinyLock && dataSource) ? dataSource.shiny : false, (!shinyLock && dataSource) ? dataSource.variant : undefined,
undefined, dataSource ? dataSource.nature : undefined, dataSource);
this.trainerSlot = trainerSlot;
this.isPopulatedFromDataSource = !!dataSource; // if a dataSource is provided, then it was populated from dataSource
@ -4680,12 +4672,15 @@ export class EnemyPokemon extends Pokemon {
if (!dataSource) {
this.generateAndPopulateMoveset();
this.trySetShiny();
if (Overrides.OPP_SHINY_OVERRIDE) {
if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) {
this.shiny = false;
} else {
this.trySetShiny();
}
if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) {
this.shiny = true;
this.initShinySparkle();
} else if (Overrides.OPP_SHINY_OVERRIDE === false) {
this.shiny = false;
}
if (this.shiny) {

View File

@ -14,6 +14,7 @@ import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import * as Utils from "#app/utils";
import { EggLapsePhase } from "./egg-lapse-phase";
import { EggHatchData } from "#app/data/egg-hatch-data";
import { doShinySparkleAnim } from "#app/field/anims";
/**
@ -341,8 +342,7 @@ export class EggHatchPhase extends Phase {
this.pokemon.cry();
if (isShiny) {
this.scene.time.delayedCall(Utils.fixedInt(500), () => {
this.pokemonShinySparkle.play(`sparkle${this.pokemon.variant ? `_${this.pokemon.variant + 1}` : ""}`);
this.scene.playSound("se/sparkle");
doShinySparkleAnim(this.scene, this.pokemonShinySparkle, this.pokemon.variant);
});
}
this.scene.time.delayedCall(Utils.fixedInt(!this.skipped ? !isShiny ? 1250 : 1750 : !isShiny ? 250 : 750), () => {

View File

@ -379,6 +379,9 @@ export class EncounterPhase extends BattlePhase {
if (encounter.onVisualsStart) {
encounter.onVisualsStart(this.scene);
} else if (encounter.spriteConfigs && introVisuals) {
// If the encounter doesn't have any special visual intro, show sparkle for shiny Pokemon
introVisuals.playShinySparkles();
}
const doEncounter = () => {

View File

@ -171,7 +171,7 @@ export default class PokemonData {
playerPokemon.nickname = this.nickname;
}
})
: scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER ? !double || !(partyMemberIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER : TrainerSlot.NONE, this.boss, this);
: scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER ? !double || !(partyMemberIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER : TrainerSlot.NONE, this.boss, false, this);
if (this.summonData) {
ret.primeSummonData(this.summonData);
}

View File

@ -18,6 +18,7 @@ import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { ModifierTier } from "#app/modifier/modifier-tier";
import * as Utils from "#app/utils";
const namespace = "mysteryEncounters/globalTradeSystem";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -176,6 +177,23 @@ describe("Global Trade System - Mystery Encounter", () => {
expect(defaultParty.includes(speciesAfter!)).toBeFalsy();
});
it("Should roll for shiny twice, with random variant and associated luck", async () => {
// This ensures that the first shiny roll gets ignored, to test the ME rerolling for shiny
game.override.enemyShiny(false);
await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty);
vi.spyOn(Utils, "randSeedInt").mockReturnValue(1); // force shiny on reroll
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 });
const receivedPokemon = scene.getPlayerParty().at(-1)!;
expect(receivedPokemon.shiny).toBeTruthy();
expect(receivedPokemon.variant).toBeDefined();
expect(receivedPokemon.luck).toBe(receivedPokemon.variant + 1);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");

View File

@ -123,7 +123,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
});
});
it("Should update the player's money properly", async () => {
it("should update the player's money properly", async () => {
const initialMoney = 20000;
scene.money = initialMoney;
const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
@ -137,7 +137,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(scene.money).toBe(initialMoney - price);
});
it("Should add the Pokemon to the party", async () => {
it("should add the Pokemon to the party", async () => {
scene.money = 20000;
await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty);
@ -153,6 +153,18 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy();
});
it("should give the purchased Pokemon its HA or make it shiny", async () => {
scene.money = 20000;
await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty);
await runMysteryEncounterToEnd(game, 1);
const newlyPurchasedPokemon = scene.getPlayerParty()[scene.getPlayerParty().length - 1];
const isshiny = newlyPurchasedPokemon.shiny;
const hasHA = newlyPurchasedPokemon.abilityIndex === 2;
expect(isshiny || hasHA).toBeTruthy();
expect(isshiny && hasHA).toBeFalsy();
});
it("should be disabled if player does not have enough money", async () => {
scene.money = 0;
await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty);

View File

@ -109,6 +109,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
shiny: false,
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],

View File

@ -92,6 +92,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
{
species: getPokemonSpecies(Species.GARBODOR),
isBoss: true,
shiny: false,
formIndex: 1,
bossSegmentModifier: 1,
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ],