mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-04 07:22:19 +02:00
* 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>
244 lines
9.3 KiB
TypeScript
244 lines
9.3 KiB
TypeScript
import {
|
|
generateModifierType,
|
|
leaveEncounterWithoutBattle,
|
|
selectPokemonForOption,
|
|
setEncounterExp,
|
|
updatePlayerMoney,
|
|
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
|
import type { PlayerPokemon } from "#app/field/pokemon";
|
|
import type Pokemon from "#app/field/pokemon";
|
|
import { modifierTypes } from "#app/data/data-lists";
|
|
import { randSeedInt } from "#app/utils/common";
|
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|
import { SpeciesId } from "#enums/species-id";
|
|
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 { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
|
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
|
import {
|
|
applyDamageToPokemon,
|
|
applyModifierTypeToPlayerPokemon,
|
|
isPokemonValidForEncounterOptionSelection,
|
|
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
|
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
|
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
|
import type { Nature } from "#enums/nature";
|
|
import { getNatureName } from "#app/data/nature";
|
|
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
|
import i18next from "i18next";
|
|
|
|
/** the i18n namespace for this encounter */
|
|
const namespace = "mysteryEncounters/shadyVitaminDealer";
|
|
|
|
const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5;
|
|
const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 5;
|
|
|
|
/**
|
|
* Shady Vitamin Dealer encounter.
|
|
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3798 | GitHub Issue #3798}
|
|
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
|
|
*/
|
|
export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(
|
|
MysteryEncounterType.SHADY_VITAMIN_DEALER,
|
|
)
|
|
.withEncounterTier(MysteryEncounterTier.COMMON)
|
|
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
|
.withSceneRequirement(new MoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)) // Must have the money for at least the cheap deal
|
|
.withPrimaryPokemonHealthRatioRequirement([0.51, 1]) // At least 1 Pokemon must have above half HP
|
|
.withIntroSpriteConfigs([
|
|
{
|
|
spriteKey: SpeciesId.KROOKODILE.toString(),
|
|
fileRoot: "pokemon",
|
|
hasShadow: true,
|
|
repeat: true,
|
|
x: 12,
|
|
y: -5,
|
|
yShadow: -5,
|
|
},
|
|
{
|
|
spriteKey: "shady_vitamin_dealer",
|
|
fileRoot: "mystery-encounters",
|
|
hasShadow: true,
|
|
x: -12,
|
|
y: 3,
|
|
yShadow: 3,
|
|
},
|
|
])
|
|
.withIntroDialogue([
|
|
{
|
|
text: `${namespace}:intro`,
|
|
},
|
|
{
|
|
text: `${namespace}:intro_dialogue`,
|
|
speaker: `${namespace}:speaker`,
|
|
},
|
|
])
|
|
.setLocalizationKey(`${namespace}`)
|
|
.withTitle(`${namespace}:title`)
|
|
.withDescription(`${namespace}:description`)
|
|
.withQuery(`${namespace}:query`)
|
|
.withOption(
|
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
|
.withSceneMoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)
|
|
.withDialogue({
|
|
buttonLabel: `${namespace}:option.1.label`,
|
|
buttonTooltip: `${namespace}:option.1.tooltip`,
|
|
selected: [
|
|
{
|
|
text: `${namespace}:option.selected`,
|
|
},
|
|
],
|
|
})
|
|
.withPreOptionPhase(async (): Promise<boolean> => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
|
// Update money
|
|
updatePlayerMoney(-(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
|
|
// Calculate modifiers and dialogue tokens
|
|
const modifiers = [
|
|
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!,
|
|
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!,
|
|
];
|
|
encounter.setDialogueToken("boost1", modifiers[0].name);
|
|
encounter.setDialogueToken("boost2", modifiers[1].name);
|
|
encounter.misc = {
|
|
chosenPokemon: pokemon,
|
|
modifiers: modifiers,
|
|
};
|
|
};
|
|
|
|
// Only Pokemon that can gain benefits are above half HP with no status
|
|
const selectableFilter = (pokemon: Pokemon) => {
|
|
// If pokemon meets primary pokemon reqs, it can be selected
|
|
if (!pokemon.isAllowedInChallenge()) {
|
|
return (
|
|
i18next.t("partyUiHandler:cantBeUsed", {
|
|
pokemonName: pokemon.getNameToRender(),
|
|
}) ?? null
|
|
);
|
|
}
|
|
if (!encounter.pokemonMeetsPrimaryRequirements(pokemon)) {
|
|
return getEncounterText(`${namespace}:invalid_selection`) ?? null;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter);
|
|
})
|
|
.withOptionPhase(async () => {
|
|
// Choose Cheap Option
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const chosenPokemon = encounter.misc.chosenPokemon;
|
|
const modifiers = encounter.misc.modifiers;
|
|
|
|
for (const modType of modifiers) {
|
|
await applyModifierTypeToPlayerPokemon(chosenPokemon, modType);
|
|
}
|
|
|
|
leaveEncounterWithoutBattle(true);
|
|
})
|
|
.withPostOptionPhase(async () => {
|
|
// Damage and status applied after dealer leaves (to make thematic sense)
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const chosenPokemon = encounter.misc.chosenPokemon as PlayerPokemon;
|
|
|
|
// Pokemon takes half max HP damage and nature is randomized (does not update dex)
|
|
applyDamageToPokemon(chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 2));
|
|
|
|
const currentNature = chosenPokemon.nature;
|
|
let newNature = randSeedInt(25) as Nature;
|
|
while (newNature === currentNature) {
|
|
newNature = randSeedInt(25) as Nature;
|
|
}
|
|
|
|
chosenPokemon.setCustomNature(newNature);
|
|
encounter.setDialogueToken("newNature", getNatureName(newNature));
|
|
queueEncounterMessage(`${namespace}:cheap_side_effects`);
|
|
setEncounterExp([chosenPokemon.id], 100);
|
|
await chosenPokemon.updateInfo();
|
|
})
|
|
.build(),
|
|
)
|
|
.withOption(
|
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
|
.withSceneMoneyRequirement(0, VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER)
|
|
.withDialogue({
|
|
buttonLabel: `${namespace}:option.2.label`,
|
|
buttonTooltip: `${namespace}:option.2.tooltip`,
|
|
selected: [
|
|
{
|
|
text: `${namespace}:option.selected`,
|
|
},
|
|
],
|
|
})
|
|
.withPreOptionPhase(async (): Promise<boolean> => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
|
// Update money
|
|
updatePlayerMoney(-(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney);
|
|
// Calculate modifiers and dialogue tokens
|
|
const modifiers = [
|
|
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!,
|
|
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!,
|
|
];
|
|
encounter.setDialogueToken("boost1", modifiers[0].name);
|
|
encounter.setDialogueToken("boost2", modifiers[1].name);
|
|
encounter.misc = {
|
|
chosenPokemon: pokemon,
|
|
modifiers: modifiers,
|
|
};
|
|
};
|
|
|
|
// Only Pokemon that can gain benefits are unfainted
|
|
const selectableFilter = (pokemon: Pokemon) => {
|
|
return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`);
|
|
};
|
|
|
|
return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter);
|
|
})
|
|
.withOptionPhase(async () => {
|
|
// Choose Expensive Option
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const chosenPokemon = encounter.misc.chosenPokemon;
|
|
const modifiers = encounter.misc.modifiers;
|
|
|
|
for (const modType of modifiers) {
|
|
await applyModifierTypeToPlayerPokemon(chosenPokemon, modType);
|
|
}
|
|
|
|
leaveEncounterWithoutBattle(true);
|
|
})
|
|
.withPostOptionPhase(async () => {
|
|
// Status applied after dealer leaves (to make thematic sense)
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const chosenPokemon = encounter.misc.chosenPokemon;
|
|
|
|
queueEncounterMessage(`${namespace}:no_bad_effects`);
|
|
setEncounterExp([chosenPokemon.id], 100);
|
|
|
|
await chosenPokemon.updateInfo();
|
|
})
|
|
.build(),
|
|
)
|
|
.withSimpleOption(
|
|
{
|
|
buttonLabel: `${namespace}:option.3.label`,
|
|
buttonTooltip: `${namespace}:option.3.tooltip`,
|
|
selected: [
|
|
{
|
|
text: `${namespace}:option.3.selected`,
|
|
speaker: `${namespace}:speaker`,
|
|
},
|
|
],
|
|
},
|
|
async () => {
|
|
// Leave encounter with no rewards or exp
|
|
leaveEncounterWithoutBattle(true);
|
|
return true;
|
|
},
|
|
)
|
|
.build();
|