mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-03-13 05:15:17 +00:00
582 lines
18 KiB
TypeScript
582 lines
18 KiB
TypeScript
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();
|
|
}
|