mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-10-17 18:47:17 +02:00
* Add enum for hit check result Co-authored-by: innerthunder <brandonerickson98@gmail.com> * Refactor parameter list for pokemon#getBaseDamage and pokemon#getAttackDamage * Rewrite move phase Co-authored-by: innerthunder <brandonerickson98@gmail.com> * Update tests to reflect move effect phase changes Co-authored-by: innerthunder <brandonerickson98@gmail.com> * Fix pluck / bug bite Co-authored-by: innerthunder <brandonerickson98@gmail.com> * Fix reviver seed ohko, remove leftover dead code Co-authored-by: innerthunder <brandonerickson98@gmail.com> * Cleanup jsdoc comments * Remove hitsSubstitute check from postDefend abilities * Fix improper i18nkey in moveEffectPhase#applyToTargets * Cleanup comments * Fix type issue with substitute test * Move MYSTERY_ENCOUNTER_WAVES to constants.ts * Update linkcode in damageparams to use proper tsdoc syntax --------- Co-authored-by: innerthunder <brandonerickson98@gmail.com>
368 lines
13 KiB
TypeScript
368 lines
13 KiB
TypeScript
import { BattlerIndex } from "#app/battle";
|
|
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, PokemonMove } from "#app/field/pokemon";
|
|
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
|
import { modifierTypes } from "#app/modifier/modifier-type";
|
|
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
|
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
|
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 { Biome } from "#enums/biome";
|
|
import { EncounterAnim } from "#enums/encounter-anims";
|
|
import { Moves } from "#enums/moves";
|
|
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 { Species } from "#enums/species";
|
|
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 = [
|
|
Biome.VOLCANO,
|
|
Biome.BEACH,
|
|
Biome.ISLAND,
|
|
Biome.WASTELAND,
|
|
Biome.MOUNTAIN,
|
|
Biome.BADLANDS,
|
|
Biome.DESERT,
|
|
];
|
|
|
|
// Electric form
|
|
const POM_POM_STYLE_BIOMES = [
|
|
Biome.CONSTRUCTION_SITE,
|
|
Biome.POWER_PLANT,
|
|
Biome.FACTORY,
|
|
Biome.LABORATORY,
|
|
Biome.SLUM,
|
|
Biome.METROPOLIS,
|
|
Biome.DOJO,
|
|
];
|
|
|
|
// Psychic form
|
|
const PAU_STYLE_BIOMES = [
|
|
Biome.JUNGLE,
|
|
Biome.FAIRY_CAVE,
|
|
Biome.MEADOW,
|
|
Biome.PLAINS,
|
|
Biome.GRASS,
|
|
Biome.TALL_GRASS,
|
|
Biome.FOREST,
|
|
];
|
|
|
|
// Ghost form
|
|
const SENSU_STYLE_BIOMES = [
|
|
Biome.RUINS,
|
|
Biome.SWAMP,
|
|
Biome.CAVE,
|
|
Biome.ABYSS,
|
|
Biome.GRAVEYARD,
|
|
Biome.LAKE,
|
|
Biome.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(Species.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 === Moves.REVELATION_DANCE)) {
|
|
if (enemyPokemon.moveset.length < 4) {
|
|
enemyPokemon.moveset.push(new PokemonMove(Moves.REVELATION_DANCE));
|
|
} else {
|
|
enemyPokemon.moveset[0] = new PokemonMove(Moves.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.unshiftPhase(
|
|
new StatStageChangePhase(
|
|
pokemon.getBattlerIndex(),
|
|
true,
|
|
[Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF],
|
|
1,
|
|
),
|
|
);
|
|
},
|
|
},
|
|
],
|
|
};
|
|
encounter.enemyPartyConfigs = [config];
|
|
encounter.misc = {
|
|
oricorioData,
|
|
};
|
|
|
|
encounter.setDialogueToken("oricorioName", getPokemonSpecies(Species.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(Moves.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.unshiftPhase(
|
|
new LearnMovePhase(globalScene.getPlayerParty().indexOf(pokemon), Moves.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();
|
|
},
|
|
});
|
|
});
|
|
}
|