diff --git a/src/ai/ai-species-gen.ts b/src/ai/ai-species-gen.ts index c6a68ae172d..5420dea068e 100644 --- a/src/ai/ai-species-gen.ts +++ b/src/ai/ai-species-gen.ts @@ -1,7 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { SpeciesFormEvolution } from "#balance/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; -import { allSpecies } from "#data/data-lists"; import type { PokemonSpecies } from "#data/pokemon-species"; import { EvoLevelThresholdKind } from "#enums/evo-level-threshold-kind"; import { PartyMemberStrength } from "#enums/party-member-strength"; @@ -37,11 +36,6 @@ function calcEvoChance(ev: SpeciesFormEvolution, level: number, encounterKind: E const levelThreshold = Math.max(ev.level, ev.evoLevelThreshold?.[encounterKind] ?? 0); // Disallow evolution if the level is below its required threshold. if (level < ev.level || level < levelThreshold) { - console.info( - "%cDisallowing evolution of %s to %s at level %d (needs %d)", - "color: blue", - allSpecies[ev.speciesId]?.name, - ); return 0; } return levelThreshold; @@ -83,14 +77,6 @@ function getRequiredPrevo( const threshold = evoThreshold?.[encounterKind] ?? levelReq; const req = levelReq === 1 ? threshold : Math.min(levelReq, threshold); if (level < req) { - console.info( - "%cForcing prevo %s for %s at level %d (needs %d)", - "color: orange", - prevoSpecies, - species.speciesId, - level, - req, - ); return prevoSpecies; } } @@ -127,13 +113,6 @@ export function determineEnemySpecies( encounterKind: EvoLevelThresholdKind = forTrainer ? EvoLevelThresholdKind.NORMAL : EvoLevelThresholdKind.WILD, tryForcePrevo = true, ): SpeciesId { - console.info( - "%c Determining species for %s at level %d with encounter kind %s", - "color: blue", - species.name, - level, - encounterKind, - ); const requiredPrevo = tryForcePrevo && pokemonPrevolutions.hasOwnProperty(species.speciesId) @@ -162,7 +141,6 @@ export function determineEnemySpecies( } } if (evoPool.length === 0) { - console.log("%c No evolutions available, returning base species", "color: blue"); return species.speciesId; } const [choice, evoSpecies] = randSeedItem(evoPool); @@ -184,14 +162,7 @@ export function determineEnemySpecies( break; } - console.info( - "%c Returning a random integer between %d and %d", - "color: blue", - choice, - Math.round(choice * multiplier), - ); const randomLevel = randSeedInt(choice, Math.round(choice * multiplier)); - console.info("%c Random level is %d", "color: blue", randomLevel); if (randomLevel <= level) { return determineEnemySpecies( getPokemonSpecies(evoSpecies), diff --git a/src/ai/rival-team-gen.ts b/src/ai/rival-team-gen.ts new file mode 100644 index 00000000000..aec00920420 --- /dev/null +++ b/src/ai/rival-team-gen.ts @@ -0,0 +1,331 @@ +import { globalScene } from "#app/global-scene"; +import type { PokemonSpecies } from "#data/pokemon-species"; +import { getTypeDamageMultiplier } from "#data/type"; +import { AbilityId } from "#enums/ability-id"; +import { ChallengeType } from "#enums/challenge-type"; +import type { PartyMemberStrength } from "#enums/party-member-strength"; +import { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; +import { TrainerSlot } from "#enums/trainer-slot"; +import type { EnemyPokemon } from "#field/pokemon"; +import { RIVAL_6_POOL, type RivalPoolConfig } from "#trainers/rival-party-config"; +import { applyChallenges } from "#utils/challenge-utils"; +import { NumberHolder, randSeedItem } from "#utils/common"; +import { getEnumValues } from "#utils/enums"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; + +/** + * The maximum number of shared weaknesses to tolerate when balancing weakness + * + * @remarks + * When generating a slot that has weakness balancing enabled, the pool will + * exclude any species that would cause a type to be a weakness for more than + * this number of party members. + * Note that it is assumed that slot 0 is always going to Terastallize to its primary type, + * so slot 0's secondary type is excluded from weakness calculations. + */ +const MAX_SHARED_WEAKNESSES = 2; +/** + * The maximum number of shared types to tolerate when balancing types + * + * @remarks + * When generating a slot that has type balancing enabled, the pool will + * exclude any species that would cause a type to be present in more than + * this number of party members. + */ +const MAX_SHARED_TYPES = 1; + +/** Record of the chosen indices in the rival species pool, for type balancing based on the final fight */ +const CHOSEN_RIVAL_ROLLS: (undefined | [number] | [number, number])[] = new Array(6); + +/** + * Return the species from the rival species pool based on a previously chosen roll + * @param param0 - The chosen rolls for the rival species pool + * @param pool - The rival species pool for the slot + * @returns - A `SpeciesId` if found, or `undefined` if no species exists at the chosen roll + */ +function rivalRollToSpecies( + [roll1, roll2]: [number] | [number, number], + pool: readonly (SpeciesId | readonly SpeciesId[])[], +): SpeciesId | undefined { + const pull1 = pool[roll1]; + if (typeof pull1 === "number") { + return pull1; + } + if (roll2 == null) { + return; + } + + return pull1[roll2]; +} + +/** + * Calculates the types that the given species is weak to + * + * @remarks + * - Considers Levitate as a guaranteed ability if the species has it as all 3 possible abilities. + * - Accounts for type effectiveness challenges via {@linkcode applyChallenges} + * + * @privateRemarks + * Despite potentially being a useful utility method, this is intentionally *not* + * exported because it uses logic specific to this file, such as excluding the second type for Tera starters + * + * @param species - The species to calculate weaknesses for + * @param exclude2ndType - (Default `false`) Whether to exclude the second type when calculating weaknesses + * Intended to be used for starters since they will terastallize to their primary type. + * @returns The set of types that the species is weak to + */ +function getWeakTypes(species: PokemonSpecies, exclude2ndType = false): Set { + const weaknesses = new Set(); + // If the species is always immune to ground, skip ground type checks + // Note that there are no other Pokémon with guaranteed immunities due to all 3 of their abilities providing + // an immunity. + // At this point, we do not have an ability to know which ability the Pokémon generated with, so we can only + // work with guaranteed immunities. + const groundImmunityAbilities: readonly AbilityId[] = [AbilityId.LEVITATE, AbilityId.EARTH_EATER]; + const isAlwaysGroundImmune = + groundImmunityAbilities.includes(species.ability1) + && (species.ability2 == null || groundImmunityAbilities.includes(species.ability2)) + && (species.abilityHidden == null || groundImmunityAbilities.includes(species.ability2)); + for (const ty of getEnumValues(PokemonType)) { + if ( + ty === PokemonType.UNKNOWN + || ty === PokemonType.STELLAR + || (ty === PokemonType.GROUND && isAlwaysGroundImmune) + ) { + continue; + } + const multiplier = new NumberHolder(getTypeDamageMultiplier(ty, species.type1)); + applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier); + + if (multiplier.value >= 2 && !exclude2ndType) { + const type2 = species.type2; + if (type2 != null) { + const multiplier2 = new NumberHolder(getTypeDamageMultiplier(ty, type2)); + applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier2); + multiplier.value *= multiplier2.value; + } + } + if (multiplier.value >= 2) { + weaknesses.add(ty); + } + } + + return weaknesses; +} + +/** + * Calculate the existing types and weaknesses in the party up to the target slot + * @remarks + * At least one of either `balanceTypes` or `balanceWeaknesses` should be `true`, + * otherwise the function does nothing. + * @param targetSlot - The slot we are calculating up to (exclusive) + * @param pokemonTypes - A map that will hold the types present in the party + * @param pokemonWeaknesses - A map that will hold the weaknesses present in the party, and their counts + * @param balanceTypes - (Default `false`) Whether to include type balancing + * @param balanceWeaknesses - (Default `false`) Whether to attempt to add the party's existing weaknesses for the purpose of weakness balancing. + * @param referenceConfig - (Default {@linkcode RIVAL_6_POOL}); The reference rival pool configuration to use for type considerations + * + * @see {@linkcode MAX_SHARED_WEAKNESSES} + */ +function calcPartyTypings( + targetSlot: number, + pokemonTypes: Map, + pokemonWeaknesses: Map, + balanceTypes = false, + balanceWeaknesses = false, + referenceConfig: RivalPoolConfig = RIVAL_6_POOL, +): void { + if (!balanceTypes && !balanceWeaknesses) { + return; + } + for (let i = 0; i < targetSlot; i++) { + const chosenRoll = CHOSEN_RIVAL_ROLLS[i]; + const refConfig = referenceConfig[i]; + // In case pokemon are somehow generating out of order, break early + if (chosenRoll == null || refConfig == null) { + break; + } + + // Get the species from the roll + const refSpecies = rivalRollToSpecies(chosenRoll, refConfig.pool); + if (refSpecies == null) { + continue; + } + const refPokeSpecies = getPokemonSpecies(refSpecies); + const type1 = refPokeSpecies.type1; + const type2 = refPokeSpecies.type2; + if (balanceTypes) { + pokemonTypes.set(type1, (pokemonTypes.get(type1) ?? 0) + 1); + if (type2 != null) { + pokemonTypes.set(type2, (pokemonTypes.get(type2) ?? 0) + 1); + } + } + if (balanceWeaknesses) { + for (const weakType of getWeakTypes(refPokeSpecies)) { + pokemonWeaknesses.set(weakType, (pokemonWeaknesses.get(weakType) ?? 0) + 1); + } + } + } +} + +/** + * Determine if the species can be added to the party without violating type or weakness constraints + * @param species - The species to check + * @param existingTypes - The existing types in the party + * @param existingWeaknesses - The existing weaknesses in the party + * @param balanceTypes - (Default `false`) Whether to include type balancing + * @param balanceWeaknesses - (Default `false`) Whether to include weakness balancing + * @returns Whether the species meets the constraints + */ +function checkTypingConstraints( + species: SpeciesId, + existingTypes: ReadonlyMap, + existingWeaknesses: ReadonlyMap, + balanceTypes = false, + balanceWeaknesses = false, +): boolean { + if (!balanceTypes && !balanceWeaknesses) { + return true; + } + const { type1, type2 } = getPokemonSpecies(species); + + if ( + balanceTypes + && ((existingTypes.get(type1) ?? 0) >= MAX_SHARED_TYPES + || (type2 != null && (existingTypes.get(type2) ?? 0) >= MAX_SHARED_TYPES)) + ) { + return false; + } + + if (balanceWeaknesses) { + const weaknesses = getWeakTypes(getPokemonSpecies(species)); + for (const weakType of weaknesses) { + if ((existingWeaknesses.get(weakType) ?? 0) >= MAX_SHARED_WEAKNESSES) { + return false; + } + } + } + + return true; +} + +/** + * Convert a species pool to a list of choices after filtering by type and weakness constraints + * @param pool - The pool to convert to choices + * @param existingTypes - The existing types in the party + * @param existingWeaknesses - The existing weaknesses in the party + * @param balanceTypes - (Default `false`) Whether to include type balancing + * @param balanceWeaknesses - (Default `false`) Whether to include weakness balancing + * @returns A list of choices, where each choice is either a single index or a tuple of indices for sub-pools + */ +function convertPoolToChoices( + pool: readonly (SpeciesId | readonly SpeciesId[])[], + existingTypes: ReadonlyMap, + existingWeaknesses: ReadonlyMap, + balanceTypes = false, + balanceWeaknesses = false, +): (number | [number, number])[] { + const choices: (number | [number, number])[] = []; + + if (balanceTypes || balanceWeaknesses) { + for (const [i, entry] of pool.entries()) { + // Determine if there is a type overlap + if ( + typeof entry === "number" + && checkTypingConstraints(entry, existingTypes, existingWeaknesses, balanceTypes, balanceWeaknesses) + ) { + choices.push(i); + } else if (typeof entry !== "number") { + for (const [j, subEntry] of entry.entries()) { + if (checkTypingConstraints(subEntry, existingTypes, existingWeaknesses, balanceTypes, balanceWeaknesses)) { + choices.push([i, j]); + } + } + } + } + } + + if (choices.length === 0) { + for (const [i, entry] of pool.entries()) { + if (typeof entry === "number") { + choices.push(i); + } else { + for (const j of entry.keys()) { + choices.push([i, j]); + } + } + } + } + + return choices; +} + +/** + * Randomly selects one of the `Species` from `speciesPool`, determines its evolution, level, and strength. + * Then adds Pokemon to `globalScene`. + * @param config - The configuration for the rival pool fight + * @param slot - The slot being generated for (0-5) + * @param referenceConfig - (Default {@linkcode RIVAL_6_POOL}); The final rival pool configuration to use if `config` is `RIVAL_POOL_CONFIG.FINAL` + * + * @throws + * If no configuration is found for the specified slot. + */ +export function getRandomRivalPartyMemberFunc( + config: RivalPoolConfig, + slot: number, + referenceConfig: RivalPoolConfig = RIVAL_6_POOL, +): (level: number, strength: PartyMemberStrength) => EnemyPokemon { + // Protect against out of range slots. + // Only care about this in dev to be caught during development; it will be excluded in production builds. + if (import.meta.env.DEV && slot > config.length) { + throw new Error(`Slot ${slot} is out of range for the provided config of length ${config.length}`); + } + return (level: number, _strength: PartyMemberStrength) => { + const { pool, postProcess, balanceTypes, balanceWeaknesses } = config[slot]; + + const existingTypes = new Map(); + const existingWeaknesses = new Map(); + + if (slot === 0) { + // Clear out the rolls from previous rival generations + CHOSEN_RIVAL_ROLLS.fill(undefined); + } else if (balanceTypes || balanceWeaknesses) { + calcPartyTypings(slot, existingTypes, existingWeaknesses, balanceTypes, balanceWeaknesses, referenceConfig); + } + + // Filter the pool to its choices, or map it + + let species: SpeciesId | SpeciesId[]; + + // When converting pool to choices, base off of the reference config + // to use for type balancing, as we only narrow based on what the slot + // will be in its final stage + const choices = convertPoolToChoices( + referenceConfig[slot].pool, + existingTypes, + existingWeaknesses, + balanceTypes, + balanceWeaknesses, + ); + + const choice = randSeedItem(choices); + if (typeof choice === "number") { + species = pool[choice] as SpeciesId; + CHOSEN_RIVAL_ROLLS[slot] = [choice]; + } else { + species = pool[choice[0]][choice[1]]; + CHOSEN_RIVAL_ROLLS[slot] = choice; + } + + return globalScene.addEnemyPokemon( + getPokemonSpecies(species), + level, + TrainerSlot.TRAINER, + undefined, + false, + undefined, + postProcess, + ); + }; +} diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts index 086024f8fb9..bccb842945e 100644 --- a/src/data/moves/pokemon-move.ts +++ b/src/data/moves/pokemon-move.ts @@ -59,7 +59,7 @@ export class PokemonMove { } if (!ignorePp && move.pp !== -1 && this.ppUsed >= this.getMovePp()) { - return [false, i18next.t("battle:moveNoPP", { moveName: move.name })]; + return [false, i18next.t("battle:moveNoPp", { moveName: move.name })]; } if (forSelection) { diff --git a/src/data/trainers/rival-party-config.ts b/src/data/trainers/rival-party-config.ts new file mode 100644 index 00000000000..44e148538fb --- /dev/null +++ b/src/data/trainers/rival-party-config.ts @@ -0,0 +1,692 @@ +import { timedEventManager } from "#app/global-event-manager"; +import { PokeballType } from "#enums/pokeball"; +import { SpeciesId } from "#enums/species-id"; +import type { EnemyPokemon } from "#field/pokemon"; +import { randSeedIntRange, randSeedItem } from "#utils/common"; + +//#region constants + +// Levels for slots 1 and 2 do not need post-processing logic + +// Fight 1 doesn't have slot 3 +const SLOT_3_FIGHT_2_LEVEL = 16; +const SLOT_3_FIGHT_3_LEVEL = 36; +const SLOT_3_FIGHT_4_LEVEL = 71; +const SLOT_3_FIGHT_5_LEVEL = 125; +const SLOT_3_FIGHT_6_LEVEL = 189; + +// Fights 1 and 2 don't have slot 4 +const SLOT_4_FIGHT_3_LEVEL = 38; +const SLOT_4_FIGHT_4_LEVEL = 71; +const SLOT_4_FIGHT_5_LEVEL = 125; +const SLOT_4_FIGHT_6_LEVEL = 189; + +// Fights 1-3 don't have slot 5 +const SLOT_5_FIGHT_4_LEVEL = 69; +const SLOT_5_FIGHT_5_LEVEL = 127; +const SLOT_5_FIGHT_6_LEVEL = 189; + +// Fights 1-4 don't have slot 6 +const SLOT_6_FIGHT_5_LEVEL = 129; +const SLOT_6_FIGHT_6_LEVEL = 200; + +//#endregion constants + +//#region Slot 1 + +/** + * Set the abiltiy index to 0 and the tera type to the primary type + * + * @param pokemon - The pokemon to force traits for + * @param bars - (default `0`) The number of boss bar segments to set. If `zero`, the pokemon will not be a boss + */ + +function forceRivalStarterTraits(pokemon: EnemyPokemon, bars = 0): void { + pokemon.abilityIndex = 0; + pokemon.teraType = pokemon.species.type1; + if (bars > 0) { + pokemon.setBoss(true, bars); + pokemon.generateAndPopulateMoveset(); + } +} + +/** Rival's slot 1 species pool for fight 1 */ +const SLOT_1_FIGHT_1 = [ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.CHIKORITA, + SpeciesId.CYNDAQUIL, + SpeciesId.TOTODILE, + SpeciesId.TREECKO, + SpeciesId.TORCHIC, + SpeciesId.MUDKIP, + SpeciesId.TURTWIG, + SpeciesId.CHIMCHAR, + SpeciesId.PIPLUP, + SpeciesId.SNIVY, + SpeciesId.TEPIG, + SpeciesId.OSHAWOTT, + SpeciesId.CHESPIN, + SpeciesId.FENNEKIN, + SpeciesId.FROAKIE, + SpeciesId.ROWLET, + SpeciesId.LITTEN, + SpeciesId.POPPLIO, + SpeciesId.GROOKEY, + SpeciesId.SCORBUNNY, + SpeciesId.SOBBLE, + SpeciesId.SPRIGATITO, + SpeciesId.FUECOCO, + SpeciesId.QUAXLY, +]; + +/** Rival's slot 1 species pool for fight 2 */ +const SLOT_1_FIGHT_2 = [ + SpeciesId.IVYSAUR, + SpeciesId.CHARMELEON, + SpeciesId.WARTORTLE, + SpeciesId.BAYLEEF, + SpeciesId.QUILAVA, + SpeciesId.CROCONAW, + SpeciesId.GROVYLE, + SpeciesId.COMBUSKEN, + SpeciesId.MARSHTOMP, + SpeciesId.GROTLE, + SpeciesId.MONFERNO, + SpeciesId.PRINPLUP, + SpeciesId.SERVINE, + SpeciesId.PIGNITE, + SpeciesId.DEWOTT, + SpeciesId.QUILLADIN, + SpeciesId.BRAIXEN, + SpeciesId.FROGADIER, + SpeciesId.DARTRIX, + SpeciesId.TORRACAT, + SpeciesId.BRIONNE, + SpeciesId.THWACKEY, + SpeciesId.RABOOT, + SpeciesId.DRIZZILE, + SpeciesId.FLORAGATO, + SpeciesId.CROCALOR, + SpeciesId.QUAXWELL, +]; + +/** Rival's slot 1 species pool for fight 3 and beyond */ +const SLOT_1_FINAL = [ + SpeciesId.VENUSAUR, + SpeciesId.CHARIZARD, + SpeciesId.BLASTOISE, + SpeciesId.MEGANIUM, + SpeciesId.TYPHLOSION, + SpeciesId.FERALIGATR, + SpeciesId.SCEPTILE, + SpeciesId.BLAZIKEN, + SpeciesId.SWAMPERT, + SpeciesId.TORTERRA, + SpeciesId.INFERNAPE, + SpeciesId.EMPOLEON, + SpeciesId.SERPERIOR, + SpeciesId.EMBOAR, + SpeciesId.SAMUROTT, + SpeciesId.CHESNAUGHT, + SpeciesId.DELPHOX, + SpeciesId.GRENINJA, + SpeciesId.DECIDUEYE, + SpeciesId.INCINEROAR, + SpeciesId.PRIMARINA, + SpeciesId.RILLABOOM, + SpeciesId.CINDERACE, + SpeciesId.INTELEON, + SpeciesId.MEOWSCARADA, + SpeciesId.SKELEDIRGE, + SpeciesId.QUAQUAVAL, +]; +//#endregion slot 1 + +//#region Slot 2 +/** + * Post-process rival birds to override their sets + * + * @remarks + * Currently used to force ability indices + * + * @param pokemon - The rival bird pokemon to force an ability for + * @param bars - (default `0`) The number of boss bar segments to set. If `zero`, the pokemon will not be a boss + */ + +function forceRivalBirdAbility(pokemon: EnemyPokemon, bars = 0): void { + switch (pokemon.species.speciesId) { + // Guts for Tailow line + case SpeciesId.TAILLOW: + case SpeciesId.SWELLOW: + // Intimidate for Starly line + case SpeciesId.STARLY: + case SpeciesId.STARAVIA: + case SpeciesId.STARAPTOR: { + pokemon.abilityIndex = 0; + break; + } + // Tangled Feet for Pidgey line + case SpeciesId.PIDGEY: + case SpeciesId.PIDGEOTTO: + case SpeciesId.PIDGEOT: + // Super Luck for pidove line + case SpeciesId.PIDOVE: + case SpeciesId.TRANQUILL: + case SpeciesId.UNFEZANT: + // Volt Absorb for Wattrel line + case SpeciesId.WATTREL: + case SpeciesId.KILOWATTREL: { + pokemon.abilityIndex = 1; + break; + } + // Tinted lens for Hoothoot line + case SpeciesId.HOOTHOOT: + case SpeciesId.NOCTOWL: + // Skill link for Pikipek line + case SpeciesId.PIKIPEK: + case SpeciesId.TRUMBEAK: + case SpeciesId.TOUCANNON: + // Gale Wings for Fletchling line + case SpeciesId.FLETCHLING: + case SpeciesId.FLETCHINDER: + case SpeciesId.TALONFLAME: { + pokemon.abilityIndex = 2; + break; + } + } + + if (bars > 0) { + pokemon.setBoss(true, bars); + pokemon.generateAndPopulateMoveset(); + } +} +/** Rival's slot 2 species pool for fight 1 */ +const SLOT_2_FIGHT_1 = [ + SpeciesId.PIDGEY, + SpeciesId.HOOTHOOT, + SpeciesId.TAILLOW, + SpeciesId.STARLY, + SpeciesId.PIDOVE, + SpeciesId.FLETCHLING, + SpeciesId.PIKIPEK, + SpeciesId.ROOKIDEE, + SpeciesId.WATTREL, +]; + +/** Rival's slot 2 species pool for fight 2 */ +const SLOT_2_FIGHT_2 = [ + SpeciesId.PIDGEOTTO, + SpeciesId.HOOTHOOT, + SpeciesId.TAILLOW, + SpeciesId.STARAVIA, + SpeciesId.TRANQUILL, + SpeciesId.FLETCHINDER, + SpeciesId.TRUMBEAK, + SpeciesId.CORVISQUIRE, + SpeciesId.WATTREL, +]; + +/** Rival's slot 2 species pool for fight 3 and beyond */ +const SLOT_2_FINAL = [ + SpeciesId.PIDGEOT, + SpeciesId.NOCTOWL, + SpeciesId.SWELLOW, + SpeciesId.STARAPTOR, + SpeciesId.UNFEZANT, + SpeciesId.TALONFLAME, + SpeciesId.TOUCANNON, + SpeciesId.CORVIKNIGHT, + SpeciesId.KILOWATTREL, +]; +//#endregion Slot 2 + +//#region Slot 3 +/** Rival's slot 3 species pool for fight 2 */ +const SLOT_3_FIGHT_2 = [ + SpeciesId.NIDORINA, + SpeciesId.NIDORINO, + SpeciesId.MANKEY, + SpeciesId.GROWLITHE, + SpeciesId.ABRA, + SpeciesId.MACHOP, + SpeciesId.GASTLY, + SpeciesId.MAGNEMITE, + SpeciesId.RHYDON, + SpeciesId.TANGELA, + SpeciesId.PORYGON, + SpeciesId.ELEKID, + SpeciesId.MAGBY, + SpeciesId.MARILL, + SpeciesId.TEDDIURSA, + SpeciesId.SWINUB, + SpeciesId.SLAKOTH, + SpeciesId.ARON, + SpeciesId.SPHEAL, + SpeciesId.FEEBAS, + SpeciesId.MUNCHLAX, + SpeciesId.ROGGENROLA, + SpeciesId.TIMBURR, + SpeciesId.TYMPOLE, + SpeciesId.SANDILE, + SpeciesId.YAMASK, + SpeciesId.SOLOSIS, + SpeciesId.VANILLITE, + SpeciesId.TYMPOLE, + SpeciesId.LITWICK, + SpeciesId.MUDBRAY, + SpeciesId.DEWPIDER, + SpeciesId.WIMPOD, + SpeciesId.HATENNA, + SpeciesId.IMPIDIMP, + SpeciesId.SMOLIV, + SpeciesId.NACLI, + [SpeciesId.CHARCADET, SpeciesId.CHARCADET], + SpeciesId.TINKATINK, + SpeciesId.GLIMMET, +]; + +/** Rival's slot 3 species pool for fight 3 */ +const SLOT_3_FIGHT_3 = [ + SpeciesId.NIDOQUEEN, + SpeciesId.NIDOKING, + SpeciesId.PRIMEAPE, + SpeciesId.ARCANINE, + SpeciesId.KADABRA, + SpeciesId.MACHOKE, + SpeciesId.HAUNTER, + SpeciesId.MAGNETON, + SpeciesId.RHYDON, + SpeciesId.TANGROWTH, + SpeciesId.PORYGON2, + SpeciesId.ELECTIVIRE, + SpeciesId.MAGMAR, + SpeciesId.AZUMARILL, + SpeciesId.URSARING, + SpeciesId.PILOSWINE, + SpeciesId.VIGOROTH, + SpeciesId.LAIRON, + SpeciesId.SEALEO, + SpeciesId.MILOTIC, + SpeciesId.SNORLAX, + SpeciesId.BOLDORE, + SpeciesId.GURDURR, + SpeciesId.PALPITOAD, + SpeciesId.KROKOROK, + SpeciesId.COFAGRIGUS, + SpeciesId.DUOSION, + SpeciesId.VANILLISH, + SpeciesId.EELEKTRIK, + SpeciesId.LAMPENT, + SpeciesId.MUDSDALE, + SpeciesId.ARAQUANID, + SpeciesId.GOLISOPOD, + SpeciesId.HATTREM, + SpeciesId.MORGREM, + SpeciesId.DOLLIV, + SpeciesId.NACLSTACK, + [SpeciesId.ARMAROUGE, SpeciesId.CERULEDGE], + SpeciesId.TINKATUFF, + SpeciesId.GLIMMORA, +]; + +/** Rival's slot 3 species pool for fight 4 and beyond */ +const SLOT_3_FINAL = [ + SpeciesId.NIDOQUEEN, + SpeciesId.NIDOKING, + SpeciesId.ANNIHILAPE, + SpeciesId.ARCANINE, + SpeciesId.ALAKAZAM, + SpeciesId.MACHAMP, + SpeciesId.GENGAR, + SpeciesId.MAGNEZONE, + SpeciesId.RHYPERIOR, + SpeciesId.TANGROWTH, + SpeciesId.PORYGON_Z, + SpeciesId.ELECTIVIRE, + SpeciesId.MAGMORTAR, + SpeciesId.AZUMARILL, + SpeciesId.URSALUNA, + SpeciesId.MAMOSWINE, + SpeciesId.SLAKING, + SpeciesId.AGGRON, + SpeciesId.WALREIN, + SpeciesId.MILOTIC, + SpeciesId.SNORLAX, + SpeciesId.GIGALITH, + SpeciesId.CONKELDURR, + SpeciesId.SEISMITOAD, + SpeciesId.KROOKODILE, + SpeciesId.COFAGRIGUS, + SpeciesId.REUNICLUS, + SpeciesId.VANILLUXE, + SpeciesId.EELEKTROSS, + SpeciesId.CHANDELURE, + SpeciesId.MUDSDALE, + SpeciesId.ARAQUANID, + SpeciesId.GOLISOPOD, + SpeciesId.HATTERENE, + SpeciesId.GRIMMSNARL, + SpeciesId.ARBOLIVA, + SpeciesId.GARGANACL, + [SpeciesId.ARMAROUGE, SpeciesId.CERULEDGE], + SpeciesId.TINKATON, + SpeciesId.GLIMMORA, +]; +//#endregion Slot 3 + +//#region Slot 4 + +/** + * Post-process logic for rival slot 4, fight 4 + * @remarks + * Set level to 38 and specific forms for certain species + * @param pokemon - The pokemon to post-process + */ +function postProcessSlot4Fight3(pokemon: EnemyPokemon): void { + pokemon.level = SLOT_4_FIGHT_3_LEVEL; + switch (pokemon.species.speciesId) { + case SpeciesId.BASCULIN: + pokemon.formIndex = 2; // White + return; + case SpeciesId.ROTOM: + // Heat, Wash, Mow + pokemon.formIndex = randSeedItem([1, 2, 5]); + return; + case SpeciesId.PALDEA_TAUROS: + pokemon.formIndex = randSeedIntRange(1, 2); // Blaze, Aqua + return; + } +} +/** Rival's slot 4 species pool for fight 3 */ +const SLOT_4_FIGHT_3 = [ + SpeciesId.CLEFABLE, + [SpeciesId.SLOWBRO, SpeciesId.SLOWKING], + SpeciesId.PINSIR, + SpeciesId.LAPRAS, + SpeciesId.SCIZOR, + SpeciesId.HERACROSS, + SpeciesId.SNEASEL, + SpeciesId.GARDEVOIR, + SpeciesId.ROSERADE, + SpeciesId.SPIRITOMB, + SpeciesId.LUCARIO, + SpeciesId.DRAPION, + SpeciesId.GALLADE, + SpeciesId.ROTOM, + SpeciesId.EXCADRILL, + [SpeciesId.ZOROARK, SpeciesId.HISUI_ZOROARK], + SpeciesId.FERROTHORN, + SpeciesId.DURANT, + SpeciesId.FLORGES, + SpeciesId.DOUBLADE, + SpeciesId.VIKAVOLT, + SpeciesId.MIMIKYU, + SpeciesId.DHELMISE, + SpeciesId.POLTEAGEIST, + SpeciesId.COPPERAJAH, + SpeciesId.KLEAVOR, + SpeciesId.BASCULIN, + SpeciesId.HISUI_SNEASEL, + SpeciesId.HISUI_QWILFISH, + SpeciesId.PAWMOT, + SpeciesId.CETITAN, + SpeciesId.DONDOZO, + SpeciesId.DUDUNSPARCE, + SpeciesId.GHOLDENGO, + SpeciesId.POLTCHAGEIST, + [SpeciesId.GALAR_SLOWBRO, SpeciesId.GALAR_SLOWKING], + SpeciesId.HISUI_ARCANINE, + SpeciesId.PALDEA_TAUROS, +]; + +/** + * Set level and and specific forms for the species in slot 4 + * @param pokemon - The pokemon to post-process + * @param level - (default {@linkcode SLOT_4_FIGHT_4_LEVEL}) The level to set the pokemon to + */ +function postProcessSlot4Fight4(pokemon: EnemyPokemon, level = SLOT_4_FIGHT_4_LEVEL): void { + pokemon.level = level; + switch (pokemon.species.speciesId) { + case SpeciesId.BASCULEGION: + pokemon.formIndex = randSeedIntRange(0, 1); + return; + case SpeciesId.ROTOM: + // Heat, Wash, Mow + pokemon.formIndex = randSeedItem([1, 2, 5]); + return; + case SpeciesId.PALDEA_TAUROS: + pokemon.formIndex = randSeedIntRange(1, 2); // Blaze, Aqua + return; + } +} + +/** Rival's slot 4 species pool for fight 4 and beyond */ +const SLOT_4_FINAL = [ + SpeciesId.CLEFABLE, + [SpeciesId.SLOWBRO, SpeciesId.SLOWKING], + SpeciesId.PINSIR, + SpeciesId.LAPRAS, + SpeciesId.SCIZOR, + SpeciesId.HERACROSS, + SpeciesId.WEAVILE, + SpeciesId.GARDEVOIR, + SpeciesId.ROSERADE, + SpeciesId.SPIRITOMB, + SpeciesId.LUCARIO, + SpeciesId.DRAPION, + SpeciesId.GALLADE, + SpeciesId.ROTOM, + SpeciesId.EXCADRILL, + [SpeciesId.ZOROARK, SpeciesId.HISUI_ZOROARK], + SpeciesId.FERROTHORN, + SpeciesId.DURANT, + SpeciesId.FLORGES, + SpeciesId.AEGISLASH, + SpeciesId.VIKAVOLT, + SpeciesId.MIMIKYU, + SpeciesId.DHELMISE, + SpeciesId.POLTEAGEIST, + SpeciesId.COPPERAJAH, + SpeciesId.KLEAVOR, + SpeciesId.BASCULEGION, // Ensure gender does not change + SpeciesId.SNEASLER, + SpeciesId.OVERQWIL, + SpeciesId.PAWMOT, + SpeciesId.CETITAN, + SpeciesId.DONDOZO, + SpeciesId.DUDUNSPARCE, + SpeciesId.GHOLDENGO, + SpeciesId.POLTCHAGEIST, + [SpeciesId.GALAR_SLOWBRO, SpeciesId.GALAR_SLOWKING], + SpeciesId.HISUI_ARCANINE, + SpeciesId.PALDEA_TAUROS, +]; +//#endregion Slot 4 + +//#region Slot 5 +/** Rival's slot 5 species pool for fight 4 and beyond */ +const SLOT_5_FINAL = [ + SpeciesId.DRAGONITE, + SpeciesId.KINGDRA, + SpeciesId.TYRANITAR, + SpeciesId.SALAMENCE, + SpeciesId.METAGROSS, + SpeciesId.GARCHOMP, + SpeciesId.HAXORUS, + SpeciesId.HYDREIGON, + SpeciesId.VOLCARONA, + SpeciesId.GOODRA, + SpeciesId.KOMMO_O, + SpeciesId.DRAGAPULT, + SpeciesId.KINGAMBIT, + SpeciesId.BAXCALIBUR, + SpeciesId.GHOLDENGO, + SpeciesId.ARCHALUDON, + SpeciesId.HYDRAPPLE, + SpeciesId.HISUI_GOODRA, +]; +//#endregion Slot 5 + +//#region Slot 6 +/** + * Post-process logic for rival slot 6, fight 5 + * + * @remarks + * Sets the level to the provided argument, sets the Pokémon to be caught in a Master Ball, sets boss + * @param pokemon - The pokemon to post-process + * @param level - (default {@linkcode SLOT_6_FIGHT_5_LEVEL}) The level to set the pokemon to + * @param overrideSegments - If `true`, will force the pokemon to have 3 boss bar segments + */ +function postProcessSlot6Fight5(pokemon: EnemyPokemon, level = SLOT_6_FIGHT_5_LEVEL, overrideSegments = true): void { + pokemon.level = level; + pokemon.pokeball = PokeballType.MASTER_BALL; + if (timedEventManager.getClassicTrainerShinyChance() === 0) { + pokemon.shiny = true; + pokemon.variant = 1; + } + // When called for fight 5, uses 3 segments. + // For fight 6, uses the logic from `getEncounterBossSegments` + pokemon.setBoss(true, overrideSegments ? 3 : undefined); +} + +/** + * Post-process logic for rival slot 6, fight 6 + * + * @remarks + * Applies {@linkcode postProcessSlot6Fight5} with an updated level + * and also sets the `formIndex` to `1` for Mega Rayquaza + * @param pokemon + */ +function postProcessSlot6Fight6(pokemon: EnemyPokemon): void { + // Guard just in case species gets overridden + if (pokemon.species.speciesId === SpeciesId.RAYQUAZA) { + pokemon.formIndex = 1; // Mega + } + postProcessSlot6Fight5(pokemon, SLOT_6_FIGHT_6_LEVEL, false); + pokemon.generateName(); +} + +/** Rival's slot 6 species pool for fight 5 and beyond */ +const SLOT_6_FINAL = [SpeciesId.RAYQUAZA]; +//#endregion Slot 6 + +export interface RivalSlotConfig { + /** + * The pool of `SpeciesId`s to choose from + * + * @remarks + * An entry may be either a single `SpeciesId` or an array of `SpeciesId`s. An + * array entry indicates that another roll is required, and is used for split + * evolution lines such as Charcadet to Armarouge/Ceruledge. + */ + readonly pool: readonly (SpeciesId | readonly SpeciesId[])[]; + + /** A function that will post-process the Pokémon after it has fully generated */ + readonly postProcess: (enemyPokemon: EnemyPokemon) => void; + + /** + * Whether to try to balance types in this slot to avoid sharing types with previous slots + * @defaultValue `false` + */ + readonly balanceTypes?: boolean; + + /** + * Whether to try to balance weaknesses in this slot to avoid adding too many weaknesses to the overall party + * @defaultValue `false` + */ + readonly balanceWeaknesses?: boolean; +} + +export type RivalPoolConfig = RivalSlotConfig[]; + +/** Pools for the first rival fight */ +export const RIVAL_1_POOL: RivalPoolConfig = [ + { pool: SLOT_1_FIGHT_1, postProcess: forceRivalStarterTraits }, + { pool: SLOT_2_FIGHT_1, postProcess: forceRivalBirdAbility }, +]; + +/** Pools for the second rival fight */ +export const RIVAL_2_POOL: RivalPoolConfig = [ + { pool: SLOT_1_FIGHT_2, postProcess: forceRivalStarterTraits }, + { pool: SLOT_2_FIGHT_2, postProcess: forceRivalBirdAbility }, + { + pool: SLOT_3_FIGHT_2, + postProcess: p => (p.level = SLOT_3_FIGHT_2_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, +]; + +/** Pools for the third rival fight */ +export const RIVAL_3_POOL: RivalPoolConfig = [ + { pool: SLOT_1_FINAL, postProcess: forceRivalStarterTraits }, + { pool: SLOT_2_FINAL, postProcess: forceRivalBirdAbility }, + { + pool: SLOT_3_FIGHT_3, + postProcess: p => (p.level = SLOT_3_FIGHT_3_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { pool: SLOT_4_FIGHT_3, postProcess: postProcessSlot4Fight3, balanceTypes: true, balanceWeaknesses: true }, +]; + +/** Pools for the fourth rival fight */ +export const RIVAL_4_POOL: RivalPoolConfig = [ + { pool: SLOT_1_FINAL, postProcess: forceRivalStarterTraits }, + { pool: SLOT_2_FINAL, postProcess: forceRivalBirdAbility }, + { + pool: SLOT_3_FINAL, + postProcess: p => (p.level = SLOT_3_FIGHT_4_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { + pool: SLOT_4_FINAL, + postProcess: p => postProcessSlot4Fight4(p, SLOT_4_FIGHT_4_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { pool: SLOT_5_FINAL, postProcess: p => (p.level = SLOT_5_FIGHT_4_LEVEL) }, +]; + +/** Pools for the fifth rival fight */ +export const RIVAL_5_POOL: RivalPoolConfig = [ + { pool: SLOT_1_FINAL, postProcess: p => forceRivalStarterTraits(p, 2) }, + { pool: SLOT_2_FINAL, postProcess: forceRivalBirdAbility }, + { + pool: SLOT_3_FINAL, + postProcess: p => (p.level = SLOT_3_FIGHT_5_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { + pool: SLOT_4_FINAL, + postProcess: p => postProcessSlot4Fight4(p, SLOT_4_FIGHT_5_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { pool: SLOT_5_FINAL, postProcess: p => (p.level = SLOT_5_FIGHT_5_LEVEL) }, + { pool: SLOT_6_FINAL, postProcess: postProcessSlot6Fight5 }, +]; + +/** Pools for the sixth rival fight */ +export const RIVAL_6_POOL: RivalPoolConfig = [ + { pool: SLOT_1_FINAL, postProcess: p => forceRivalStarterTraits(p, 3) }, + { pool: SLOT_2_FINAL, postProcess: p => forceRivalBirdAbility(p, 2) }, + { + pool: SLOT_3_FINAL, + postProcess: p => (p.level = SLOT_3_FIGHT_6_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { + pool: SLOT_4_FINAL, + postProcess: p => postProcessSlot4Fight4(p, SLOT_4_FIGHT_6_LEVEL), + balanceTypes: true, + balanceWeaknesses: true, + }, + { pool: SLOT_5_FINAL, postProcess: p => (p.level = SLOT_5_FIGHT_6_LEVEL) }, + { pool: SLOT_6_FINAL, postProcess: postProcessSlot6Fight6 }, +]; diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index b6b09d5ba2a..4b7c945b64d 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -1,10 +1,7 @@ -import { timedEventManager } from "#app/global-event-manager"; +import { getRandomRivalPartyMemberFunc } from "#app/ai/rival-team-gen"; import { globalScene } from "#app/global-scene"; -import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { signatureSpecies } from "#balance/signature-species"; import { tmSpecies } from "#balance/tms"; -// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment -import type { RARE_EGG_MOVE_LEVEL_REQUIREMENT } from "#data/balance/moveset-generation"; import { modifierTypes } from "#data/data-lists"; import { doubleBattleDialogue } from "#data/double-battle-dialogue"; import { Gender } from "#data/gender"; @@ -25,6 +22,14 @@ import { PokemonMove } from "#moves/pokemon-move"; import { getIsInitialized, initI18n } from "#plugins/i18n"; import type { EvilTeam } from "#trainers/evil-admin-trainer-pools"; import { evilAdminTrainerPools } from "#trainers/evil-admin-trainer-pools"; +import { + RIVAL_1_POOL, + RIVAL_2_POOL, + RIVAL_3_POOL, + RIVAL_4_POOL, + RIVAL_5_POOL, + RIVAL_6_POOL, +} from "#trainers/rival-party-config"; import { getEvilGruntPartyTemplate, getGymLeaderPartyTemplate, @@ -988,32 +993,30 @@ export class TrainerConfig { } } -let t = 0; - /** * Randomly selects one of the `Species` from `speciesPool`, determines its evolution, level, and strength. * Then adds Pokemon to globalScene. - * @param speciesPool - * @param trainerSlot - * @param ignoreEvolution - * @param postProcess + * @param speciesPool - The pool of species to choose from. Can be a list of `SpeciesId` or a list of lists of `SpeciesId`. + * @param trainerSlot - (default {@linkcode TrainerSlot.TRAINER | TRAINER}); The trainer slot to generate for. + * @param ignoreEvolution - (default `false`); Whether to ignore evolution when determining the species to use. + * @param postProcess - An optional function to post-process the generated `EnemyPokemon` */ export function getRandomPartyMemberFunc( - speciesPool: SpeciesId[], + speciesPool: (SpeciesId | SpeciesId[])[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution = false, postProcess?: (enemyPokemon: EnemyPokemon) => void, -) { +): (level: number, strength: PartyMemberStrength) => EnemyPokemon { return (level: number, strength: PartyMemberStrength) => { - let species = randSeedItem(speciesPool); + let species: SpeciesId | SpeciesId[] | typeof speciesPool = speciesPool; + do { + species = randSeedItem(species); + } while (Array.isArray(species)); + if (!ignoreEvolution) { - species = getPokemonSpecies(species).getTrainerSpeciesForLevel( - level, - true, - strength, - // TODO: What EvoLevelThresholdKind to use here? - ); + species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength); } + return globalScene.addEnemyPokemon( getPokemonSpecies(species), level, @@ -1026,6 +1029,7 @@ export function getRandomPartyMemberFunc( }; } +// biome-ignore lint/correctness/noUnusedVariables: potentially useful function getSpeciesFilterRandomPartyMemberFunc( originalSpeciesFilter: PokemonSpeciesFilter, trainerSlot: TrainerSlot = TrainerSlot.TRAINER, @@ -1050,6 +1054,7 @@ function getSpeciesFilterRandomPartyMemberFunc( }; } +let t = 0; export const trainerConfigs: TrainerConfigs = { [TrainerType.UNKNOWN]: new TrainerConfig(0).setHasGenders(), [TrainerType.ACE_TRAINER]: new TrainerConfig(++t) @@ -4548,61 +4553,8 @@ export const trainerConfigs: TrainerConfigs = { () => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE, ) - .setPartyMemberFunc( - 0, - getRandomPartyMemberFunc( - [ - SpeciesId.BULBASAUR, - SpeciesId.CHARMANDER, - SpeciesId.SQUIRTLE, - SpeciesId.CHIKORITA, - SpeciesId.CYNDAQUIL, - SpeciesId.TOTODILE, - SpeciesId.TREECKO, - SpeciesId.TORCHIC, - SpeciesId.MUDKIP, - SpeciesId.TURTWIG, - SpeciesId.CHIMCHAR, - SpeciesId.PIPLUP, - SpeciesId.SNIVY, - SpeciesId.TEPIG, - SpeciesId.OSHAWOTT, - SpeciesId.CHESPIN, - SpeciesId.FENNEKIN, - SpeciesId.FROAKIE, - SpeciesId.ROWLET, - SpeciesId.LITTEN, - SpeciesId.POPPLIO, - SpeciesId.GROOKEY, - SpeciesId.SCORBUNNY, - SpeciesId.SOBBLE, - SpeciesId.SPRIGATITO, - SpeciesId.FUECOCO, - SpeciesId.QUAXLY, - ], - TrainerSlot.TRAINER, - true, - p => (p.abilityIndex = 0), - ), - ) - .setPartyMemberFunc( - 1, - getRandomPartyMemberFunc( - [ - SpeciesId.PIDGEY, - SpeciesId.HOOTHOOT, - SpeciesId.TAILLOW, - SpeciesId.STARLY, - SpeciesId.PIDOVE, - SpeciesId.FLETCHLING, - SpeciesId.PIKIPEK, - SpeciesId.ROOKIDEE, - SpeciesId.WATTREL, - ], - TrainerSlot.TRAINER, - true, - ), - ), + .setPartyMemberFunc(0, getRandomRivalPartyMemberFunc(RIVAL_1_POOL, 0)) + .setPartyMemberFunc(1, getRandomRivalPartyMemberFunc(RIVAL_1_POOL, 1)), [TrainerType.RIVAL_2]: new TrainerConfig(++t) .setName("Finn") .setHasGenders("Ivy") @@ -4615,70 +4567,9 @@ export const trainerConfigs: TrainerConfigs = { .setMixedBattleBgm("battle_rival") .setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) - .setPartyMemberFunc( - 0, - getRandomPartyMemberFunc( - [ - SpeciesId.IVYSAUR, - SpeciesId.CHARMELEON, - SpeciesId.WARTORTLE, - SpeciesId.BAYLEEF, - SpeciesId.QUILAVA, - SpeciesId.CROCONAW, - SpeciesId.GROVYLE, - SpeciesId.COMBUSKEN, - SpeciesId.MARSHTOMP, - SpeciesId.GROTLE, - SpeciesId.MONFERNO, - SpeciesId.PRINPLUP, - SpeciesId.SERVINE, - SpeciesId.PIGNITE, - SpeciesId.DEWOTT, - SpeciesId.QUILLADIN, - SpeciesId.BRAIXEN, - SpeciesId.FROGADIER, - SpeciesId.DARTRIX, - SpeciesId.TORRACAT, - SpeciesId.BRIONNE, - SpeciesId.THWACKEY, - SpeciesId.RABOOT, - SpeciesId.DRIZZILE, - SpeciesId.FLORAGATO, - SpeciesId.CROCALOR, - SpeciesId.QUAXWELL, - ], - TrainerSlot.TRAINER, - true, - p => (p.abilityIndex = 0), - ), - ) - .setPartyMemberFunc( - 1, - getRandomPartyMemberFunc( - [ - SpeciesId.PIDGEOTTO, - SpeciesId.HOOTHOOT, - SpeciesId.TAILLOW, - SpeciesId.STARAVIA, - SpeciesId.TRANQUILL, - SpeciesId.FLETCHINDER, - SpeciesId.TRUMBEAK, - SpeciesId.CORVISQUIRE, - SpeciesId.WATTREL, - ], - TrainerSlot.TRAINER, - true, - ), - ) - .setPartyMemberFunc( - 2, - getSpeciesFilterRandomPartyMemberFunc( - (species: PokemonSpecies) => - !pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && species.baseTotal >= 450, - ), - ), + .setPartyMemberFunc(0, getRandomRivalPartyMemberFunc(RIVAL_2_POOL, 0)) + .setPartyMemberFunc(1, getRandomRivalPartyMemberFunc(RIVAL_2_POOL, 1)) + .setPartyMemberFunc(2, getRandomRivalPartyMemberFunc(RIVAL_2_POOL, 2)), [TrainerType.RIVAL_3]: new TrainerConfig(++t) .setName("Finn") .setHasGenders("Ivy") @@ -4690,71 +4581,10 @@ export const trainerConfigs: TrainerConfigs = { .setBattleBgm("battle_rival") .setMixedBattleBgm("battle_rival") .setPartyTemplates(trainerPartyTemplates.RIVAL_3) - .setPartyMemberFunc( - 0, - getRandomPartyMemberFunc( - [ - SpeciesId.VENUSAUR, - SpeciesId.CHARIZARD, - SpeciesId.BLASTOISE, - SpeciesId.MEGANIUM, - SpeciesId.TYPHLOSION, - SpeciesId.FERALIGATR, - SpeciesId.SCEPTILE, - SpeciesId.BLAZIKEN, - SpeciesId.SWAMPERT, - SpeciesId.TORTERRA, - SpeciesId.INFERNAPE, - SpeciesId.EMPOLEON, - SpeciesId.SERPERIOR, - SpeciesId.EMBOAR, - SpeciesId.SAMUROTT, - SpeciesId.CHESNAUGHT, - SpeciesId.DELPHOX, - SpeciesId.GRENINJA, - SpeciesId.DECIDUEYE, - SpeciesId.INCINEROAR, - SpeciesId.PRIMARINA, - SpeciesId.RILLABOOM, - SpeciesId.CINDERACE, - SpeciesId.INTELEON, - SpeciesId.MEOWSCARADA, - SpeciesId.SKELEDIRGE, - SpeciesId.QUAQUAVAL, - ], - TrainerSlot.TRAINER, - true, - p => (p.abilityIndex = 0), - ), - ) - .setPartyMemberFunc( - 1, - getRandomPartyMemberFunc( - [ - SpeciesId.PIDGEOT, - SpeciesId.NOCTOWL, - SpeciesId.SWELLOW, - SpeciesId.STARAPTOR, - SpeciesId.UNFEZANT, - SpeciesId.TALONFLAME, - SpeciesId.TOUCANNON, - SpeciesId.CORVIKNIGHT, - SpeciesId.KILOWATTREL, - ], - TrainerSlot.TRAINER, - true, - ), - ) - .setPartyMemberFunc( - 2, - getSpeciesFilterRandomPartyMemberFunc( - (species: PokemonSpecies) => - !pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && species.baseTotal >= 450, - ), - ) - .setSpeciesFilter(species => species.baseTotal >= 540), + .setPartyMemberFunc(0, getRandomRivalPartyMemberFunc(RIVAL_3_POOL, 0)) + .setPartyMemberFunc(1, getRandomRivalPartyMemberFunc(RIVAL_3_POOL, 1)) + .setPartyMemberFunc(2, getRandomRivalPartyMemberFunc(RIVAL_3_POOL, 2)) + .setPartyMemberFunc(3, getRandomRivalPartyMemberFunc(RIVAL_3_POOL, 3)), [TrainerType.RIVAL_4]: new TrainerConfig(++t) .setName("Finn") .setHasGenders("Ivy") @@ -4768,74 +4598,11 @@ export const trainerConfigs: TrainerConfigs = { .setMixedBattleBgm("battle_rival_2") .setPartyTemplates(trainerPartyTemplates.RIVAL_4) .setModifierRewardFuncs(() => modifierTypes.TERA_ORB) - .setPartyMemberFunc( - 0, - getRandomPartyMemberFunc( - [ - SpeciesId.VENUSAUR, - SpeciesId.CHARIZARD, - SpeciesId.BLASTOISE, - SpeciesId.MEGANIUM, - SpeciesId.TYPHLOSION, - SpeciesId.FERALIGATR, - SpeciesId.SCEPTILE, - SpeciesId.BLAZIKEN, - SpeciesId.SWAMPERT, - SpeciesId.TORTERRA, - SpeciesId.INFERNAPE, - SpeciesId.EMPOLEON, - SpeciesId.SERPERIOR, - SpeciesId.EMBOAR, - SpeciesId.SAMUROTT, - SpeciesId.CHESNAUGHT, - SpeciesId.DELPHOX, - SpeciesId.GRENINJA, - SpeciesId.DECIDUEYE, - SpeciesId.INCINEROAR, - SpeciesId.PRIMARINA, - SpeciesId.RILLABOOM, - SpeciesId.CINDERACE, - SpeciesId.INTELEON, - SpeciesId.MEOWSCARADA, - SpeciesId.SKELEDIRGE, - SpeciesId.QUAQUAVAL, - ], - TrainerSlot.TRAINER, - true, - p => { - p.abilityIndex = 0; - p.teraType = p.species.type1; - }, - ), - ) - .setPartyMemberFunc( - 1, - getRandomPartyMemberFunc( - [ - SpeciesId.PIDGEOT, - SpeciesId.NOCTOWL, - SpeciesId.SWELLOW, - SpeciesId.STARAPTOR, - SpeciesId.UNFEZANT, - SpeciesId.TALONFLAME, - SpeciesId.TOUCANNON, - SpeciesId.CORVIKNIGHT, - SpeciesId.KILOWATTREL, - ], - TrainerSlot.TRAINER, - true, - ), - ) - .setPartyMemberFunc( - 2, - getSpeciesFilterRandomPartyMemberFunc( - (species: PokemonSpecies) => - !pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && species.baseTotal >= 450, - ), - ) - .setSpeciesFilter(species => species.baseTotal >= 540) + .setPartyMemberFunc(0, getRandomRivalPartyMemberFunc(RIVAL_4_POOL, 0)) + .setPartyMemberFunc(1, getRandomRivalPartyMemberFunc(RIVAL_4_POOL, 1)) + .setPartyMemberFunc(2, getRandomRivalPartyMemberFunc(RIVAL_4_POOL, 2)) + .setPartyMemberFunc(3, getRandomRivalPartyMemberFunc(RIVAL_4_POOL, 3)) + .setPartyMemberFunc(4, getRandomRivalPartyMemberFunc(RIVAL_4_POOL, 4)) .setInstantTera(0), // Tera starter to primary type [TrainerType.RIVAL_5]: new TrainerConfig(++t) .setName("Finn") @@ -4844,89 +4611,17 @@ export const trainerConfigs: TrainerConfigs = { .setTitle("Rival") .setBoss() .setStaticParty() - .setMoneyMultiplier(2.25) + .setMoneyMultiplier(2.5) .setEncounterBgm(TrainerType.RIVAL) .setBattleBgm("battle_rival_3") .setMixedBattleBgm("battle_rival_3") .setPartyTemplates(trainerPartyTemplates.RIVAL_5) - .setPartyMemberFunc( - 0, - getRandomPartyMemberFunc( - [ - SpeciesId.VENUSAUR, - SpeciesId.CHARIZARD, - SpeciesId.BLASTOISE, - SpeciesId.MEGANIUM, - SpeciesId.TYPHLOSION, - SpeciesId.FERALIGATR, - SpeciesId.SCEPTILE, - SpeciesId.BLAZIKEN, - SpeciesId.SWAMPERT, - SpeciesId.TORTERRA, - SpeciesId.INFERNAPE, - SpeciesId.EMPOLEON, - SpeciesId.SERPERIOR, - SpeciesId.EMBOAR, - SpeciesId.SAMUROTT, - SpeciesId.CHESNAUGHT, - SpeciesId.DELPHOX, - SpeciesId.GRENINJA, - SpeciesId.DECIDUEYE, - SpeciesId.INCINEROAR, - SpeciesId.PRIMARINA, - SpeciesId.RILLABOOM, - SpeciesId.CINDERACE, - SpeciesId.INTELEON, - SpeciesId.MEOWSCARADA, - SpeciesId.SKELEDIRGE, - SpeciesId.QUAQUAVAL, - ], - TrainerSlot.TRAINER, - true, - p => { - p.setBoss(true, 2); - p.abilityIndex = 0; - p.teraType = p.species.type1; - }, - ), - ) - .setPartyMemberFunc( - 1, - getRandomPartyMemberFunc( - [ - SpeciesId.PIDGEOT, - SpeciesId.NOCTOWL, - SpeciesId.SWELLOW, - SpeciesId.STARAPTOR, - SpeciesId.UNFEZANT, - SpeciesId.TALONFLAME, - SpeciesId.TOUCANNON, - SpeciesId.CORVIKNIGHT, - SpeciesId.KILOWATTREL, - ], - TrainerSlot.TRAINER, - true, - ), - ) - .setPartyMemberFunc( - 2, - getSpeciesFilterRandomPartyMemberFunc( - (species: PokemonSpecies) => - !pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && species.baseTotal >= 450, - ), - ) - .setSpeciesFilter(species => species.baseTotal >= 540) - .setPartyMemberFunc( - 5, - getRandomPartyMemberFunc([SpeciesId.RAYQUAZA], TrainerSlot.TRAINER, true, p => { - p.setBoss(true, 3); - p.pokeball = PokeballType.MASTER_BALL; - p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0; - p.variant = 1; - }), - ) + .setPartyMemberFunc(0, getRandomRivalPartyMemberFunc(RIVAL_5_POOL, 0)) + .setPartyMemberFunc(1, getRandomRivalPartyMemberFunc(RIVAL_5_POOL, 1)) + .setPartyMemberFunc(2, getRandomRivalPartyMemberFunc(RIVAL_5_POOL, 2)) + .setPartyMemberFunc(3, getRandomRivalPartyMemberFunc(RIVAL_5_POOL, 3)) + .setPartyMemberFunc(4, getRandomRivalPartyMemberFunc(RIVAL_5_POOL, 4)) + .setPartyMemberFunc(5, getRandomRivalPartyMemberFunc(RIVAL_5_POOL, 5)) .setInstantTera(0), // Tera starter to primary type [TrainerType.RIVAL_6]: new TrainerConfig(++t) .setName("Finn") @@ -4940,94 +4635,13 @@ export const trainerConfigs: TrainerConfigs = { .setBattleBgm("battle_rival_3") .setMixedBattleBgm("battle_rival_3") .setPartyTemplates(trainerPartyTemplates.RIVAL_6) - .setPartyMemberFunc( - 0, - getRandomPartyMemberFunc( - [ - SpeciesId.VENUSAUR, - SpeciesId.CHARIZARD, - SpeciesId.BLASTOISE, - SpeciesId.MEGANIUM, - SpeciesId.TYPHLOSION, - SpeciesId.FERALIGATR, - SpeciesId.SCEPTILE, - SpeciesId.BLAZIKEN, - SpeciesId.SWAMPERT, - SpeciesId.TORTERRA, - SpeciesId.INFERNAPE, - SpeciesId.EMPOLEON, - SpeciesId.SERPERIOR, - SpeciesId.EMBOAR, - SpeciesId.SAMUROTT, - SpeciesId.CHESNAUGHT, - SpeciesId.DELPHOX, - SpeciesId.GRENINJA, - SpeciesId.DECIDUEYE, - SpeciesId.INCINEROAR, - SpeciesId.PRIMARINA, - SpeciesId.RILLABOOM, - SpeciesId.CINDERACE, - SpeciesId.INTELEON, - SpeciesId.MEOWSCARADA, - SpeciesId.SKELEDIRGE, - SpeciesId.QUAQUAVAL, - ], - TrainerSlot.TRAINER, - true, - p => { - p.setBoss(true, 3); - p.abilityIndex = 0; - p.teraType = p.species.type1; - p.generateAndPopulateMoveset(); - }, - ), - ) - .setPartyMemberFunc( - 1, - getRandomPartyMemberFunc( - [ - SpeciesId.PIDGEOT, - SpeciesId.NOCTOWL, - SpeciesId.SWELLOW, - SpeciesId.STARAPTOR, - SpeciesId.UNFEZANT, - SpeciesId.TALONFLAME, - SpeciesId.TOUCANNON, - SpeciesId.CORVIKNIGHT, - SpeciesId.KILOWATTREL, - ], - TrainerSlot.TRAINER, - true, - p => { - p.setBoss(true, 2); - p.generateAndPopulateMoveset(); - }, - ), - ) - .setPartyMemberFunc( - 2, - getSpeciesFilterRandomPartyMemberFunc( - (species: PokemonSpecies) => - !pokemonEvolutions.hasOwnProperty(species.speciesId) - && !pokemonPrevolutions.hasOwnProperty(species.speciesId) - && species.baseTotal >= 450, - ), - ) - .setSpeciesFilter(species => species.baseTotal >= 540) - .setPartyMemberFunc( - 5, - getRandomPartyMemberFunc([SpeciesId.RAYQUAZA], TrainerSlot.TRAINER, true, p => { - p.setBoss(); - p.generateAndPopulateMoveset(); - p.pokeball = PokeballType.MASTER_BALL; - p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0; - p.variant = 1; - p.formIndex = 1; // Mega Rayquaza - p.generateName(); - }), - ) + .setPartyMemberFunc(0, getRandomRivalPartyMemberFunc(RIVAL_6_POOL, 0)) + .setPartyMemberFunc(1, getRandomRivalPartyMemberFunc(RIVAL_6_POOL, 1)) + .setPartyMemberFunc(2, getRandomRivalPartyMemberFunc(RIVAL_6_POOL, 2)) + .setPartyMemberFunc(3, getRandomRivalPartyMemberFunc(RIVAL_6_POOL, 3)) + .setPartyMemberFunc(4, getRandomRivalPartyMemberFunc(RIVAL_6_POOL, 4)) + .setPartyMemberFunc(5, getRandomRivalPartyMemberFunc(RIVAL_6_POOL, 5)) .setInstantTera(0), // Tera starter to primary type - [TrainerType.ROCKET_BOSS_GIOVANNI_1]: new TrainerConfig((t = TrainerType.ROCKET_BOSS_GIOVANNI_1)) .setName("Giovanni") .initForEvilTeamLeader("Rocket Boss", []) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index a4529fc54a5..9bb4c54d448 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1304,7 +1304,6 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { const variableTypeAttr = move.getAttrs("VariableMoveTypeAttr")[0]; const types = variableTypeAttr?.getTypesForItemSpawn(p, move) ?? [move.type]; for (const type of types) { - console.info("%cConsidering type " + PokemonType[type], "color: orange"); const currentWeight = attackMoveTypeWeights.get(type) ?? 0; if (currentWeight < 3) { attackMoveTypeWeights.set(type, currentWeight + 1); @@ -1319,11 +1318,9 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { } const randInt = randSeedInt(totalWeight); - console.log("%cTotal weight " + totalWeight + ", rolled " + randInt, "color: orange"); let weight = 0; for (const [type, typeWeight] of attackMoveTypeWeights.entries()) { - console.log("%cWeighted type " + PokemonType[type] + " with weight " + typeWeight, "color: orange"); if (randInt < weight + typeWeight) { return new AttackTypeBoosterModifierType(type, TYPE_BOOST_ITEM_BOOST_PERCENT); } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index a171be51145..4b80b0852f6 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -292,7 +292,10 @@ export class EncounterPhase extends BattlePhase { } globalScene.ui.setMode(UiMode.MESSAGE).then(() => { - if (!this.loaded) { + if (this.loaded) { + this.doEncounter(); + globalScene.resetSeed(); + } else { this.trySetWeatherIfNewBiome(); // Set weather before session gets saved // Game syncs to server on waves X1 and X6 (As of 1.2.0) globalScene.gameData @@ -305,9 +308,6 @@ export class EncounterPhase extends BattlePhase { this.doEncounter(); globalScene.resetSeed(); }); - } else { - this.doEncounter(); - globalScene.resetSeed(); } }); }); @@ -490,36 +490,25 @@ export class EncounterPhase extends BattlePhase { this.end(); }; - if (showEncounterMessage) { - const introDialogue = encounter.dialogue.intro; - if (!introDialogue) { - doShowEncounterOptions(); - } else { - const FIRST_DIALOGUE_PROMPT_DELAY = 750; - let i = 0; - const showNextDialogue = () => { - const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue; - const dialogue = introDialogue[i]; - const title = getEncounterText(dialogue?.speaker); - const text = getEncounterText(dialogue.text)!; - i++; - if (title) { - globalScene.ui.showDialogue( - text, - title, - null, - nextAction, - 0, - i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, - ); - } else { - globalScene.ui.showText(text, null, nextAction, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, true); - } - }; - - if (introDialogue.length > 0) { - showNextDialogue(); + const introDialogue = encounter.dialogue.intro; + if (showEncounterMessage && introDialogue) { + const FIRST_DIALOGUE_PROMPT_DELAY = 750; + let i = 0; + const showNextDialogue = () => { + const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue; + const dialogue = introDialogue[i]; + const title = getEncounterText(dialogue?.speaker); + const text = getEncounterText(dialogue.text)!; + i++; + if (title) { + globalScene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + globalScene.ui.showText(text, null, nextAction, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, true); } + }; + + if (introDialogue.length > 0) { + showNextDialogue(); } } else { doShowEncounterOptions(); @@ -528,13 +517,13 @@ export class EncounterPhase extends BattlePhase { const encounterMessage = i18next.t("battle:mysteryEncounterAppeared"); - if (!encounterMessage) { - doEncounter(); - } else { + if (encounterMessage) { doTrainerExclamation(); globalScene.ui.showDialogue(encounterMessage, "???", null, () => { globalScene.charSprite.hide().then(() => globalScene.hideFieldOverlay(250).then(() => doEncounter())); }); + } else { + doEncounter(); } } }