import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import type { BerryModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#enums/moves"; import { BattlerTagType } from "#enums/battler-tag-type"; import { randInt } from "#app/utils"; import { BattlerIndex } from "#app/battle"; import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { TrainerSlot } from "#app/data/trainer-config"; import { PokeballType } from "#enums/pokeball"; import type HeldModifierConfig from "#app/interfaces/held-modifier-config"; import type { BerryType } from "#enums/berry-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import i18next from "i18next"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/absoluteAvarice"; /** * Absolute Avarice encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} * @see For biome requirements check {@linkcode mysteryEncountersByBiome} */ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.ABSOLUTE_AVARICE, ) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Must have at least 4 berries to spawn .withFleeAllowed(false) .withIntroSpriteConfigs([ { // This sprite has the shadow spriteKey: "", fileRoot: "", species: Species.GREEDENT, hasShadow: true, alpha: 0.001, repeat: true, x: -5, }, { spriteKey: "", fileRoot: "", species: Species.GREEDENT, hasShadow: false, repeat: true, x: -5, }, { spriteKey: "lum_berry", fileRoot: "items", isItem: true, x: 7, y: -14, hidden: true, disableAnimation: true, }, { spriteKey: "salac_berry", fileRoot: "items", isItem: true, x: 2, y: 4, hidden: true, disableAnimation: true, }, { spriteKey: "lansat_berry", fileRoot: "items", isItem: true, x: 32, y: 5, hidden: true, disableAnimation: true, }, { spriteKey: "liechi_berry", fileRoot: "items", isItem: true, x: 6, y: -5, hidden: true, disableAnimation: true, }, { spriteKey: "sitrus_berry", fileRoot: "items", isItem: true, x: 7, y: 8, hidden: true, disableAnimation: true, }, { spriteKey: "enigma_berry", fileRoot: "items", isItem: true, x: 26, y: -4, hidden: true, disableAnimation: true, }, { spriteKey: "leppa_berry", fileRoot: "items", isItem: true, x: 16, y: -27, hidden: true, disableAnimation: true, }, { spriteKey: "petaya_berry", fileRoot: "items", isItem: true, x: 30, y: -17, hidden: true, disableAnimation: true, }, { spriteKey: "ganlon_berry", fileRoot: "items", isItem: true, x: 16, y: -11, hidden: true, disableAnimation: true, }, { spriteKey: "apicot_berry", fileRoot: "items", isItem: true, x: 14, y: -2, hidden: true, disableAnimation: true, }, { spriteKey: "starf_berry", fileRoot: "items", isItem: true, x: 18, y: 9, hidden: true, disableAnimation: true, }, ]) .withHideWildIntroMessage(true) .withAutoHideIntroVisuals(false) .withIntroDialogue([ { text: `${namespace}:intro`, }, ]) .setLocalizationKey(`${namespace}`) .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) .withOnInit(() => { const encounter = globalScene.currentBattle.mysteryEncounter!; globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); // Get all player berry items, remove from party, and store reference const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; // Sort berries by party member ID to more easily re-add later if necessary const berryItemsMap = new Map<number, BerryModifier[]>(); globalScene.getPlayerParty().forEach(pokemon => { const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); if (pokemonBerries?.length > 0) { berryItemsMap.set(pokemon.id, pokemonBerries); } }); encounter.misc = { berryItemsMap }; // Generates copies of the stolen berries to put on the Greedent const bossModifierConfigs: HeldModifierConfig[] = []; berryItems.forEach(berryMod => { // Can't define stack count on a ModifierType, have to just create separate instances for each stack // Overflow berries will be "lost" on the boss, but it's un-catchable anyway for (let i = 0; i < berryMod.stackCount; i++) { const modifierType = generateModifierType(modifierTypes.BERRY, [ berryMod.berryType, ]) as PokemonHeldItemModifierType; bossModifierConfigs.push({ modifier: modifierType }); } }); // Do NOT remove the real berries yet or else it will be persisted in the session data // SpDef buff below wave 50, +1 to all stats otherwise const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]; // Calculate boss mon const config: EnemyPartyConfig = { levelAdditiveModifier: 1, pokemonConfigs: [ { 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], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { queueEncounterMessage(`${namespace}:option.1.boss_enraged`); globalScene.unshiftPhase( new StatStageChangePhase(pokemon.getBattlerIndex(), true, statChangesForBattle, 1), ); }, }, ], }; encounter.enemyPartyConfigs = [config]; encounter.setDialogueToken("greedentName", getPokemonSpecies(Species.GREEDENT).getName()); return true; }) .withOnVisualsStart(() => { doGreedentSpriteSteal(); doBerrySpritePile(); // Remove the berries from the party // Session has been safely saved at this point, so data won't be lost const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; berryItems.forEach(berryMod => { globalScene.removeModifier(berryMod); }); globalScene.updateModifiers(true); return true; }) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option.1.label`, buttonTooltip: `${namespace}:option.1.tooltip`, selected: [ { text: `${namespace}:option.1.selected`, }, ], }) .withOptionPhase(async () => { // Pick battle const encounter = globalScene.currentBattle.mysteryEncounter!; // Provides 1x Reviver Seed to each party member at end of battle const revSeed = generateModifierType(modifierTypes.REVIVER_SEED); encounter.setDialogueToken( "foodReward", revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), ); const givePartyPokemonReviverSeeds = () => { const party = globalScene.getPlayerParty(); party.forEach(p => { const heldItems = p.getHeldItems(); if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { const seedModifier = revSeed.newModifier(p); globalScene.addModifier(seedModifier, false, false, false, true); } }); queueEncounterMessage(`${namespace}:option.1.food_stash`); }; setEncounterRewards({ fillRemaining: true }, undefined, givePartyPokemonReviverSeeds); encounter.startOfBattleEffects.push({ sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY], move: new PokemonMove(Moves.STUFF_CHEEKS), ignorePp: true, }); await transitionMysteryEncounterIntroVisuals(true, true, 500); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); }) .build(), ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, selected: [ { text: `${namespace}:option.2.selected`, }, ], }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; const berryMap = encounter.misc.berryItemsMap; // Returns 2/5 of the berries stolen to each Pokemon const party = globalScene.getPlayerParty(); party.forEach(pokemon => { const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); const berryTypesAsArray: BerryType[] = []; stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType))); const returnedBerryCount = Math.floor(((berryTypesAsArray.length ?? 0) * 2) / 5); if (returnedBerryCount > 0) { for (let i = 0; i < returnedBerryCount; i++) { // Shuffle remaining berry types and pop Phaser.Math.RND.shuffle(berryTypesAsArray); const randBerryType = berryTypesAsArray.pop(); const berryModType = generateModifierType(modifierTypes.BERRY, [randBerryType]) as BerryModifierType; applyModifierTypeToPlayerPokemon(pokemon, berryModType); } } }); await globalScene.updateModifiers(true); await transitionMysteryEncounterIntroVisuals(true, true, 500); leaveEncounterWithoutBattle(true); }) .build(), ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, selected: [ { text: `${namespace}:option.3.selected`, }, ], }) .withPreOptionPhase(async () => { // Animate berries being eaten doGreedentEatBerries(); doBerrySpritePile(true); return true; }) .withOptionPhase(async () => { // Let it have the food // Greedent joins the team, level equal to 2 below highest party member (shiny locked) const level = getHighestLevelPlayerPokemon(false, true).level - 2; const greedent = new EnemyPokemon(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; await transitionMysteryEncounterIntroVisuals(true, true, 500); await catchPokemon(greedent, null, PokeballType.POKEBALL, false); leaveEncounterWithoutBattle(true); }) .build(), ) .build(); function doGreedentSpriteSteal() { const shakeDelay = 50; const slideDelay = 500; const greedentSprites = globalScene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); globalScene.playSound("battle_anims/Follow Me"); globalScene.tweens.chain({ targets: greedentSprites, tweens: [ { // Slide Greedent diagonally duration: slideDelay, ease: "Cubic.easeOut", y: "+=75", x: "-=65", scale: 1.1, }, { // Shake duration: shakeDelay, ease: "Cubic.easeOut", yoyo: true, x: (randInt(2) > 0 ? "-=" : "+=") + 5, y: (randInt(2) > 0 ? "-=" : "+=") + 5, }, { // Shake duration: shakeDelay, ease: "Cubic.easeOut", yoyo: true, x: (randInt(2) > 0 ? "-=" : "+=") + 5, y: (randInt(2) > 0 ? "-=" : "+=") + 5, }, { // Shake duration: shakeDelay, ease: "Cubic.easeOut", yoyo: true, x: (randInt(2) > 0 ? "-=" : "+=") + 5, y: (randInt(2) > 0 ? "-=" : "+=") + 5, }, { // Shake duration: shakeDelay, ease: "Cubic.easeOut", yoyo: true, x: (randInt(2) > 0 ? "-=" : "+=") + 5, y: (randInt(2) > 0 ? "-=" : "+=") + 5, }, { // Shake duration: shakeDelay, ease: "Cubic.easeOut", yoyo: true, x: (randInt(2) > 0 ? "-=" : "+=") + 5, y: (randInt(2) > 0 ? "-=" : "+=") + 5, }, { // Shake duration: shakeDelay, ease: "Cubic.easeOut", yoyo: true, x: (randInt(2) > 0 ? "-=" : "+=") + 5, y: (randInt(2) > 0 ? "-=" : "+=") + 5, }, { // Slide Greedent diagonally duration: slideDelay, ease: "Cubic.easeOut", y: "-=75", x: "+=65", scale: 1, }, { // Bounce at the end duration: 300, ease: "Cubic.easeOut", yoyo: true, y: "-=20", loop: 1, }, ], }); } function doGreedentEatBerries() { const greedentSprites = globalScene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); let index = 1; globalScene.tweens.add({ targets: greedentSprites, duration: 150, ease: "Cubic.easeOut", yoyo: true, y: "-=8", loop: 5, onStart: () => { globalScene.playSound("battle_anims/PRSFX- Bug Bite"); }, onLoop: () => { if (index % 2 === 0) { globalScene.playSound("battle_anims/PRSFX- Bug Bite"); } index++; }, }); } /** * @param isEat Default false. Will "create" pile when false, and remove pile when true. */ function doBerrySpritePile(isEat = false) { const berryAddDelay = 150; let animationOrder = [ "starf", "sitrus", "lansat", "salac", "apicot", "enigma", "liechi", "ganlon", "lum", "petaya", "leppa", ]; if (isEat) { animationOrder = animationOrder.reverse(); } const encounter = globalScene.currentBattle.mysteryEncounter!; animationOrder.forEach((berry, i) => { const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey?.includes(berry)); let sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite; const sprites = encounter.introVisuals?.getSpriteAtIndex(introVisualsIndex); if (sprites) { sprite = sprites[0]; tintSprite = sprites[1]; } globalScene.time.delayedCall(berryAddDelay * i + 400, () => { if (sprite) { sprite.setVisible(!isEat); } if (tintSprite) { tintSprite.setVisible(!isEat); } // Animate Petaya berry falling off the pile if (berry === "petaya" && sprite && tintSprite && !isEat) { globalScene.time.delayedCall(200, () => { doBerryBounce([sprite, tintSprite], 30, 500); }); } }); }); } function doBerryBounce(berrySprites: Phaser.GameObjects.Sprite[], yd: number, baseBounceDuration: number) { let bouncePower = 1; let bounceYOffset = yd; const doBounce = () => { globalScene.tweens.add({ targets: berrySprites, y: "+=" + bounceYOffset, x: { value: "+=" + bouncePower * bouncePower * 10, ease: "Linear" }, duration: bouncePower * baseBounceDuration, ease: "Cubic.easeIn", onComplete: () => { bouncePower = bouncePower > 0.01 ? bouncePower * 0.5 : 0; if (bouncePower) { bounceYOffset = bounceYOffset * bouncePower; globalScene.tweens.add({ targets: berrySprites, y: "-=" + bounceYOffset, x: { value: "+=" + bouncePower * bouncePower * 10, ease: "Linear" }, duration: bouncePower * baseBounceDuration, ease: "Cubic.easeOut", onComplete: () => doBounce(), }); } }, }); }; doBounce(); }