pokerogue/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts
Bertie690 c0da686ba0
[Dev] Migrated to Biome 2.2.3, added more rules (#6259)
* Added more biome rules

* Fixes

* Added a few more rules

* Added global phaser to biome

* Fix tpyo

* Updated biome to 2.1.4; improved docs on linting/localization; added vcs support

Also added `.build` to gitignore cuz reasons

* Fixed tpyo

* dd

* Applied linter fixes

* Partially fixed some private property issues

* Upgraded to Biome 2.2.0; added `operatorLinebreak` and a few new rules

* Moved operator linebreaks before lines

* Applied kev's suggestions

* Update biome.jsonc

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

* added like all the rules and then some

* modify biome.jsonc

* apply biome formatting

* Reverted changes to balance folder

* fixed stuff

* Fixed biome stripping trailing globstars from everything

* made `noInvertedElse` an error rule

* Add & apply fixes for `useExplicitLengthCheck`, `useAtIndex` and `noNonNullAssertedOptionalChain`

* Bumped biome to 2.2.3

* Fixed a few syntax errors

* Removed trailing globstars since biome actually fixed their shit

* Final clean up

* foobarbaz

* Fixed remaining issues

* Fixed a few errors in SSUI

* fixed rounding issue

* Fixed test to not round funky

* Fixed biome false positive for vitest hooks

* Apply biome:all

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-09-08 10:35:18 -05:00

256 lines
9.9 KiB
TypeScript

import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene";
import { NON_LEGEND_PARADOX_POKEMON, NON_LEGEND_ULTRA_BEASTS } from "#balance/special-species-groups";
import { speciesStarterCosts } from "#balance/starters";
import type { PokemonSpecies } from "#data/pokemon-species";
import { AbilityId } from "#enums/ability-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 type { EnemyPokemon } from "#field/pokemon";
import { PlayerPokemon } from "#field/pokemon";
import { showEncounterDialogue } from "#mystery-encounters/encounter-dialogue-utils";
import {
leaveEncounterWithoutBattle,
transitionMysteryEncounterIntroVisuals,
updatePlayerMoney,
} from "#mystery-encounters/encounter-phase-utils";
import {
catchPokemon,
getRandomSpeciesByStarterCost,
getSpriteKeysFromPokemon,
} from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import { PokemonData } from "#system/pokemon-data";
import { isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/thePokemonSalesman";
const MAX_POKEMON_PRICE_MULTIPLIER = 4;
/** Odds of shiny magikarp will be 1/value */
const SHINY_MAGIKARP_WEIGHT = 100;
/** Odds of event sale will be value/100 */
const EVENT_THRESHOLD = 50;
/**
* Pokemon Salesman encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3799 | GitHub Issue #3799}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(
MysteryEncounterType.THE_POKEMON_SALESMAN,
)
.withEncounterTier(MysteryEncounterTier.ULTRA)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withSceneRequirement(new MoneyRequirement(0, MAX_POKEMON_PRICE_MULTIPLIER)) // Some costs may not be as significant, this is the max you'd pay
.withAutoHideIntroVisuals(false)
.withIntroSpriteConfigs([
{
spriteKey: "pokemon_salesman",
fileRoot: "mystery-encounters",
hasShadow: true,
},
])
.withIntroDialogue([
{
text: `${namespace}:intro`,
},
{
text: `${namespace}:introDialogue`,
speaker: `${namespace}:speaker`,
},
])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`)
.withOnInit(() => {
const encounter = globalScene.currentBattle.mysteryEncounter!;
let species = getSalesmanSpeciesOffer();
let tries = 0;
// Reroll any species that don't have HAs
while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) && tries < 5) {
species = getSalesmanSpeciesOffer();
tries++;
}
const r = randSeedInt(SHINY_MAGIKARP_WEIGHT);
const validEventEncounters = timedEventManager
.getEventEncounters()
.filter(
s =>
!getPokemonSpecies(s.species).legendary
&& !getPokemonSpecies(s.species).subLegendary
&& !getPokemonSpecies(s.species).mythical
&& !NON_LEGEND_PARADOX_POKEMON.includes(s.species)
&& !NON_LEGEND_ULTRA_BEASTS.includes(s.species),
);
let pokemon: PlayerPokemon;
/**
* Mon is determined as follows:
* If you roll the 1% for Shiny Magikarp, you get Magikarp with a random variant
* If an event with more than 1 valid event encounter species is active, you have 20% chance to get one of those
* If the rolled species has no HA, and there are valid event encounters, you will get one of those
* If the rolled species has no HA and there are no valid event encounters, you will get Shiny Magikarp
* Mons rolled from the event encounter pool get 3 extra shiny rolls
*/
if (
r === 0
|| ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE)
&& validEventEncounters.length === 0)
) {
// If you roll 1%, give shiny Magikarp with random variant
species = getPokemonSpecies(SpeciesId.MAGIKARP);
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
} else if (
validEventEncounters.length > 0
&& (r <= EVENT_THRESHOLD || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE)
) {
tries = 0;
do {
// If you roll 20%, give event encounter with 3 extra shiny rolls and its HA, if it has one
const enc = randSeedItem(validEventEncounters);
species = getPokemonSpecies(enc.species);
pokemon = new PlayerPokemon(
species,
5,
species.abilityHidden === AbilityId.NONE ? undefined : 2,
enc.formIndex,
);
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
if (pokemon.shiny || pokemon.abilityIndex === 2) {
break;
}
tries++;
} while (tries < 6);
if (!pokemon.shiny && pokemon.abilityIndex !== 2) {
// If, after 6 tries, you STILL somehow don't have an HA or shiny mon, pick from only the event mons that have an HA.
if (validEventEncounters.some(s => !!getPokemonSpecies(s.species).abilityHidden)) {
validEventEncounters.filter(s => !!getPokemonSpecies(s.species).abilityHidden);
const enc = randSeedItem(validEventEncounters);
species = getPokemonSpecies(enc.species);
pokemon = new PlayerPokemon(species, 5, 2, enc.formIndex);
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
} else {
// If there's, and this would never happen, no eligible event encounters with a hidden ability, just do Magikarp
species = getPokemonSpecies(SpeciesId.MAGIKARP);
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
}
}
} else {
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex);
}
pokemon.generateAndPopulateMoveset();
const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(pokemon);
encounter.spriteConfigs.push({
spriteKey,
fileRoot,
hasShadow: true,
repeat: true,
isPokemon: true,
isShiny: pokemon.shiny,
variant: pokemon.variant,
});
const starterTier = speciesStarterCosts[species.speciesId];
// Prices decrease by starter tier less than 5, but only reduces cost by half at max
let priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER * (Math.max(starterTier, 2.5) / 5);
if (pokemon.shiny) {
// Always max price for shiny (flip HA back to normal), and add special messaging
priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER;
pokemon.abilityIndex = 0;
encounter.dialogue.encounterOptionsDialogue!.description = `${namespace}:descriptionShiny`;
encounter.options[0].dialogue!.buttonTooltip = `${namespace}:option.1.tooltipShiny`;
}
const price = globalScene.getWaveMoneyAmount(priceMultiplier);
encounter.setDialogueToken("purchasePokemon", pokemon.getNameToRender());
encounter.setDialogueToken("price", price.toString());
encounter.misc = {
price,
pokemon,
};
pokemon.calculateStats();
return true;
})
.withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withHasDexProgress(true)
.withSceneMoneyRequirement(0, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2
.withDialogue({
buttonLabel: `${namespace}:option.1.label`,
buttonTooltip: `${namespace}:option.1.tooltip`,
selected: [
{
text: `${namespace}:option.1.selectedMessage`,
},
],
})
.withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!;
const price = encounter.misc.price;
const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon;
// Update money
updatePlayerMoney(-price, true, false);
// Show dialogue
await showEncounterDialogue(`${namespace}:option.1.selectedDialogue`, `${namespace}:speaker`);
await transitionMysteryEncounterIntroVisuals();
// "Catch" purchased pokemon
const data = new PokemonData(purchasedPokemon);
data.player = false;
await catchPokemon(data.toPokemon() as EnemyPokemon, null, PokeballType.POKEBALL, true, true);
leaveEncounterWithoutBattle(true);
})
.build(),
)
.withSimpleOption(
{
buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`,
selected: [
{
text: `${namespace}:option.2.selected`,
},
],
},
async () => {
// Leave encounter with no rewards or exp
leaveEncounterWithoutBattle(true);
return true;
},
)
.build();
/**
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
*/
export function getSalesmanSpeciesOffer(): PokemonSpecies {
return getPokemonSpecies(
getRandomSpeciesByStarterCost([0, 5], NON_LEGEND_PARADOX_POKEMON, undefined, false, false, false),
);
}