pokerogue/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts
Sirz Benjie 1029afcdbf
[Refactor] Remove circular dependencies (part 4) (#5964)
* Add abilityAttr.is methods

* [WIP] move modifier stuff around

* Untangle circular deps from modifiers

* Move unlockables to own file

* Untangle all circular deps outside of MEs

* Move constants in MEs to their own files

* Re-add missing import to battle.ts

* Add necessary overload for getTag

* Add missing type import in weather.ts

* Init modifier types and pools in loading-scene

* Remove stray commented code

* Apply kev's suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-06-11 19:42:04 -07:00

368 lines
13 KiB
TypeScript

import { BattlerIndex } from "#enums/battler-index";
import { globalScene } from "#app/global-scene";
import { EncounterBattleAnim } from "#app/data/battle-anims";
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 { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import {
initBattleWithEnemyConfig,
leaveEncounterWithoutBattle,
selectPokemonForOption,
setEncounterRewards,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import {
catchPokemon,
getEncounterPokemonLevelForWave,
STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { TrainerSlot } from "#enums/trainer-slot";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon } from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { modifierTypes } from "#app/data/data-lists";
import PokemonData from "#app/system/pokemon-data";
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id";
import { EncounterAnim } from "#enums/encounter-anims";
import { MoveId } from "#enums/move-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokeballType } from "#enums/pokeball";
import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import i18next from "i18next";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/dancingLessons";
// Fire form
const BAILE_STYLE_BIOMES = [
BiomeId.VOLCANO,
BiomeId.BEACH,
BiomeId.ISLAND,
BiomeId.WASTELAND,
BiomeId.MOUNTAIN,
BiomeId.BADLANDS,
BiomeId.DESERT,
];
// Electric form
const POM_POM_STYLE_BIOMES = [
BiomeId.CONSTRUCTION_SITE,
BiomeId.POWER_PLANT,
BiomeId.FACTORY,
BiomeId.LABORATORY,
BiomeId.SLUM,
BiomeId.METROPOLIS,
BiomeId.DOJO,
];
// Psychic form
const PAU_STYLE_BIOMES = [
BiomeId.JUNGLE,
BiomeId.FAIRY_CAVE,
BiomeId.MEADOW,
BiomeId.PLAINS,
BiomeId.GRASS,
BiomeId.TALL_GRASS,
BiomeId.FOREST,
];
// Ghost form
const SENSU_STYLE_BIOMES = [
BiomeId.RUINS,
BiomeId.SWAMP,
BiomeId.CAVE,
BiomeId.ABYSS,
BiomeId.GRAVEYARD,
BiomeId.LAKE,
BiomeId.TEMPLE,
];
/**
* Dancing Lessons encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3823 | GitHub Issue #3823}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(
MysteryEncounterType.DANCING_LESSONS,
)
.withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([]) // Uses a real Pokemon sprite instead of ME Intro Visuals
.withAnimations(EncounterAnim.DANCE)
.withHideWildIntroMessage(true)
.withAutoHideIntroVisuals(false)
.withCatchAllowed(true)
.withFleeAllowed(false)
.withOnVisualsStart(() => {
const oricorio = globalScene.getEnemyPokemon()!;
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, oricorio, globalScene.getPlayerPokemon()!);
danceAnim.play(false, () => {
if (oricorio.shiny) {
oricorio.sparkle();
}
});
return true;
})
.withIntroDialogue([
{
text: `${namespace}:intro`,
},
])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`)
.withOnInit(() => {
const encounter = globalScene.currentBattle.mysteryEncounter!;
const species = getPokemonSpecies(SpeciesId.ORICORIO);
const level = getEncounterPokemonLevelForWave(STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
const enemyPokemon = new EnemyPokemon(species, level, TrainerSlot.NONE, false);
if (!enemyPokemon.moveset.some(m => m && m.getMove().id === MoveId.REVELATION_DANCE)) {
if (enemyPokemon.moveset.length < 4) {
enemyPokemon.moveset.push(new PokemonMove(MoveId.REVELATION_DANCE));
} else {
enemyPokemon.moveset[0] = new PokemonMove(MoveId.REVELATION_DANCE);
}
}
// Set the form index based on the biome
// Defaults to Baile style if somehow nothing matches
const currentBiome = globalScene.arena.biomeType;
if (BAILE_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 0;
} else if (POM_POM_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 1;
} else if (PAU_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 2;
} else if (SENSU_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 3;
} else {
enemyPokemon.formIndex = 0;
}
const oricorioData = new PokemonData(enemyPokemon);
const oricorio = globalScene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation)
for (const enemyPokemon of globalScene.getEnemyParty()) {
enemyPokemon.leaveField(true, true, true);
}
globalScene.currentBattle.enemyParty = [oricorio];
globalScene.field.add(oricorio);
// Spawns on offscreen field
oricorio.x -= 300;
encounter.loadAssets.push(oricorio.loadAssets());
const config: EnemyPartyConfig = {
pokemonConfigs: [
{
species: species,
dataSource: oricorioData,
isBoss: true,
// Gets +1 to all stats except SPD on battle start
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(`${namespace}:option.1.boss_enraged`);
globalScene.phaseManager.unshiftNew(
"StatStageChangePhase",
pokemon.getBattlerIndex(),
true,
[Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF],
1,
);
},
},
],
};
encounter.enemyPartyConfigs = [config];
encounter.misc = {
oricorioData,
};
encounter.setDialogueToken("oricorioName", getPokemonSpecies(SpeciesId.ORICORIO).getName());
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!;
encounter.startOfBattleEffects.push({
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.REVELATION_DANCE),
ignorePp: true,
});
await hideOricorioPokemon();
setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.BATON],
fillRemaining: true,
});
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`,
},
],
})
.withPreOptionPhase(async () => {
// Learn its Dance
const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
globalScene.phaseManager.unshiftNew(
"LearnMovePhase",
globalScene.getPlayerParty().indexOf(pokemon),
MoveId.REVELATION_DANCE,
);
// Play animation again to "learn" the dance
const danceAnim = new EncounterBattleAnim(
EncounterAnim.DANCE,
globalScene.getEnemyPokemon()!,
globalScene.getPlayerPokemon(),
);
danceAnim.play();
};
return selectPokemonForOption(onPokemonSelected);
})
.withOptionPhase(async () => {
// Learn its Dance
await hideOricorioPokemon();
leaveEncounterWithoutBattle(true);
})
.build(),
)
.withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES, true)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,
disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`,
secondOptionPrompt: `${namespace}:option.3.select_prompt`,
selected: [
{
text: `${namespace}:option.3.selected`,
},
],
})
.withPreOptionPhase(async () => {
// Open menu for selecting pokemon with a Dancing move
const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Return the options for nature selection
return pokemon.moveset
.filter(move => move && DANCING_MOVES.includes(move.getMove().id))
.map((move: PokemonMove) => {
const option: OptionSelectItem = {
label: move.getName(),
handler: () => {
// Pokemon and second option selected
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
encounter.setDialogueToken("selectedMove", move.getName());
encounter.misc.selectedMove = move;
return true;
},
};
return option;
});
};
// Only challenge legal/unfainted Pokemon that have a Dancing move can be selected
const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected
if (!pokemon.isAllowedInBattle()) {
return (
i18next.t("partyUiHandler:cantBeUsed", {
pokemonName: pokemon.getNameToRender(),
}) ?? null
);
}
const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(pokemon);
if (!meetsReqs) {
return getEncounterText(`${namespace}:invalid_selection`) ?? null;
}
return null;
};
return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter);
})
.withOptionPhase(async () => {
// Show the Oricorio a dance, and recruit it
const encounter = globalScene.currentBattle.mysteryEncounter!;
const oricorio = encounter.misc.oricorioData.toPokemon();
oricorio.passive = true;
// Ensure the Oricorio's moveset gains the Dance move the player used
const move = encounter.misc.selectedMove?.getMove().id;
if (!oricorio.moveset.some(m => m.getMove().id === move)) {
if (oricorio.moveset.length < 4) {
oricorio.moveset.push(new PokemonMove(move));
} else {
oricorio.moveset[3] = new PokemonMove(move);
}
}
await hideOricorioPokemon();
await catchPokemon(oricorio, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(true);
})
.build(),
)
.build();
function hideOricorioPokemon() {
return new Promise<void>(resolve => {
const oricorioSprite = globalScene.getEnemyParty()[0];
globalScene.tweens.add({
targets: oricorioSprite,
x: "+=16",
y: "-=16",
alpha: 0,
ease: "Sine.easeInOut",
duration: 750,
onComplete: () => {
globalScene.field.remove(oricorioSprite, true);
resolve();
},
});
});
}