mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-12-15 06:15:20 +01:00
[Refactor] Refactor ME mon generation and event encounters, add to Safari Zone & GTS (#6695)
* Refactor event encounters * Fix safari test * Apply biome fixes * Cleanup, 100% event chance for WT * Fix Safari Zone * Fix shiny chance * Run biome * Apply suggestions from code review Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Documentation for randomEncParams * other * Updated doc comments on interface to be less jank * >'less janky'>look inside>linting error * Update encounter-phase-utils.ts doc comment * Update encounter-phase-utils.ts * Update src/data/mystery-encounters/encounters/global-trade-system-encounter.ts Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * thing --------- Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: damocleas <damocleas25@gmail.com>
This commit is contained in:
parent
e5154850c6
commit
79148452e9
98
src/@types/pokemon-common.ts
Normal file
98
src/@types/pokemon-common.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import type { PokemonSpecies, PokemonSpeciesFilter } from "#data/pokemon-species";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import type { BooleanHolder } from "#utils/common";
|
||||
|
||||
/**
|
||||
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
|
||||
*/
|
||||
export type SerializedSpeciesForm = {
|
||||
id: SpeciesId;
|
||||
formIdx: number;
|
||||
};
|
||||
|
||||
export interface RandomEncounterParams {
|
||||
/** The level of the mon */
|
||||
level: number;
|
||||
|
||||
/** A custom function used to return the {@linkcode PokemonSpecies} to generate */
|
||||
speciesFunction?: () => PokemonSpecies;
|
||||
|
||||
/**
|
||||
* Whether the Pokemon should be a Boss.
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
isBoss?: boolean;
|
||||
|
||||
/**
|
||||
* Whether Sub-legendaries can be encountered, mainly for event encounters
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
includeSubLegendary?: boolean;
|
||||
|
||||
/**
|
||||
* Whether Legendaries can be encountered
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
includeLegendary?: boolean;
|
||||
|
||||
/**
|
||||
* Whether Mythicals can be encountered
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
includeMythical?: boolean;
|
||||
|
||||
/**
|
||||
* The chance out of 100 to pick an event encounter
|
||||
* @defaultValue `50`
|
||||
*/
|
||||
eventChance?: number;
|
||||
|
||||
/**
|
||||
* Number of rerolls for Hidden Ability (HA) that should be attempted
|
||||
* @defaultValue `0`
|
||||
*/
|
||||
hiddenRerolls?: number;
|
||||
|
||||
/**
|
||||
* Number of rerolls for shininess/variants that should be attempted
|
||||
* @defaultValue `0`
|
||||
*/
|
||||
shinyRerolls?: number;
|
||||
|
||||
/**
|
||||
* Number of extra HA rerolls for event mons
|
||||
* @defaultValue `0`
|
||||
*/
|
||||
eventHiddenRerolls?: number;
|
||||
|
||||
/**
|
||||
* Number of extra shiny rerolls for event mons
|
||||
* @defaultValue `0`
|
||||
*/
|
||||
eventShinyRerolls?: number;
|
||||
|
||||
/**
|
||||
* The overridden HA chance, defaults to base
|
||||
*/
|
||||
hiddenAbilityChance?: number;
|
||||
|
||||
/**
|
||||
* The overridden shiny chance, defaults to base
|
||||
*/
|
||||
shinyChance?: number;
|
||||
|
||||
/**
|
||||
* The max shiny threshold after modifiers are applied. Values below 1 mean no maximum
|
||||
* @defaultValue `0` (no maximum)
|
||||
*/
|
||||
maxShinyChance?: number;
|
||||
|
||||
/**
|
||||
* An optional filter for eligible mons, applied to the event encounter pool.
|
||||
* If omitted, no filter will be applied.
|
||||
*/
|
||||
speciesFilter?: PokemonSpeciesFilter;
|
||||
|
||||
/** An optional {@linkcode BooleanHolder} used to let the caller know if it pulled from an event. */
|
||||
isEventEncounter?: BooleanHolder;
|
||||
}
|
||||
@ -18,7 +18,7 @@ import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils
|
||||
import {
|
||||
generateModifierType,
|
||||
generateModifierTypeOption,
|
||||
getRandomEncounterSpecies,
|
||||
getRandomEncounterPokemon,
|
||||
initBattleWithEnemyConfig,
|
||||
leaveEncounterWithoutBattle,
|
||||
setEncounterExp,
|
||||
@ -66,7 +66,12 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
|
||||
|
||||
// Calculate boss mon
|
||||
const level = getEncounterPokemonLevelForWave(STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
|
||||
const bossPokemon = getRandomEncounterSpecies(level, true);
|
||||
const bossPokemon = getRandomEncounterPokemon({
|
||||
level,
|
||||
isBoss: true,
|
||||
eventShinyRerolls: 2,
|
||||
eventHiddenRerolls: 1,
|
||||
});
|
||||
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
|
||||
const config: EnemyPartyConfig = {
|
||||
pokemonConfigs: [
|
||||
|
||||
@ -12,7 +12,7 @@ import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "
|
||||
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
|
||||
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
|
||||
import {
|
||||
getRandomEncounterSpecies,
|
||||
getRandomEncounterPokemon,
|
||||
initBattleWithEnemyConfig,
|
||||
leaveEncounterWithoutBattle,
|
||||
setEncounterExp,
|
||||
@ -58,7 +58,12 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder.
|
||||
|
||||
// Calculate boss mon
|
||||
const level = getEncounterPokemonLevelForWave(STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER);
|
||||
const bossPokemon = getRandomEncounterSpecies(level, true);
|
||||
const bossPokemon = getRandomEncounterPokemon({
|
||||
level,
|
||||
isBoss: true,
|
||||
eventShinyRerolls: 2,
|
||||
eventHiddenRerolls: 1,
|
||||
});
|
||||
encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender());
|
||||
const config: EnemyPartyConfig = {
|
||||
pokemonConfigs: [
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { timedEventManager } from "#app/global-event-manager";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { allSpecies } from "#data/data-lists";
|
||||
import { Gender, getGenderSymbol } from "#data/gender";
|
||||
@ -20,17 +19,13 @@ import { doShinySparkleAnim } from "#field/anims";
|
||||
import type { PlayerPokemon, Pokemon } from "#field/pokemon";
|
||||
import { EnemyPokemon } from "#field/pokemon";
|
||||
import type { PokemonHeldItemModifier } from "#modifiers/modifier";
|
||||
import {
|
||||
HiddenAbilityRateBoosterModifier,
|
||||
PokemonFormChangeItemModifier,
|
||||
ShinyRateBoosterModifier,
|
||||
SpeciesStatBoosterModifier,
|
||||
} from "#modifiers/modifier";
|
||||
import { PokemonFormChangeItemModifier, SpeciesStatBoosterModifier } from "#modifiers/modifier";
|
||||
import type { ModifierTypeOption } from "#modifiers/modifier-type";
|
||||
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#modifiers/modifier-type";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
|
||||
import {
|
||||
getRandomEncounterPokemon,
|
||||
leaveEncounterWithoutBattle,
|
||||
selectPokemonForOption,
|
||||
setEncounterRewards,
|
||||
@ -43,7 +38,7 @@ import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requ
|
||||
import { PokemonData } from "#system/pokemon-data";
|
||||
import { MusicPreference } from "#system/settings";
|
||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
import { NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common";
|
||||
import { randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common";
|
||||
import { getEnumKeys } from "#utils/enums";
|
||||
import { getRandomLocaleEntry } from "#utils/i18n";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
@ -58,6 +53,8 @@ const WONDER_TRADE_SHINY_CHANCE = 512;
|
||||
/** Max shiny chance of 4096/65536 -> 1/16 odds. */
|
||||
const MAX_WONDER_TRADE_SHINY_CHANCE = 4096;
|
||||
|
||||
const WONDER_TRADE_HIDDEN_ABILITY_CHANCE = 64;
|
||||
|
||||
const LEGENDARY_TRADE_POOLS = {
|
||||
1: [SpeciesId.RATTATA, SpeciesId.PIDGEY, SpeciesId.WEEDLE],
|
||||
2: [SpeciesId.SENTRET, SpeciesId.HOOTHOOT, SpeciesId.LEDYBA],
|
||||
@ -273,38 +270,23 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
|
||||
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
||||
// Randomly generate a Wonder Trade pokemon
|
||||
const randomTradeOption = generateTradeOption(globalScene.getPlayerParty().map(p => p.species));
|
||||
const tradePokemon = new EnemyPokemon(randomTradeOption, pokemon.level, TrainerSlot.NONE, false);
|
||||
// Extra shiny roll at 1/128 odds (boosted by events and charms)
|
||||
if (!tradePokemon.shiny) {
|
||||
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
|
||||
if (timedEventManager.isEventActive()) {
|
||||
shinyThreshold.value *= timedEventManager.getShinyEncounterMultiplier();
|
||||
}
|
||||
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
|
||||
|
||||
// Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms
|
||||
// Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that
|
||||
const shinyChance = Math.min(shinyThreshold.value, MAX_WONDER_TRADE_SHINY_CHANCE);
|
||||
|
||||
tradePokemon.trySetShinySeed(shinyChance, false);
|
||||
}
|
||||
|
||||
// Extra HA roll at base 1/64 odds (boosted by events and charms)
|
||||
const hiddenIndex = tradePokemon.species.ability2 ? 2 : 1;
|
||||
if (tradePokemon.species.abilityHidden && tradePokemon.abilityIndex < hiddenIndex) {
|
||||
const hiddenAbilityChance = new NumberHolder(64);
|
||||
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
||||
|
||||
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
|
||||
|
||||
if (hasHiddenAbility) {
|
||||
tradePokemon.abilityIndex = hiddenIndex;
|
||||
}
|
||||
}
|
||||
const tradePokemon = getRandomEncounterPokemon({
|
||||
level: pokemon.level,
|
||||
speciesFunction: () => generateTradeOption(globalScene.getPlayerParty().map(p => p.species)),
|
||||
isBoss: false,
|
||||
eventChance: 100,
|
||||
shinyRerolls: 1,
|
||||
hiddenRerolls: 1,
|
||||
eventShinyRerolls: 1,
|
||||
eventHiddenRerolls: 1,
|
||||
hiddenAbilityChance: WONDER_TRADE_HIDDEN_ABILITY_CHANCE,
|
||||
shinyChance: WONDER_TRADE_SHINY_CHANCE,
|
||||
maxShinyChance: MAX_WONDER_TRADE_SHINY_CHANCE,
|
||||
speciesFilter: s => !globalScene.getPlayerParty().some(p => p.species === s),
|
||||
});
|
||||
|
||||
// If Pokemon is still not shiny or with HA, give the Pokemon a random Common egg move in its moveset
|
||||
if (!tradePokemon.shiny && (!tradePokemon.species.abilityHidden || tradePokemon.abilityIndex < hiddenIndex)) {
|
||||
if (!tradePokemon.shiny && (!tradePokemon.species.abilityHidden || tradePokemon.abilityIndex < 2)) {
|
||||
const eggMoves = tradePokemon.getEggMoves();
|
||||
if (eggMoves) {
|
||||
// Cannot gen the rare egg move, only 1 of the first 3 common moves
|
||||
|
||||
@ -9,11 +9,11 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { TrainerSlot } from "#enums/trainer-slot";
|
||||
import type { EnemyPokemon } from "#field/pokemon";
|
||||
import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#modifiers/modifier";
|
||||
import { IvScannerModifier } from "#modifiers/modifier";
|
||||
import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
|
||||
import {
|
||||
getRandomEncounterPokemon,
|
||||
initSubsequentOptionSelect,
|
||||
leaveEncounterWithoutBattle,
|
||||
transitionMysteryEncounterIntroVisuals,
|
||||
@ -30,7 +30,7 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
||||
import type { MysteryEncounterOption } from "#mystery-encounters/mystery-encounter-option";
|
||||
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
||||
import { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
||||
import { NumberHolder, randSeedInt } from "#utils/common";
|
||||
import { BooleanHolder, NumberHolder, randSeedInt } from "#utils/common";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
@ -42,6 +42,9 @@ const SAFARI_MONEY_MULTIPLIER = 2;
|
||||
|
||||
const NUM_SAFARI_ENCOUNTERS = 3;
|
||||
|
||||
const eventEncs = new NumberHolder(0);
|
||||
const eventChance = new NumberHolder(50);
|
||||
|
||||
/**
|
||||
* Safari Zone encounter.
|
||||
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3800 | GitHub Issue #3800}
|
||||
@ -74,6 +77,8 @@ export const SafariZoneEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
|
||||
.withQuery(`${namespace}:query`)
|
||||
.withOnInit(() => {
|
||||
globalScene.currentBattle.mysteryEncounter?.setDialogueToken("numEncounters", NUM_SAFARI_ENCOUNTERS.toString());
|
||||
eventEncs.value = 0;
|
||||
eventChance.value = 50;
|
||||
return true;
|
||||
})
|
||||
.withOption(
|
||||
@ -279,37 +284,37 @@ async function summonSafariPokemon() {
|
||||
|
||||
// Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken
|
||||
// Safari pokemon roll twice on shiny and HA chances, but are otherwise normal
|
||||
let enemySpecies: PokemonSpecies;
|
||||
let pokemon: any;
|
||||
globalScene.executeWithSeedOffset(
|
||||
() => {
|
||||
enemySpecies = getSafariSpeciesSpawn();
|
||||
const level = globalScene.currentBattle.getLevelForWave();
|
||||
enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, globalScene.gameMode));
|
||||
pokemon = globalScene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false);
|
||||
console.log("Event chance %d", eventChance.value);
|
||||
const fromEvent = new BooleanHolder(false);
|
||||
pokemon = getRandomEncounterPokemon({
|
||||
level: globalScene.currentBattle.getLevelForWave(),
|
||||
includeLegendary: false,
|
||||
includeSubLegendary: false,
|
||||
includeMythical: false,
|
||||
speciesFunction: getSafariSpeciesSpawn,
|
||||
shinyRerolls: 1,
|
||||
eventShinyRerolls: 1,
|
||||
hiddenRerolls: 1,
|
||||
eventHiddenRerolls: 1,
|
||||
eventChance: eventChance.value,
|
||||
isEventEncounter: fromEvent,
|
||||
});
|
||||
|
||||
// Roll shiny twice
|
||||
if (!pokemon.shiny) {
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.init();
|
||||
|
||||
// Increase chance of event encounter by 25% until one spawns
|
||||
if (fromEvent.value) {
|
||||
console.log("Safari zone encounter is from event");
|
||||
eventEncs.value++;
|
||||
eventChance.value = 50;
|
||||
} else if (eventEncs.value === 0) {
|
||||
console.log("Safari zone encounter is not from event");
|
||||
eventChance.value += 25;
|
||||
}
|
||||
|
||||
// Roll HA twice
|
||||
if (pokemon.species.abilityHidden) {
|
||||
const hiddenIndex = pokemon.species.ability2 ? 2 : 1;
|
||||
if (pokemon.abilityIndex < hiddenIndex) {
|
||||
const hiddenAbilityChance = new NumberHolder(256);
|
||||
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
||||
|
||||
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
|
||||
|
||||
if (hasHiddenAbility) {
|
||||
pokemon.abilityIndex = hiddenIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pokemon.calculateStats();
|
||||
|
||||
globalScene.currentBattle.enemyParty.unshift(pokemon);
|
||||
},
|
||||
globalScene.currentBattle.waveIndex * 1000 * encounter.misc.safariPokemonRemaining,
|
||||
@ -569,7 +574,7 @@ async function doEndTurn(cursorIndex: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
|
||||
* @returns A function to get a random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
|
||||
*/
|
||||
export function getSafariSpeciesSpawn(): PokemonSpecies {
|
||||
return getPokemonSpecies(
|
||||
|
||||
@ -77,6 +77,8 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
.withOnInit(() => {
|
||||
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||
|
||||
let isEventEncounter = false;
|
||||
|
||||
let species = getSalesmanSpeciesOffer();
|
||||
let tries = 0;
|
||||
|
||||
@ -88,16 +90,12 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
|
||||
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),
|
||||
);
|
||||
const validEventEncounters = timedEventManager.getAllValidEventEncounters(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
s => !NON_LEGEND_PARADOX_POKEMON.includes(s.speciesId) && !NON_LEGEND_ULTRA_BEASTS.includes(s.speciesId),
|
||||
);
|
||||
|
||||
let pokemon: PlayerPokemon;
|
||||
/**
|
||||
@ -122,7 +120,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
) {
|
||||
tries = 0;
|
||||
do {
|
||||
// If you roll 20%, give event encounter with 3 extra shiny rolls and its HA, if it has one
|
||||
// If you roll 50%, 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(
|
||||
@ -135,6 +133,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
if (pokemon.shiny || pokemon.abilityIndex === 2) {
|
||||
isEventEncounter = true;
|
||||
break;
|
||||
}
|
||||
tries++;
|
||||
@ -149,6 +148,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
isEventEncounter = true;
|
||||
} else {
|
||||
// If there's, and this would never happen, no eligible event encounters with a hidden ability, just do Magikarp
|
||||
species = getPokemonSpecies(SpeciesId.MAGIKARP);
|
||||
@ -177,7 +177,9 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
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;
|
||||
if (!isEventEncounter) {
|
||||
pokemon.abilityIndex = 0;
|
||||
}
|
||||
encounter.dialogue.encounterOptionsDialogue!.description = `${namespace}:descriptionShiny`;
|
||||
encounter.options[0].dialogue!.buttonTooltip = `${namespace}:option.1.tooltipShiny`;
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
|
||||
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
|
||||
import {
|
||||
getRandomEncounterSpecies,
|
||||
getRandomEncounterPokemon,
|
||||
initBattleWithEnemyConfig,
|
||||
leaveEncounterWithoutBattle,
|
||||
setEncounterExp,
|
||||
@ -62,7 +62,12 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
|
||||
// Calculate boss mon
|
||||
// Level equal to 2 below highest party member
|
||||
const level = getHighestLevelPlayerPokemon(false, true).level - 2;
|
||||
const pokemon = getRandomEncounterSpecies(level, true, true);
|
||||
const pokemon = getRandomEncounterPokemon({
|
||||
level,
|
||||
isBoss: true,
|
||||
eventShinyRerolls: 2,
|
||||
eventHiddenRerolls: 1,
|
||||
});
|
||||
|
||||
// Pokemon will always have one of its egg moves in its moveset
|
||||
const eggMoves = pokemon.getEggMoves();
|
||||
|
||||
@ -4,6 +4,7 @@ import { timedEventManager } from "#app/global-event-manager";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { biomeLinks } from "#balance/biomes";
|
||||
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE } from "#balance/rates";
|
||||
import { initMoveAnim, loadMoveAnimAssets } from "#data/battle-anims";
|
||||
import { modifierTypes } from "#data/data-lists";
|
||||
import type { IEggOptions } from "#data/egg";
|
||||
@ -47,11 +48,12 @@ import type { PokemonData } from "#system/pokemon-data";
|
||||
import type { TrainerConfig } from "#trainers/trainer-config";
|
||||
import { trainerConfigs } from "#trainers/trainer-config";
|
||||
import type { HeldModifierConfig } from "#types/held-modifier-config";
|
||||
import type { RandomEncounterParams } from "#types/pokemon-common";
|
||||
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler";
|
||||
import { PartyUiMode } from "#ui/party-ui-handler";
|
||||
import { coerceArray } from "#utils/array";
|
||||
import { randomString, randSeedInt, randSeedItem } from "#utils/common";
|
||||
import { BooleanHolder, randomString, randSeedInt, randSeedItem } from "#utils/common";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import i18next from "i18next";
|
||||
|
||||
@ -990,19 +992,39 @@ export function handleMysteryEncounterTurnStartEffects(): boolean {
|
||||
|
||||
/**
|
||||
* Helper function for encounters such as {@linkcode UncommonBreedEncounter} which call for a random species including event encounters.
|
||||
* If the mon is from the event encounter list, it will do an extra shiny roll.
|
||||
* @param level the level of the mon, which differs between MEs
|
||||
* @param isBoss whether the mon should be a Boss
|
||||
* @param rerollHidden whether the mon should get an extra roll for Hidden Ability
|
||||
* @returns for the requested encounter
|
||||
* If the mon is from the event encounter list, it may do an extra shiny or HA roll.
|
||||
* @param params - The {@linkcode RandomEncounterParams} used to configure the encounter
|
||||
* @returns The generated {@linkcode EnemyPokemon} for the requested encounter
|
||||
*/
|
||||
export function getRandomEncounterSpecies(level: number, isBoss = false, rerollHidden = false): EnemyPokemon {
|
||||
export function getRandomEncounterPokemon(params: RandomEncounterParams): EnemyPokemon {
|
||||
let {
|
||||
level,
|
||||
speciesFunction,
|
||||
isBoss = false,
|
||||
includeSubLegendary = true,
|
||||
includeLegendary = true,
|
||||
includeMythical = true,
|
||||
eventChance = 50,
|
||||
hiddenRerolls = 0,
|
||||
shinyRerolls = 0,
|
||||
eventHiddenRerolls = 0,
|
||||
eventShinyRerolls = 0,
|
||||
hiddenAbilityChance = BASE_HIDDEN_ABILITY_CHANCE,
|
||||
shinyChance = BASE_SHINY_CHANCE,
|
||||
maxShinyChance = 0,
|
||||
speciesFilter = () => true,
|
||||
isEventEncounter = new BooleanHolder(false),
|
||||
} = params;
|
||||
let bossSpecies: PokemonSpecies;
|
||||
let isEventEncounter = false;
|
||||
const eventEncounters = timedEventManager.getEventEncounters();
|
||||
const eventEncounters = timedEventManager.getAllValidEventEncounters(
|
||||
includeSubLegendary,
|
||||
includeLegendary,
|
||||
includeMythical,
|
||||
speciesFilter,
|
||||
);
|
||||
let formIndex: number | undefined;
|
||||
|
||||
if (eventEncounters.length > 0 && randSeedInt(2) === 1) {
|
||||
if (eventChance && eventEncounters.length > 0 && (eventChance === 100 || randSeedInt(100) < eventChance)) {
|
||||
const eventEncounter = randSeedItem(eventEncounters);
|
||||
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(
|
||||
level,
|
||||
@ -1010,9 +1032,13 @@ export function getRandomEncounterSpecies(level: number, isBoss = false, rerollH
|
||||
isBoss,
|
||||
globalScene.gameMode,
|
||||
);
|
||||
isEventEncounter = true;
|
||||
if (params.isEventEncounter) {
|
||||
params.isEventEncounter.value = true;
|
||||
}
|
||||
bossSpecies = getPokemonSpecies(levelSpecies);
|
||||
formIndex = eventEncounter.formIndex;
|
||||
} else if (speciesFunction) {
|
||||
bossSpecies = speciesFunction();
|
||||
} else {
|
||||
bossSpecies = globalScene.arena.randomSpecies(
|
||||
globalScene.currentBattle.waveIndex,
|
||||
@ -1027,13 +1053,19 @@ export function getRandomEncounterSpecies(level: number, isBoss = false, rerollH
|
||||
ret.formIndex = formIndex;
|
||||
}
|
||||
|
||||
//Reroll shiny or variant for event encounters
|
||||
if (isEventEncounter) {
|
||||
ret.trySetShinySeed();
|
||||
if (isEventEncounter.value) {
|
||||
hiddenRerolls += eventHiddenRerolls;
|
||||
shinyRerolls += eventShinyRerolls;
|
||||
}
|
||||
//Reroll hidden ability
|
||||
if (rerollHidden && ret.abilityIndex !== 2 && ret.species.abilityHidden) {
|
||||
ret.tryRerollHiddenAbilitySeed();
|
||||
|
||||
while (shinyRerolls > 0) {
|
||||
ret.trySetShinySeed(shinyChance, true, maxShinyChance);
|
||||
shinyRerolls--;
|
||||
}
|
||||
|
||||
while (hiddenRerolls > 0) {
|
||||
ret.tryRerollHiddenAbilitySeed(hiddenAbilityChance, true);
|
||||
hiddenRerolls--;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@ -13,18 +13,11 @@ import type { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { IllusionData } from "#types/illusion-data";
|
||||
import type { SerializedSpeciesForm } from "#types/pokemon-common";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
|
||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||
|
||||
/**
|
||||
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
|
||||
*/
|
||||
type SerializedSpeciesForm = {
|
||||
id: SpeciesId;
|
||||
formIdx: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Permanent data that can customize a Pokemon in non-standard ways from its Species.
|
||||
* Includes abilities, nature, changed types, etc.
|
||||
|
||||
@ -2903,12 +2903,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (thresholdOverride === undefined) {
|
||||
if (timedEventManager.isEventActive()) {
|
||||
const tchance = timedEventManager.getClassicTrainerShinyChance();
|
||||
shinyThreshold.value *= timedEventManager.getShinyEncounterMultiplier();
|
||||
if (this.hasTrainer() && tchance > 0) {
|
||||
if (this.isEnemy() && this.hasTrainer() && tchance > 0) {
|
||||
shinyThreshold.value = Math.max(tchance, shinyThreshold.value); // Choose the higher boost
|
||||
} else {
|
||||
// Wild shiny event multiplier
|
||||
shinyThreshold.value *= timedEventManager.getShinyEncounterMultiplier();
|
||||
}
|
||||
}
|
||||
if (!this.hasTrainer()) {
|
||||
if (this.isPlayer() || !this.hasTrainer()) {
|
||||
// Apply shiny modifiers only to Player or wild mons
|
||||
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
|
||||
}
|
||||
} else {
|
||||
@ -2932,11 +2935,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck.
|
||||
*
|
||||
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
|
||||
* @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
|
||||
* @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
|
||||
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
|
||||
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
|
||||
* @param maxThreshold The maximum threshold allowed after applying modifiers
|
||||
* @returns Whether this Pokémon was set to shiny
|
||||
*/
|
||||
public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean {
|
||||
public trySetShinySeed(
|
||||
thresholdOverride?: number,
|
||||
applyModifiersToOverride?: boolean,
|
||||
maxThreshold?: number,
|
||||
): boolean {
|
||||
if (!this.shiny) {
|
||||
const shinyThreshold = new NumberHolder(thresholdOverride ?? BASE_SHINY_CHANCE);
|
||||
if (applyModifiersToOverride) {
|
||||
@ -2946,6 +2954,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
|
||||
}
|
||||
|
||||
if (maxThreshold && maxThreshold > 0) {
|
||||
shinyThreshold.value = Math.min(maxThreshold, shinyThreshold.value);
|
||||
}
|
||||
|
||||
this.shiny = randSeedInt(65536) < shinyThreshold.value;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { SHINY_CATCH_RATE_MULTIPLIER } from "#balance/rates";
|
||||
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "#balance/starters";
|
||||
import type { PokemonSpeciesFilter } from "#data/pokemon-species";
|
||||
import type { WeatherPoolEntry } from "#data/weather";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
@ -11,6 +12,7 @@ import { WeatherType } from "#enums/weather-type";
|
||||
import type { ModifierTypeKeys } from "#modifiers/modifier-type";
|
||||
import type { nil } from "#types/common";
|
||||
import { addTextObject } from "#ui/text";
|
||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||
import i18next from "i18next";
|
||||
|
||||
export enum EventType {
|
||||
@ -27,7 +29,7 @@ interface EventBanner {
|
||||
readonly availableLangs?: readonly string[];
|
||||
}
|
||||
|
||||
interface EventEncounter {
|
||||
export interface EventEncounter {
|
||||
readonly species: SpeciesId;
|
||||
readonly blockEvolution?: boolean;
|
||||
readonly formIndex?: number;
|
||||
@ -390,6 +392,49 @@ const timedEvents: readonly TimedEvent[] = [
|
||||
],
|
||||
dailyRunStartingItems: ["SHINY_CHARM", "ABILITY_CHARM"],
|
||||
},
|
||||
{
|
||||
name: "Halloween 25",
|
||||
eventType: EventType.SHINY,
|
||||
startDate: new Date(Date.UTC(2025, 9, 29)),
|
||||
endDate: new Date(Date.UTC(2025, 10, 10)),
|
||||
bannerKey: "halloween2025",
|
||||
scale: 0.25, // Replace with actual number
|
||||
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-419", "pt-BR", "zh-Hans", "zh-Hant"],
|
||||
shinyEncounterMultiplier: 2,
|
||||
shinyCatchMultiplier: 3,
|
||||
eventEncounters: [
|
||||
{ species: SpeciesId.CATERPIE },
|
||||
{ species: SpeciesId.SPEAROW },
|
||||
{ species: SpeciesId.PARAS },
|
||||
{ species: SpeciesId.LICKITUNG },
|
||||
{ species: SpeciesId.AERODACTYL },
|
||||
{ species: SpeciesId.SMOOCHUM },
|
||||
{ species: SpeciesId.RALTS },
|
||||
{ species: SpeciesId.GULPIN },
|
||||
{ species: SpeciesId.FEEBAS },
|
||||
{ species: SpeciesId.WYNAUT },
|
||||
{ species: SpeciesId.CLAMPERL },
|
||||
{ species: SpeciesId.BUDEW },
|
||||
{ species: SpeciesId.DEOXYS },
|
||||
{ species: SpeciesId.CHINGLING },
|
||||
{ species: SpeciesId.DWEBBLE },
|
||||
{ species: SpeciesId.TIRTOUGA },
|
||||
{ species: SpeciesId.LARVESTA },
|
||||
{ species: SpeciesId.SPRITZEE },
|
||||
{ species: SpeciesId.SWIRLIX },
|
||||
{ species: SpeciesId.BINACLE },
|
||||
{ species: SpeciesId.PUMPKABOO },
|
||||
{ species: SpeciesId.SANDYGAST },
|
||||
],
|
||||
classicWaveRewards: [
|
||||
{ wave: 8, type: "SHINY_CHARM" },
|
||||
{ wave: 8, type: "ABILITY_CHARM" },
|
||||
{ wave: 8, type: "CATCHING_CHARM" },
|
||||
{ wave: 25, type: "SHINY_CHARM" },
|
||||
{ wave: 25, type: "CANDY_JAR" },
|
||||
],
|
||||
dailyRunStartingItems: ["ABILITY_CHARM", "SHINY_CHARM", "CANDY_JAR"],
|
||||
},
|
||||
];
|
||||
|
||||
export class TimedEventManager {
|
||||
@ -450,6 +495,23 @@ export class TimedEventManager {
|
||||
return [...(this.activeEvent()?.eventEncounters ?? [])];
|
||||
}
|
||||
|
||||
getAllValidEventEncounters(
|
||||
allowSubLegendary = true,
|
||||
allowLegendary = true,
|
||||
allowMythical = true,
|
||||
speciesFilter: PokemonSpeciesFilter,
|
||||
): EventEncounter[] {
|
||||
return this.getEventEncounters().filter(enc => {
|
||||
const species = getPokemonSpecies(enc.species);
|
||||
return (
|
||||
(allowSubLegendary || !species.subLegendary)
|
||||
&& (allowLegendary || !species.legendary)
|
||||
&& (allowMythical || !species.mythical)
|
||||
&& speciesFilter(species)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For events that change the classic candy friendship multiplier
|
||||
* @returns The classic friendship multiplier of the active {@linkcode TimedEvent}, or the default {@linkcode CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user