diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 62e9d8ea717..2c38bb6632f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -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 => { + 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(); } diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index 9c00148fbac..6b0f239d28d 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -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; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 5524511c67b..786ca3e8fc0 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -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 } ]; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index bae5a8790e9..841aadd7c36 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -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 => { diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 3f9030dc3b2..3533e10df29 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -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 }, ]; diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index c286fffe0de..84c3e56a836 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -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); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 2d569621449..934fc1b805b 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -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()); } diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 3fb502be545..8dd03e12caa 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -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: [ diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 61a4de908e9..feb6e68d1d1 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -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]; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 754632aedea..c5cfd3f954e 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -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 ], diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index fba3a6ca95e..dfd89cdfb63 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -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 ] diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index a2c32c6af40..d3679825ac8 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -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")); diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index b5dd43a9221..d43bce0ace5 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -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); } } diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index ec104d4d4aa..09788e353cf 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -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 => { - 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) { diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 5e5f38bd00d..d99ca601bdf 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -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); }; } diff --git a/src/field/anims.ts b/src/field/anims.ts index dddf38e4a7e..10198c29005 100644 --- a/src/field/anims.ts +++ b/src/field/anims.ts @@ -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"); +} diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 1577d1157d7..b1b85de9b29 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -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 */ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5706725f763..0675b9485cf 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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) { diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 90aceeb46bc..d45c580228c 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -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), () => { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 03126ba81bb..b524172af9d 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -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 = () => { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 443382186c7..64801cc0ff1 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -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); } diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index e8d19ff50b9..2c226df3c8c 100644 --- a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -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"); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 91b4a4bcab5..e90bc4efe56 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -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); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index ae725f3480a..5c965b13bd4 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -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 ], diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 8286c6a694b..f8d96487092 100644 --- a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -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 ],