diff --git a/config/formats.ts b/config/formats.ts index d7a1671c93..89a8384568 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -488,11 +488,13 @@ export const Formats: import('../sim/dex-formats').FormatList = [ column: 2, }, { - name: "[Gen 9] BSS Factory (Bo3)", - desc: `Randomized 3v3 Singles featuring Pokémon and movesets popular in Battle Stadium Singles.`, - mod: 'gen9', - team: 'randomBSSFactory', - ruleset: ['Flat Rules', 'VGC Timer', 'Best of = 3'], + name: "[Gen 9] Godly Gift Random Battle", + desc: `Each Pokémon receives one base stat from the God in the first slot depending on its position in the team.`, + team: 'randomGodlyGift', + ruleset: ['[Gen 9] Random Battle', 'Godly Gift Mod', 'Team Preview'], + onBegin() { + this.add(`raw|
In this format, the "God" in the first slot has "gifted" (shared) its base attack to the Pokémon in the second slot, defense to the one in the third slot, etc."`); + }, }, { name: "[Gen 9] Linked", diff --git a/data/random-battles/gen9/teams.ts b/data/random-battles/gen9/teams.ts index 4569c1dabc..266f9b65ec 100644 --- a/data/random-battles/gen9/teams.ts +++ b/data/random-battles/gen9/teams.ts @@ -1920,6 +1920,216 @@ export class RandomTeams { return pokemon; } + randomGodlyGiftTeam() { + this.enforceNoDirectCustomBanlistChanges(); + + const seed = this.prng.getSeed(); + const ruleTable = this.dex.formats.getRuleTable(this.format); + const pokemon: RandomTeamsTypes.RandomSet[] = []; + + // For Monotype + const isMonotype = !!this.forceMonotype || ruleTable.has('sametypeclause'); + const isDoubles = this.format.gameType !== 'singles'; + const typePool = this.dex.types.names().filter(name => name !== "Stellar"); + const type = this.forceMonotype || this.sample(typePool); + + // PotD stuff + const usePotD = global.Config && Config.potd && ruleTable.has('potd'); + const potd = usePotD ? this.dex.species.get(Config.potd) : null; + + const baseFormes: { [k: string]: number } = {}; + + const typeCount: { [k: string]: number } = {}; + const typeComboCount: { [k: string]: number } = {}; + const typeWeaknesses: { [k: string]: number } = {}; + const typeDoubleWeaknesses: { [k: string]: number } = {}; + const teamDetails: RandomTeamsTypes.TeamDetails = {}; + let numMaxLevelPokemon = 0; + + const pokemonList = isDoubles ? Object.keys(this.randomDoublesSets) : Object.keys(this.randomSets); + // God Pokemon are those with at least 510 BST, not including HP + const godPokemonList = pokemonList.filter(poke => { + const baseStats = this.dex.species.get(poke).baseStats; + return baseStats.atk + baseStats.def + baseStats.spa + baseStats.spd + baseStats.spe >= 510; + }); + const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, pokemonList); + const [godPokemonPool, godBaseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, godPokemonList); + + while (baseSpeciesPool.length && pokemon.length < this.maxTeamSize) { + let baseSpecies, species; + // Generate a God Pokemon in the lead slot + if (pokemon.length === 0) { + baseSpecies = this.sampleNoReplace(godBaseSpeciesPool); + species = this.dex.species.get(this.sample(godPokemonPool[baseSpecies])); + } else { + baseSpecies = this.sampleNoReplace(baseSpeciesPool); + species = this.dex.species.get(this.sample(pokemonPool[baseSpecies])); + + // Ensure that the species isn't harmed by Godly Gift stat passing + if (pokemon.length < 6) { + const s: StatID[] = ["hp", "atk", "def", "spa", "spd", "spe"]; + const passedStatName = s[pokemon.length]; + const passedStat = (this.dex.species.get(pokemon[0].speciesId).baseStats[passedStatName]); + // If Deoxys-Attack is the god, just make sure the def/spd stat is <= 50 + if (Math.max(passedStat, 50) < species.baseStats[passedStatName]) continue; + } + } + if (!species.exists) continue; + + // Limit to one of each species (Species Clause) + if (baseFormes[species.baseSpecies]) continue; + + // Treat Ogerpon formes and Terapagos like the Tera Blast user role; reject if team has one already + if (['ogerpon', 'ogerponhearthflame', 'terapagos'].includes(species.id) && teamDetails.teraBlast) continue; + + // Illusion shouldn't be on the last slot + if (species.baseSpecies === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue; + + const types = species.types; + const typeCombo = types.slice().sort().join(); + const weakToFreezeDry = ( + this.dex.getEffectiveness('Ice', species) > 0 || + (this.dex.getEffectiveness('Ice', species) > -2 && types.includes('Water')) + ); + // Dynamically scale limits for different team sizes. The default and minimum value is 1. + const limitFactor = Math.round(this.maxTeamSize / 6) || 1; + + if (!isMonotype && !this.forceMonotype) { + let skip = false; + + // Limit two of any type + for (const typeName of types) { + if (typeCount[typeName] >= 2 * limitFactor) { + skip = true; + break; + } + } + if (skip) continue; + + // Limit three weak to any type, and one double weak to any type + for (const typeName of this.dex.types.names()) { + // it's weak to the type + if (this.dex.getEffectiveness(typeName, species) > 0) { + if (!typeWeaknesses[typeName]) typeWeaknesses[typeName] = 0; + if (typeWeaknesses[typeName] >= 3 * limitFactor) { + skip = true; + break; + } + } + if (this.dex.getEffectiveness(typeName, species) > 1) { + if (!typeDoubleWeaknesses[typeName]) typeDoubleWeaknesses[typeName] = 0; + if (typeDoubleWeaknesses[typeName] >= limitFactor) { + skip = true; + break; + } + } + } + if (skip) continue; + + // Count Dry Skin/Fluffy as Fire weaknesses + if ( + this.dex.getEffectiveness('Fire', species) === 0 && + Object.values(species.abilities).filter(a => ['Dry Skin', 'Fluffy'].includes(a)).length + ) { + if (!typeWeaknesses['Fire']) typeWeaknesses['Fire'] = 0; + if (typeWeaknesses['Fire'] >= 3 * limitFactor) continue; + } + + // Limit four weak to Freeze-Dry + if (weakToFreezeDry) { + if (!typeWeaknesses['Freeze-Dry']) typeWeaknesses['Freeze-Dry'] = 0; + if (typeWeaknesses['Freeze-Dry'] >= 4 * limitFactor) continue; + } + + // Limit one level 100 Pokemon + if (!this.adjustLevel && (this.getLevel(species, isDoubles) === 100) && numMaxLevelPokemon >= limitFactor) { + continue; + } + + // Check compatibility with team + if (!this.getPokemonCompatibility(species, pokemon, isDoubles)) continue; + } + + // Limit three of any type combination in Monotype + if (!this.forceMonotype && isMonotype && (typeComboCount[typeCombo] >= 3 * limitFactor)) continue; + + // The Pokemon of the Day + if (potd?.exists && (pokemon.length === 1 || this.maxTeamSize === 1)) species = potd; + + const set = this.randomSet(species, teamDetails, false, isDoubles); + pokemon.push(set); + + // Don't bother tracking details for the last Pokemon + if (pokemon.length === this.maxTeamSize) break; + + // Now that our Pokemon has passed all checks, we can increment our counters + baseFormes[species.baseSpecies] = 1; + + // Increment type counters + for (const typeName of types) { + if (typeName in typeCount) { + typeCount[typeName]++; + } else { + typeCount[typeName] = 1; + } + } + if (typeCombo in typeComboCount) { + typeComboCount[typeCombo]++; + } else { + typeComboCount[typeCombo] = 1; + } + + // Increment weakness counter + for (const typeName of this.dex.types.names()) { + // it's weak to the type + if (this.dex.getEffectiveness(typeName, species) > 0) { + typeWeaknesses[typeName]++; + } + if (this.dex.getEffectiveness(typeName, species) > 1) { + typeDoubleWeaknesses[typeName]++; + } + } + // Count Dry Skin/Fluffy as Fire weaknesses + if (['Dry Skin', 'Fluffy'].includes(set.ability) && this.dex.getEffectiveness('Fire', species) === 0) { + typeWeaknesses['Fire']++; + } + if (weakToFreezeDry) typeWeaknesses['Freeze-Dry']++; + + // Increment level 100 counter + if (set.level === 100) numMaxLevelPokemon++; + + // Track what the team has + if (set.ability === 'Drizzle' || set.moves.includes('raindance')) teamDetails.rain = 1; + if (set.ability === 'Drought' || set.ability === 'Orichalcum Pulse' || set.moves.includes('sunnyday')) { + teamDetails.sun = 1; + } + if (set.ability === 'Sand Stream') teamDetails.sand = 1; + if (set.ability === 'Snow Warning' || set.moves.includes('snowscape') || set.moves.includes('chillyreception')) { + teamDetails.snow = 1; + } + if (set.moves.includes('healbell')) teamDetails.statusCure = 1; + if (set.moves.includes('spikes') || set.moves.includes('ceaselessedge')) { + teamDetails.spikes = (teamDetails.spikes || 0) + 1; + } + if (set.moves.includes('toxicspikes') || set.ability === 'Toxic Debris') teamDetails.toxicSpikes = 1; + if (set.moves.includes('stealthrock') || set.moves.includes('stoneaxe')) teamDetails.stealthRock = 1; + if (set.moves.includes('stickyweb')) teamDetails.stickyWeb = 1; + if (set.moves.includes('defog')) teamDetails.defog = 1; + if (set.moves.includes('rapidspin') || set.moves.includes('mortalspin')) teamDetails.rapidSpin = 1; + if (set.moves.includes('auroraveil') || (set.moves.includes('reflect') && set.moves.includes('lightscreen'))) { + teamDetails.screens = 1; + } + if (set.role === 'Tera Blast user' || ['ogerpon', 'ogerponhearthflame', 'terapagos'].includes(species.id)) { + teamDetails.teraBlast = 1; + } + } + if (pokemon.length < this.maxTeamSize && pokemon.length < 12) { // large teams sometimes cannot be built + throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`); + } + + return pokemon; + } + randomCCTeam(): RandomTeamsTypes.RandomSet[] { this.enforceNoDirectCustomBanlistChanges();