import { Dex, toID } from '../../../sim/dex'; import { Utils } from '../../../lib'; import { PRNG, type PRNGSeed } from '../../../sim/prng'; import { type RuleTable } from '../../../sim/dex-formats'; import { Tags } from './../../tags'; export interface TeamData { typeCount: { [k: string]: number }; typeComboCount: { [k: string]: number }; baseFormes: { [k: string]: number }; megaCount?: number; zCount?: number; has: { [k: string]: number }; forceResult: boolean; weaknesses: { [k: string]: number }; resistances: { [k: string]: number }; weather?: string; eeveeLimCount?: number; gigantamax?: boolean; } export interface BattleFactorySpecies { flags: { limEevee?: 1 }; sets: BattleFactorySet[]; } export interface OldRandomBattleSpecies { level?: number; moves?: ID[]; doublesLevel?: number; doublesMoves?: ID[]; noDynamaxMoves?: ID[]; } interface BattleFactorySet { species: string; item: string; ability: string; nature: string; moves: string[]; evs?: Partial; ivs?: Partial; } export class MoveCounter extends Utils.Multiset { damagingMoves: Set; setupType: string; constructor() { super(); this.damagingMoves = new Set(); this.setupType = ''; } } type MoveEnforcementChecker = ( movePool: string[], moves: Set, abilities: string[], types: Set, counter: MoveCounter, species: Species, teamDetails: RandomTeamsTypes.TeamDetails ) => boolean; // Moves that restore HP: const RECOVERY_MOVES = [ 'healorder', 'milkdrink', 'moonlight', 'morningsun', 'recover', 'roost', 'shoreup', 'slackoff', 'softboiled', 'strengthsap', 'synthesis', ]; // Moves that drop stats: const CONTRARY_MOVES = [ 'closecombat', 'leafstorm', 'overheat', 'superpower', 'vcreate', ]; // Moves that boost Attack: const PHYSICAL_SETUP = [ 'bellydrum', 'bulkup', 'coil', 'curse', 'dragondance', 'honeclaws', 'howl', 'meditate', 'poweruppunch', 'screech', 'swordsdance', ]; // Moves which boost Special Attack: const SPECIAL_SETUP = [ 'calmmind', 'chargebeam', 'geomancy', 'nastyplot', 'quiverdance', 'tailglow', ]; // Moves that boost Attack AND Special Attack: const MIXED_SETUP = [ 'clangoroussoul', 'growth', 'happyhour', 'holdhands', 'noretreat', 'shellsmash', 'workup', ]; // Some moves that only boost Speed: const SPEED_SETUP = [ 'agility', 'autotomize', 'flamecharge', 'rockpolish', ]; // Moves that shouldn't be the only STAB moves: const NO_STAB = [ 'accelerock', 'aquajet', 'beakblast', 'bounce', 'breakingswipe', 'chatter', 'clearsmog', 'dragontail', 'eruption', 'explosion', 'fakeout', 'firstimpression', 'flamecharge', 'flipturn', 'iceshard', 'icywind', 'incinerate', 'machpunch', 'meteorbeam', 'pluck', 'pursuit', 'quickattack', 'reversal', 'selfdestruct', 'skydrop', 'snarl', 'suckerpunch', 'uturn', 'watershuriken', 'vacuumwave', 'voltswitch', 'waterspout', ]; // Hazard-setting moves const HAZARDS = [ 'spikes', 'stealthrock', 'stickyweb', 'toxicspikes', ]; function sereneGraceBenefits(move: Move) { return move.secondary?.chance && move.secondary.chance >= 20 && move.secondary.chance < 100; } export class RandomGen8Teams { readonly dex: ModdedDex; gen: number; factoryTier: string; format: Format; prng: PRNG; noStab: string[]; priorityPokemon: string[]; readonly maxTeamSize: number; readonly adjustLevel: number | null; readonly maxMoveCount: number; readonly forceMonotype: string | undefined; randomData: { [species: string]: OldRandomBattleSpecies } = require('./data.json'); /** * Checkers for move enforcement based on a Pokémon's types or other factors * * returns true to reject one of its other moves to try to roll the forced move, false otherwise. */ moveEnforcementCheckers: { [k: string]: MoveEnforcementChecker }; /** Used by .getPools() */ private poolsCacheKey: [string | undefined, number | undefined, RuleTable | undefined, boolean] | undefined; private cachedPool: number[] | undefined; private cachedSpeciesPool: Species[] | undefined; constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { format = Dex.formats.get(format); this.dex = Dex.forFormat(format); this.gen = this.dex.gen; this.noStab = NO_STAB; this.priorityPokemon = []; const ruleTable = Dex.formats.getRuleTable(format); this.maxTeamSize = ruleTable.maxTeamSize; this.adjustLevel = ruleTable.adjustLevel; this.maxMoveCount = ruleTable.maxMoveCount; const forceMonotype = ruleTable.valueRules.get('forcemonotype'); this.forceMonotype = forceMonotype && this.dex.types.get(forceMonotype).exists ? this.dex.types.get(forceMonotype).name : undefined; this.factoryTier = ''; this.format = format; this.prng = PRNG.get(prng); this.moveEnforcementCheckers = { screens: (movePool, moves, abilities, types, counter, species, teamDetails) => { if (teamDetails.screens) return false; return ( (moves.has('lightscreen') && movePool.includes('reflect')) || (moves.has('reflect') && movePool.includes('lightscreen')) ); }, recovery: (movePool, moves, abilities, types, counter, species, teamDetails) => ( !!counter.get('Status') && !counter.setupType && ['morningsun', 'recover', 'roost', 'slackoff', 'softboiled'].some(moveid => movePool.includes(moveid)) && ['healingwish', 'switcheroo', 'trick', 'trickroom'].every(moveid => !moves.has(moveid)) ), misc: (movePool, moves, abilities, types, counter, species, teamDetails) => { if (movePool.includes('milkdrink') || movePool.includes('quiverdance')) return true; return movePool.includes('stickyweb') && !counter.setupType && !teamDetails.stickyWeb; }, lead: (movePool, moves, abilities, types, counter) => ( movePool.includes('stealthrock') && !!counter.get('Status') && !counter.setupType && !counter.get('speedsetup') && !moves.has('substitute') ), leechseed: (movePool, moves) => ( !moves.has('calmmind') && ['protect', 'substitute', 'spikyshield'].some(m => movePool.includes(m)) ), Bug: movePool => movePool.includes('megahorn'), Dark: (movePool, moves, abilities, types, counter) => { if (!counter.get('Dark')) return true; return moves.has('suckerpunch') && (movePool.includes('knockoff') || movePool.includes('wickedblow')); }, Dragon: (movePool, moves, abilities, types, counter) => ( !counter.get('Dragon') && !moves.has('dragonascent') && !moves.has('substitute') && !(moves.has('rest') && moves.has('sleeptalk')) ), Electric: (movePool, moves, abilities, types, counter) => !counter.get('Electric') || movePool.includes('thunder'), Fairy: (movePool, moves, abilities, types, counter) => ( !counter.get('Fairy') && ['dazzlinggleam', 'moonblast', 'fleurcannon', 'playrough', 'strangesteam'].some(moveid => movePool.includes(moveid)) ), Fighting: (movePool, moves, abilities, types, counter) => !counter.get('Fighting') || !counter.get('stab'), Fire: (movePool, moves, abilities, types, counter, species) => { // Entei should never reject Extreme Speed even if Flare Blitz could be rolled instead const enteiException = moves.has('extremespeed') && species.id === 'entei'; return !moves.has('bellydrum') && (!counter.get('Fire') || (!enteiException && movePool.includes('flareblitz'))); }, Flying: (movePool, moves, abilities, types, counter) => ( !counter.get('Flying') && !types.has('Dragon') && [ 'airslash', 'bravebird', 'dualwingbeat', 'oblivionwing', ].some(moveid => movePool.includes(moveid)) ), Ghost: (movePool, moves, abilities, types, counter) => { if (moves.has('nightshade')) return false; if (!counter.get('Ghost') && !types.has('Dark')) return true; if (movePool.includes('poltergeist')) return true; return movePool.includes('spectralthief') && !counter.get('Dark'); }, Grass: (movePool, moves, abilities, types, counter, species) => { if (movePool.includes('leafstorm') || movePool.includes('grassyglide')) return true; return !counter.get('Grass') && species.baseStats.atk >= 100; }, Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'), Ice: (movePool, moves, abilities, types, counter) => { if (!counter.get('Ice')) return true; if (movePool.includes('iciclecrash')) return true; return abilities.includes('Snow Warning') && movePool.includes('blizzard'); }, Normal: (movePool, moves, abilities, types, counter) => ( (abilities.includes('Guts') && movePool.includes('facade')) || (abilities.includes('Pixilate') && !counter.get('Normal')) ), Poison: (movePool, moves, abilities, types, counter) => { if (counter.get('Poison')) return false; return types.has('Ground') || types.has('Psychic') || types.has('Grass') || !!counter.setupType || movePool.includes('gunkshot'); }, Psychic: (movePool, moves, abilities, types, counter) => { if (counter.get('Psychic')) return false; if (types.has('Ghost') || types.has('Steel')) return false; return abilities.includes('Psychic Surge') || !!counter.setupType || movePool.includes('psychicfangs'); }, Rock: (movePool, moves, abilities, types, counter, species) => !counter.get('Rock') && species.baseStats.atk >= 80, Steel: (movePool, moves, abilities, types, counter, species) => { if (species.baseStats.atk < 95) return false; if (movePool.includes('meteormash')) return true; return !counter.get('Steel'); }, Water: (movePool, moves, abilities, types, counter, species) => { if (!counter.get('Water') && !moves.has('hypervoice')) return true; if (['hypervoice', 'liquidation', 'surgingstrikes'].some(m => movePool.includes(m))) return true; return abilities.includes('Huge Power') && movePool.includes('aquajet'); }, }; this.poolsCacheKey = undefined; this.cachedPool = undefined; this.cachedSpeciesPool = undefined; } setSeed(prng?: PRNG | PRNGSeed) { this.prng = PRNG.get(prng); } getTeam(options?: PlayerOptions | null): PokemonSet[] { const generatorName = ( typeof this.format.team === 'string' && this.format.team.startsWith('random') ) ? this.format.team + 'Team' : ''; // @ts-expect-error property access return this[generatorName || 'randomTeam'](options); } randomChance(numerator: number, denominator: number) { return this.prng.randomChance(numerator, denominator); } sample(items: readonly T[]): T { return this.prng.sample(items); } sampleIfArray(item: T | T[]): T { if (Array.isArray(item)) { return this.sample(item); } return item; } random(m?: number, n?: number) { return this.prng.random(m, n); } /** * Remove an element from an unsorted array significantly faster * than .splice */ fastPop(list: any[], index: number) { // If an array doesn't need to be in order, replacing the // element at the given index with the removed element // is much, much faster than using list.splice(index, 1). const length = list.length; if (index < 0 || index >= list.length) { // sanity check throw new Error(`Index ${index} out of bounds for given array`); } const element = list[index]; list[index] = list[length - 1]; list.pop(); return element; } /** * Remove a random element from an unsorted array and return it. * Uses the battle's RNG if in a battle. */ sampleNoReplace(list: any[]) { const length = list.length; if (length === 0) return null; const index = this.random(length); return this.fastPop(list, index); } /** * Removes n random elements from an unsorted array and returns them. * If n is less than the array's length, randomly removes and returns all the elements * in the array (so the returned array could have length < n). */ multipleSamplesNoReplace(list: T[], n: number): T[] { const samples = []; while (samples.length < n && list.length) { samples.push(this.sampleNoReplace(list)); } return samples; } /** * Check if user has directly tried to ban/unban/restrict things in a custom battle. * Doesn't count bans nested inside other formats/rules. */ private hasDirectCustomBanlistChanges() { if (!this.format.customRules) return false; for (const rule of this.format.customRules) { for (const banlistOperator of ['-', '+', '*']) { if (rule.startsWith(banlistOperator)) return true; } } return false; } /** * Inform user when custom bans are unsupported in a team generator. */ protected enforceNoDirectCustomBanlistChanges() { if (this.hasDirectCustomBanlistChanges()) { throw new Error(`Custom bans are not currently supported in ${this.format.name}.`); } } /** * Inform user when complex bans are unsupported in a team generator. */ protected enforceNoDirectComplexBans() { if (!this.format.customRules) return false; for (const rule of this.format.customRules) { if (rule.includes('+') && !rule.startsWith('+')) { throw new Error(`Complex bans are not currently supported in ${this.format.name}.`); } } } /** * Validate set element pool size is sufficient to support size requirements after simple bans. */ private enforceCustomPoolSizeNoComplexBans( effectTypeName: string, basicEffectPool: BasicEffect[], requiredCount: number, requiredCountExplanation: string ) { if (basicEffectPool.length >= requiredCount) return; throw new Error(`Legal ${effectTypeName} count is insufficient to support ${requiredCountExplanation} (${basicEffectPool.length} / ${requiredCount}).`); } unrejectableMovesInSingles(move: Move) { // These moves cannot be rejected in favor of a forced move in singles return (move.category !== 'Status' || !move.flags.heal) && ![ 'facade', 'leechseed', 'lightscreen', 'reflect', 'sleeptalk', 'spore', 'substitute', 'switcheroo', 'teleport', 'toxic', 'trick', ].includes(move.id); } unrejectableMovesInDoubles(move: Move) { // These moves cannot be rejected in favor of a forced move in doubles return move.id !== 'bodypress'; } randomCCTeam(): RandomTeamsTypes.RandomSet[] { this.enforceNoDirectCustomBanlistChanges(); const dex = this.dex; const team = []; const natures = this.dex.natures.all(); const items = this.dex.items.all(); const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined, undefined, true); for (let forme of randomN) { let species = dex.species.get(forme); if (species.isNonstandard) species = dex.species.get(species.baseSpecies); // Random legal item let item = ''; if (this.gen >= 2) { do { item = this.sample(items).name; } while (this.dex.items.get(item).gen > this.gen || this.dex.items.get(item).isNonstandard); } // Make sure forme is legal if (species.battleOnly) { if (typeof species.battleOnly === 'string') { species = dex.species.get(species.battleOnly); } else { species = dex.species.get(this.sample(species.battleOnly)); } forme = species.name; } else if (species.requiredItems && !species.requiredItems.some(req => toID(req) === item)) { if (!species.changesFrom) throw new Error(`${species.name} needs a changesFrom value`); species = dex.species.get(species.changesFrom); forme = species.name; } // Make sure that a base forme does not hold any forme-modifier items. let itemData = this.dex.items.get(item); if (itemData.forcedForme && forme === this.dex.species.get(itemData.forcedForme).baseSpecies) { do { itemData = this.sample(items); item = itemData.name; } while ( itemData.gen > this.gen || itemData.isNonstandard || (itemData.forcedForme && forme === this.dex.species.get(itemData.forcedForme).baseSpecies) ); } // Random legal ability const abilities = Object.values(species.abilities).filter(a => this.dex.abilities.get(a).gen <= this.gen); const ability: string = this.gen <= 2 ? 'No Ability' : this.sample(abilities); // Four random unique moves from the movepool let pool = ['struggle']; if (forme === 'Smeargle') { pool = this.dex.moves.all() .filter(move => !(move.isNonstandard || move.isZ || move.isMax || move.realMove)) .map(m => m.id); } else { pool = [...this.dex.species.getMovePool(species.id)]; } const moves = this.multipleSamplesNoReplace(pool, this.maxMoveCount); // Random EVs const evs: StatsTable = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; const s: StatID[] = ["hp", "atk", "def", "spa", "spd", "spe"]; let evpool = 510; do { const x = this.sample(s); const y = this.random(Math.min(256 - evs[x], evpool + 1)); evs[x] += y; evpool -= y; } while (evpool > 0); // Random IVs const ivs = { hp: this.random(32), atk: this.random(32), def: this.random(32), spa: this.random(32), spd: this.random(32), spe: this.random(32), }; // Random nature const nature = this.sample(natures).name; // Level balance--calculate directly from stats rather than using some silly lookup table const mbstmin = 1307; // Sunkern has the lowest modified base stat total, and that total is 807 let stats = species.baseStats; // If Wishiwashi, use the school-forme's much higher stats if (species.baseSpecies === 'Wishiwashi') stats = Dex.species.get('wishiwashischool').baseStats; // Modified base stat total assumes 31 IVs, 85 EVs in every stat let mbst = (stats["hp"] * 2 + 31 + 21 + 100) + 10; mbst += (stats["atk"] * 2 + 31 + 21 + 100) + 5; mbst += (stats["def"] * 2 + 31 + 21 + 100) + 5; mbst += (stats["spa"] * 2 + 31 + 21 + 100) + 5; mbst += (stats["spd"] * 2 + 31 + 21 + 100) + 5; mbst += (stats["spe"] * 2 + 31 + 21 + 100) + 5; let level; if (this.adjustLevel) { level = this.adjustLevel; } else { level = Math.floor(100 * mbstmin / mbst); // Initial level guess will underestimate while (level < 100) { mbst = Math.floor((stats["hp"] * 2 + 31 + 21 + 100) * level / 100 + 10); // Since damage is roughly proportional to level mbst += Math.floor(((stats["atk"] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); mbst += Math.floor((stats["def"] * 2 + 31 + 21 + 100) * level / 100 + 5); mbst += Math.floor(((stats["spa"] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); mbst += Math.floor((stats["spd"] * 2 + 31 + 21 + 100) * level / 100 + 5); mbst += Math.floor((stats["spe"] * 2 + 31 + 21 + 100) * level / 100 + 5); if (mbst >= mbstmin) break; level++; } } // Random happiness const happiness = this.random(256); // Random shininess const shiny = this.randomChance(1, 1024); const set: RandomTeamsTypes.RandomSet = { name: species.baseSpecies, species: species.name, gender: species.gender, item, ability, moves, evs, ivs, nature, level, happiness, shiny, }; if (this.gen === 9) { // Tera type set.teraType = this.sample(this.dex.types.names()); } team.push(set); } return team; } private getPools(requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { // Memoize pool and speciesPool because, at least during tests, they are constructed with the same parameters // hundreds of times and are expensive to compute. const isNotCustom = !ruleTable; let pool: number[] = []; let speciesPool: Species[] = []; const ck = this.poolsCacheKey; if (ck && this.cachedPool && this.cachedSpeciesPool && ck[0] === requiredType && ck[1] === minSourceGen && ck[2] === ruleTable && ck[3] === requireMoves) { speciesPool = this.cachedSpeciesPool.slice(); pool = this.cachedPool.slice(); } else if (isNotCustom) { speciesPool = [...this.dex.species.all()]; for (const species of speciesPool) { if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue; if (requireMoves) { const hasMovesInCurrentGen = this.dex.species.getMovePool(species.id).size; if (!hasMovesInCurrentGen) continue; } if (requiredType && !species.types.includes(requiredType)) continue; if (minSourceGen && species.gen < minSourceGen) continue; const num = species.num; if (num <= 0 || pool.includes(num)) continue; pool.push(num); } this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; this.cachedPool = pool.slice(); this.cachedSpeciesPool = speciesPool.slice(); } else { const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent']; const nonexistentBanReason = ruleTable.check('nonexistent'); // Assume tierSpecies does not differ from species here (mega formes can be used without their stone, etc) for (const species of this.dex.species.all()) { if (requiredType && !species.types.includes(requiredType)) continue; let banReason = ruleTable.check('pokemon:' + species.id); if (banReason) continue; if (banReason !== '') { if (species.isMega && ruleTable.check('pokemontag:mega')) continue; banReason = ruleTable.check('basepokemon:' + toID(species.baseSpecies)); if (banReason) continue; if (banReason !== '' || this.dex.species.get(species.baseSpecies).isNonstandard !== species.isNonstandard) { const nonexistentCheck = Tags.nonexistent.genericFilter!(species) && nonexistentBanReason; let tagWhitelisted = false; let tagBlacklisted = false; for (const ruleid of ruleTable.tagRules) { if (ruleid.startsWith('*')) continue; const tagid = ruleid.slice(12) as ID; const tag = Tags[tagid]; if ((tag.speciesFilter || tag.genericFilter)!(species)) { const existenceTag = EXISTENCE_TAG.includes(tagid); if (ruleid.startsWith('+')) { if (!existenceTag && nonexistentCheck) continue; tagWhitelisted = true; break; } tagBlacklisted = true; break; } } if (tagBlacklisted) continue; if (!tagWhitelisted) { if (ruleTable.check('pokemontag:allpokemon')) continue; } } } speciesPool.push(species); const num = species.num; if (pool.includes(num)) continue; pool.push(num); } this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; this.cachedPool = pool.slice(); this.cachedSpeciesPool = speciesPool.slice(); } return { pool, speciesPool }; } randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { // Picks `n` random pokemon--no repeats, even among formes // Also need to either normalize for formes or select formes at random // Unreleased are okay but no CAP if (requiredType && !this.dex.types.get(requiredType).exists) { throw new Error(`"${requiredType}" is not a valid type.`); } const { pool, speciesPool } = this.getPools(requiredType, minSourceGen, ruleTable, requireMoves); const isNotCustom = !ruleTable; const hasDexNumber: { [k: string]: number } = {}; for (let i = 0; i < n; i++) { const num = this.sampleNoReplace(pool); hasDexNumber[num] = i; } const formes: string[][] = []; for (const species of speciesPool) { if (!(species.num in hasDexNumber)) continue; if (isNotCustom && (species.gen > this.gen || (species.isNonstandard && species.isNonstandard !== 'Unobtainable'))) continue; if (requiredType && !species.types.includes(requiredType)) continue; if (!formes[hasDexNumber[species.num]]) formes[hasDexNumber[species.num]] = []; formes[hasDexNumber[species.num]].push(species.name); } if (formes.length < n) { throw new Error(`Legal Pokemon forme count insufficient to support Max Team Size: (${formes.length} / ${n}).`); } const nPokemon = []; for (let i = 0; i < n; i++) { if (!formes[i].length) { throw new Error(`Invalid pokemon gen ${this.gen}: ${JSON.stringify(formes)} numbers ${JSON.stringify(hasDexNumber)}`); } nPokemon.push(this.sample(formes[i])); } return nPokemon; } randomHCTeam(): PokemonSet[] { const hasCustomBans = this.hasDirectCustomBanlistChanges(); const ruleTable = this.dex.formats.getRuleTable(this.format); const hasNonexistentBan = hasCustomBans && ruleTable.check('nonexistent'); const hasNonexistentWhitelist = hasCustomBans && (hasNonexistentBan === ''); if (hasCustomBans) { this.enforceNoDirectComplexBans(); } // Item Pool const doItemsExist = this.gen > 1; let itemPool: Item[] = []; if (doItemsExist) { if (!hasCustomBans) { itemPool = [...this.dex.items.all()].filter(item => (item.gen <= this.gen && !item.isNonstandard)); } else { const hasAllItemsBan = ruleTable.check('pokemontag:allitems'); for (const item of this.dex.items.all()) { let banReason = ruleTable.check('item:' + item.id); if (banReason) continue; if (banReason !== '' && item.id) { if (hasAllItemsBan) continue; if (item.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(item.isNonstandard)); if (banReason) continue; if (banReason !== '' && item.isNonstandard !== 'Unobtainable') { if (hasNonexistentBan) continue; if (!hasNonexistentWhitelist) continue; } } } itemPool.push(item); } if (ruleTable.check('item:noitem')) { this.enforceCustomPoolSizeNoComplexBans('item', itemPool, this.maxTeamSize, 'Max Team Size'); } } } // Ability Pool const doAbilitiesExist = (this.gen > 2) && (this.dex.currentMod !== 'gen7letsgo'); let abilityPool: Ability[] = []; if (doAbilitiesExist) { if (!hasCustomBans) { abilityPool = [...this.dex.abilities.all()].filter(ability => (ability.gen <= this.gen && !ability.isNonstandard)); } else { const hasAllAbilitiesBan = ruleTable.check('pokemontag:allabilities'); for (const ability of this.dex.abilities.all()) { let banReason = ruleTable.check('ability:' + ability.id); if (banReason) continue; if (banReason !== '') { if (hasAllAbilitiesBan) continue; if (ability.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(ability.isNonstandard)); if (banReason) continue; if (banReason !== '') { if (hasNonexistentBan) continue; if (!hasNonexistentWhitelist) continue; } } } abilityPool.push(ability); } if (ruleTable.check('ability:noability')) { this.enforceCustomPoolSizeNoComplexBans('ability', abilityPool, this.maxTeamSize, 'Max Team Size'); } } } // Move Pool const setMoveCount = ruleTable.maxMoveCount; let movePool: Move[] = []; if (!hasCustomBans) { movePool = [...this.dex.moves.all()].filter(move => (move.gen <= this.gen && !move.isNonstandard && !move.name.startsWith('Hidden Power '))); } else { const hasAllMovesBan = ruleTable.check('pokemontag:allmoves'); for (const move of this.dex.moves.all()) { // Legality of specific HP types can't be altered in built formats anyway if (move.name.startsWith('Hidden Power ')) continue; let banReason = ruleTable.check('move:' + move.id); if (banReason) continue; if (banReason !== '') { if (hasAllMovesBan) continue; if (move.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(move.isNonstandard)); if (banReason) continue; if (banReason !== '' && move.isNonstandard !== 'Unobtainable') { if (hasNonexistentBan) continue; if (!hasNonexistentWhitelist) continue; } } } movePool.push(move); } this.enforceCustomPoolSizeNoComplexBans('move', movePool, this.maxTeamSize * setMoveCount, 'Max Team Size * Max Move Count'); } // Nature Pool const doNaturesExist = this.gen > 2; let naturePool: Nature[] = []; if (doNaturesExist) { if (!hasCustomBans) { naturePool = [...this.dex.natures.all()]; } else { const hasAllNaturesBan = ruleTable.check('pokemontag:allnatures'); for (const nature of this.dex.natures.all()) { let banReason = ruleTable.check('nature:' + nature.id); if (banReason) continue; if (banReason !== '' && nature.id) { if (hasAllNaturesBan) continue; if (nature.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(nature.isNonstandard)); if (banReason) continue; if (banReason !== '' && nature.isNonstandard !== 'Unobtainable') { if (hasNonexistentBan) continue; if (!hasNonexistentWhitelist) continue; } } } naturePool.push(nature); } // There is no 'nature:nonature' rule so do not constrain pool size } } const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined, hasCustomBans ? ruleTable : undefined); const team = []; for (const forme of randomN) { // Choose forme const species = this.dex.species.get(forme); // Random unique item let item = ''; let itemData; if (doItemsExist) { itemData = this.sampleNoReplace(itemPool); item = itemData?.name; } // Random unique ability let ability = 'No Ability'; let abilityData; if (doAbilitiesExist) { abilityData = this.sampleNoReplace(abilityPool); ability = abilityData?.name; } // Random unique moves const m = []; do { const move = this.sampleNoReplace(movePool); m.push(move.id); } while (m.length < setMoveCount); // Random EVs const evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; if (this.gen === 6) { let evpool = 510; do { const x = this.sample(Dex.stats.ids()); const y = this.random(Math.min(256 - evs[x], evpool + 1)); evs[x] += y; evpool -= y; } while (evpool > 0); } else { for (const x of Dex.stats.ids()) { evs[x] = this.random(256); } } // Random IVs const ivs: StatsTable = { hp: this.random(32), atk: this.random(32), def: this.random(32), spa: this.random(32), spd: this.random(32), spe: this.random(32), }; // Random nature let nature = ''; if (doNaturesExist && (naturePool.length > 0)) { nature = this.sample(naturePool).name; } // Level balance const mbstmin = 1307; const stats = species.baseStats; let mbst = (stats['hp'] * 2 + 31 + 21 + 100) + 10; mbst += (stats['atk'] * 2 + 31 + 21 + 100) + 5; mbst += (stats['def'] * 2 + 31 + 21 + 100) + 5; mbst += (stats['spa'] * 2 + 31 + 21 + 100) + 5; mbst += (stats['spd'] * 2 + 31 + 21 + 100) + 5; mbst += (stats['spe'] * 2 + 31 + 21 + 100) + 5; let level; if (this.adjustLevel) { level = this.adjustLevel; } else { level = Math.floor(100 * mbstmin / mbst); while (level < 100) { mbst = Math.floor((stats['hp'] * 2 + 31 + 21 + 100) * level / 100 + 10); mbst += Math.floor(((stats['atk'] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); mbst += Math.floor((stats['def'] * 2 + 31 + 21 + 100) * level / 100 + 5); mbst += Math.floor(((stats['spa'] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); mbst += Math.floor((stats['spd'] * 2 + 31 + 21 + 100) * level / 100 + 5); mbst += Math.floor((stats['spe'] * 2 + 31 + 21 + 100) * level / 100 + 5); if (mbst >= mbstmin) break; level++; } } // Random happiness const happiness = this.random(256); // Random shininess const shiny = this.randomChance(1, 1024); const set: PokemonSet = { name: species.baseSpecies, species: species.name, gender: species.gender, item, ability, moves: m, evs, ivs, nature, level, happiness, shiny, }; if (this.gen === 9) { // Random Tera type set.teraType = this.sample(this.dex.types.names()); } team.push(set); } return team; } queryMoves( moves: Set | null, types: string[], abilities: string[], movePool: string[] = [] ): MoveCounter { // This is primarily a helper function for random setbuilder functions. const counter = new MoveCounter(); if (!moves?.size) return counter; const categories = { Physical: 0, Special: 0, Status: 0 }; // Iterate through all moves we've chosen so far and keep track of what they do: for (const moveid of moves) { let move = this.dex.moves.get(moveid); if (move.id === 'naturepower') { if (this.gen === 5) move = this.dex.moves.get('earthquake'); } let moveType = move.type; if (['judgment', 'multiattack', 'revelationdance'].includes(moveid)) moveType = types[0]; if (move.damage || move.damageCallback) { // Moves that do a set amount of damage: counter.add('damage'); counter.damagingMoves.add(move); } else { // Are Physical/Special/Status moves: categories[move.category]++; } // Moves that have a low base power: if (moveid === 'lowkick' || (move.basePower && move.basePower <= 60 && moveid !== 'rapidspin')) { counter.add('technician'); } // Moves that hit up to 5 times: if (move.multihit && Array.isArray(move.multihit) && move.multihit[1] === 5) counter.add('skilllink'); if (move.recoil || move.hasCrashDamage) counter.add('recoil'); if (move.drain) counter.add('drain'); // Moves which have a base power, but aren't super-weak like Rapid Spin: if (move.basePower > 30 || move.multihit || move.basePowerCallback || moveid === 'infestation') { counter.add(moveType); if (types.includes(moveType)) { // STAB: // Certain moves aren't acceptable as a Pokemon's only STAB attack if (!this.noStab.includes(moveid) && (!moveid.startsWith('hiddenpower') || types.length === 1)) { counter.add('stab'); // Ties between Physical and Special setup should broken in favor of STABs categories[move.category] += 0.1; } } else if ( // Less obvious forms of STAB (moveType === 'Normal' && (['Aerilate', 'Galvanize', 'Pixilate', 'Refrigerate'].some(a => abilities.includes(a)))) || (move.priority === 0 && (['Libero', 'Protean'].some(a => abilities.includes(a))) && !this.noStab.includes(moveid)) || (moveType === 'Steel' && abilities.includes('Steelworker')) ) { counter.add('stab'); } if (move.flags['bite']) counter.add('strongjaw'); if (move.flags['punch']) counter.add('ironfist'); if (move.flags['sound']) counter.add('sound'); if (move.priority !== 0 || (moveid === 'grassyglide' && abilities.includes('Grassy Surge'))) { counter.add('priority'); } counter.damagingMoves.add(move); } // Moves with secondary effects: if (move.secondary) { counter.add('sheerforce'); if (sereneGraceBenefits(move)) { counter.add('serenegrace'); } } // Moves with low accuracy: if (move.accuracy && move.accuracy !== true && move.accuracy < 90) counter.add('inaccurate'); // Moves that change stats: if (RECOVERY_MOVES.includes(moveid)) counter.add('recovery'); if (CONTRARY_MOVES.includes(moveid)) counter.add('contrary'); if (PHYSICAL_SETUP.includes(moveid)) { counter.add('physicalsetup'); counter.setupType = 'Physical'; } else if (SPECIAL_SETUP.includes(moveid)) { counter.add('specialsetup'); counter.setupType = 'Special'; } if (MIXED_SETUP.includes(moveid)) counter.add('mixedsetup'); if (SPEED_SETUP.includes(moveid)) counter.add('speedsetup'); if (HAZARDS.includes(moveid)) counter.add('hazards'); } // Keep track of the available moves for (const moveid of movePool) { const move = this.dex.moves.get(moveid); if (move.damageCallback) continue; if (move.category === 'Physical') counter.add('physicalpool'); if (move.category === 'Special') counter.add('specialpool'); } // Choose a setup type: if (counter.get('mixedsetup')) { counter.setupType = 'Mixed'; } else if (counter.get('physicalsetup') && counter.get('specialsetup')) { const pool = { Physical: categories['Physical'] + counter.get('physicalpool'), Special: categories['Special'] + counter.get('specialpool'), }; if (pool.Physical === pool.Special) { if (categories['Physical'] > categories['Special']) counter.setupType = 'Physical'; if (categories['Special'] > categories['Physical']) counter.setupType = 'Special'; } else { counter.setupType = pool.Physical > pool.Special ? 'Physical' : 'Special'; } } else if (counter.setupType === 'Physical') { if ( (categories['Physical'] < 2 && (!counter.get('stab') || !counter.get('physicalpool'))) && !(moves.has('rest') && moves.has('sleeptalk')) && !moves.has('batonpass') ) { counter.setupType = ''; } } else if (counter.setupType === 'Special') { if ( (categories['Special'] < 2 && (!counter.get('stab') || !counter.get('specialpool'))) && !moves.has('quiverdance') && !(moves.has('rest') && moves.has('sleeptalk')) && !(moves.has('wish') && moves.has('protect')) && !moves.has('batonpass') ) { counter.setupType = ''; } } counter.set('Physical', Math.floor(categories['Physical'])); counter.set('Special', Math.floor(categories['Special'])); counter.set('Status', categories['Status']); return counter; } shouldCullMove( move: Move, types: Set, moves: Set, abilities: string[], counter: MoveCounter, movePool: string[], teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, isNoDynamax: boolean, ): { cull: boolean, isSetup?: boolean } { if (isDoubles && species.baseStats.def >= 140 && movePool.includes('bodypress')) { // In Doubles, Pokémon with Defense stats >= 140 should always have body press return { cull: true }; } if ( (species.id === 'doublade' && movePool.includes('swordsdance')) || (species.id === 'entei' && movePool.includes('extremespeed')) || (species.id === 'genesectdouse' && movePool.includes('technoblast')) || (species.id === 'golisopod' && movePool.includes('leechlife') && movePool.includes('firstimpression')) ) { // Entei should always have Extreme Speed, and Genesect-Douse should always have Techno Blast // Golisopod should always have one of its bug moves (Leech Life or First Impression) return { cull: true }; } const hasRestTalk = moves.has('rest') && moves.has('sleeptalk'); // Reject moves that need support switch (move.id) { case 'acrobatics': case 'junglehealing': // Special case to prevent lead Acrobatics Rillaboom return { cull: (species.id.startsWith('rillaboom') && isLead) || (!isDoubles && !counter.setupType) }; case 'dualwingbeat': case 'fly': return { cull: !types.has(move.type) && !counter.setupType && !!counter.get('Status') }; case 'healbell': return { cull: movePool.includes('protect') || movePool.includes('wish') }; case 'fireblast': // Special case for Togekiss, which always wants Aura Sphere return { cull: abilities.includes('Serene Grace') && (!moves.has('trick') || counter.get('Status') > 1) }; case 'firepunch': // Special case for Darmanitan-Zen-Galar, which doesn't always want Fire Punch return { cull: movePool.includes('bellydrum') || (moves.has('earthquake') && movePool.includes('substitute')) }; case 'flamecharge': return { cull: movePool.includes('swordsdance') }; case 'hypervoice': // Special case for Heliolisk, which always wants Thunderbolt return { cull: types.has('Electric') && movePool.includes('thunderbolt') }; case 'payback': case 'psychocut': // Special case for Type: Null and Malamar, which don't want these + RestTalk return { cull: !counter.get('Status') || hasRestTalk }; case 'rest': const bulkySetup = !moves.has('sleeptalk') && ['bulkup', 'calmmind', 'coil', 'curse'].some(m => movePool.includes(m)); // Registeel would otherwise get Curse sets without Rest, which are very bad generally return { cull: species.id !== 'registeel' && (movePool.includes('sleeptalk') || bulkySetup) }; case 'sleeptalk': if (!moves.has('rest')) return { cull: true }; if (movePool.length > 1 && !abilities.includes('Contrary')) { const rest = movePool.indexOf('rest'); if (rest >= 0) this.fastPop(movePool, rest); } break; case 'storedpower': return { cull: !counter.setupType }; case 'switcheroo': case 'trick': return { cull: counter.get('Physical') + counter.get('Special') < 3 || moves.has('rapidspin') }; case 'trickroom': const webs = !!teamDetails.stickyWeb; return { cull: isLead || webs || !!counter.get('speedsetup') || counter.damagingMoves.size < 2 || movePool.includes('nastyplot'), }; case 'zenheadbutt': // Special case for Victini, which should prefer Bolt Strike to Zen Headbutt return { cull: movePool.includes('boltstrike') || (species.id === 'eiscue' && moves.has('substitute')) }; // Set up once and only if we have the moves for it case 'bellydrum': case 'bulkup': case 'coil': case 'curse': case 'dragondance': case 'honeclaws': case 'swordsdance': if (counter.setupType !== 'Physical') return { cull: true }; // if we're not setting up physically this is pointless if (counter.get('Physical') + counter.get('physicalpool') < 2 && !hasRestTalk) return { cull: true }; // First Impression + setup is undesirable in Doubles if (isDoubles && moves.has('firstimpression')) return { cull: true }; if (move.id === 'swordsdance' && moves.has('dragondance')) return { cull: true }; // Dragon Dance is judged as better return { cull: false, isSetup: true }; case 'calmmind': case 'nastyplot': if (species.id === 'togekiss') return { cull: false }; if (counter.setupType !== 'Special') return { cull: true }; if ( (counter.get('Special') + counter.get('specialpool')) < 2 && !hasRestTalk && !(moves.has('wish') && moves.has('protect')) ) return { cull: true }; if (moves.has('healpulse') || move.id === 'calmmind' && moves.has('trickroom')) return { cull: true }; return { cull: false, isSetup: true }; case 'quiverdance': return { cull: false, isSetup: true }; case 'clangoroussoul': case 'shellsmash': case 'workup': if (counter.setupType !== 'Mixed') return { cull: true }; if (counter.damagingMoves.size + counter.get('physicalpool') + counter.get('specialpool') < 2) return { cull: true }; return { cull: false, isSetup: true }; case 'agility': case 'autotomize': case 'rockpolish': case 'shiftgear': if (counter.damagingMoves.size < 2 || moves.has('rest')) return { cull: true }; if (movePool.includes('calmmind') || movePool.includes('nastyplot')) return { cull: true }; return { cull: false, isSetup: !counter.setupType }; // Bad after setup case 'coaching': case 'counter': case 'reversal': // Counter: special case for Alakazam, which doesn't want Counter + Nasty Plot return { cull: !!counter.setupType }; case 'bulletpunch': case 'extremespeed': case 'rockblast': return { cull: ( !!counter.get('speedsetup') || (!isDoubles && moves.has('dragondance')) || counter.damagingMoves.size < 2 ) }; case 'closecombat': case 'flashcannon': case 'pollenpuff': const substituteCullCondition = ( (moves.has('substitute') && !types.has('Fighting')) || (moves.has('toxic') && movePool.includes('substitute')) ); const preferHJKOverCCCullCondition = ( move.id === 'closecombat' && !counter.setupType && (moves.has('highjumpkick') || movePool.includes('highjumpkick')) ); return { cull: substituteCullCondition || preferHJKOverCCCullCondition }; case 'defog': return { cull: !!counter.setupType || moves.has('healbell') || moves.has('toxicspikes') || !!teamDetails.defog }; case 'fakeout': return { cull: !!counter.setupType || ['protect', 'rapidspin', 'substitute', 'uturn'].some(m => moves.has(m)) }; case 'firstimpression': case 'glare': case 'icywind': case 'tailwind': case 'waterspout': return { cull: !!counter.setupType || !!counter.get('speedsetup') || moves.has('rest') }; case 'healingwish': case 'memento': return { cull: !!counter.setupType || !!counter.get('recovery') || moves.has('substitute') || moves.has('uturn') }; case 'highjumpkick': // Special case for Hitmonlee to prevent non-Unburden Curse return { cull: moves.has('curse') }; case 'partingshot': return { cull: !!counter.get('speedsetup') || moves.has('bulkup') || moves.has('uturn') }; case 'protect': if (!isDoubles && ((counter.setupType && !moves.has('wish')) || moves.has('rest'))) return { cull: true }; if ( !isDoubles && counter.get('Status') < 2 && ['Hunger Switch', 'Speed Boost'].every(m => !abilities.includes(m)) ) return { cull: true }; if (movePool.includes('leechseed') || (movePool.includes('toxic') && !moves.has('wish'))) return { cull: true }; if (isDoubles && ( ['bellydrum', 'fakeout', 'shellsmash', 'spore'].some(m => movePool.includes(m)) || moves.has('tailwind') || moves.has('waterspout') || counter.get('recovery') )) return { cull: true }; return { cull: false }; case 'rapidspin': const setup = ['curse', 'nastyplot', 'shellsmash'].some(m => moves.has(m)); return { cull: !!teamDetails.rapidSpin || setup || (!!counter.setupType && counter.get('Fighting') >= 2) }; case 'shadowsneak': const sneakIncompatible = ['substitute', 'trickroom', 'dualwingbeat', 'toxic'].some(m => moves.has(m)); return { cull: hasRestTalk || sneakIncompatible || counter.setupType === 'Special' }; case 'spikes': return { cull: !!counter.setupType || (!!teamDetails.spikes && teamDetails.spikes > 1) }; case 'stealthrock': return { cull: !!counter.setupType || !!counter.get('speedsetup') || !!teamDetails.stealthRock || ['rest', 'substitute', 'trickroom', 'teleport'].some(m => moves.has(m)) || (species.id === 'palossand' && movePool.includes('shoreup')), }; case 'stickyweb': return { cull: counter.setupType === 'Special' || !!teamDetails.stickyWeb }; case 'taunt': return { cull: moves.has('encore') || moves.has('nastyplot') || moves.has('swordsdance') }; case 'thunderwave': case 'voltswitch': const cullInDoubles = isDoubles && (moves.has('electroweb') || moves.has('nuzzle')); return { cull: ( !!counter.setupType || !!counter.get('speedsetup') || moves.has('shiftgear') || moves.has('raindance') || cullInDoubles ) }; case 'toxic': return { cull: !!counter.setupType || ['sludgewave', 'thunderwave', 'willowisp'].some(m => moves.has(m)) }; case 'toxicspikes': return { cull: !!counter.setupType || !!teamDetails.toxicSpikes }; case 'uturn': const bugSwordsDanceCase = types.has('Bug') && counter.get('recovery') && moves.has('swordsdance'); return { cull: ( !!counter.get('speedsetup') || (counter.setupType && !bugSwordsDanceCase) || (isDoubles && moves.has('leechlife')) || moves.has('shiftgear') ) }; /** * Ineffective to have both moves together * * These are sorted in order of: * Normal>Fire>Water>Electric>Grass>Ice>Fighting>Poison>Ground>Flying>Psychic>Bug>Rock>Ghost>Dragon>Dark>Fairy * and then subsorted alphabetically. * This type order is arbitrary and referenced from https://pokemondb.net/type. */ case 'explosion': // Rock Blast: Special case for Gigalith to prevent Stone Edge-less Choice Band sets const otherMoves = ['curse', 'stompingtantrum', 'rockblast', 'painsplit', 'wish'].some(m => moves.has(m)); return { cull: !!counter.get('speedsetup') || !!counter.get('recovery') || otherMoves }; case 'facade': // Special case for Snorlax return { cull: movePool.includes('doubleedge') }; case 'quickattack': // Diggersby wants U-turn on Choiced sets const diggersbyCull = counter.get('Physical') > 3 && movePool.includes('uturn'); return { cull: !!counter.get('speedsetup') || (types.has('Rock') && !!counter.get('Status')) || diggersbyCull }; case 'blazekick': return { cull: species.id === 'genesect' && counter.get('Special') >= 1 }; case 'blueflare': return { cull: moves.has('vcreate') }; case 'firefang': case 'flamethrower': // Fire Fang: Special case for Garchomp, which doesn't want Fire Fang w/o Swords Dance const otherFireMoves = ['heatwave', 'overheat'].some(m => moves.has(m)); return { cull: (moves.has('fireblast') && counter.setupType !== 'Physical') || otherFireMoves }; case 'flareblitz': // Special case for Solgaleo to prevent Flame Charge + Flare Blitz return { cull: species.id === 'solgaleo' && moves.has('flamecharge') }; case 'overheat': return { cull: moves.has('flareblitz') || (isDoubles && moves.has('calmmind')) }; case 'aquatail': case 'flipturn': return { cull: moves.has('aquajet') || !!counter.get('Status') }; case 'hydropump': return { cull: moves.has('scald') && ( (counter.get('Special') < 4 && !moves.has('uturn')) || (species.types.length > 1 && counter.get('stab') < 3) ) }; case 'muddywater': return { cull: moves.has('liquidation') }; case 'scald': // Special case for Clawitzer return { cull: moves.has('waterpulse') }; case 'thunderbolt': // Special case for Goodra, which only wants one move to hit Water-types return { cull: moves.has('powerwhip') }; case 'energyball': // Special case to prevent Shiinotic with four Grass moves and no Moonblast return { cull: species.id === 'shiinotic' && !moves.has('moonblast') }; case 'gigadrain': // Celebi always wants Leaf Storm on its more pivoting-focused non-Nasty Plot sets const celebiPreferLeafStorm = species.id === 'celebi' && !counter.setupType && moves.has('uturn'); return { cull: celebiPreferLeafStorm || (types.has('Poison') && !counter.get('Poison')) }; case 'leafblade': // Special case for Virizion to prevent Leaf Blade on Assault Vest sets return { cull: (moves.has('leafstorm') || movePool.includes('leafstorm')) && counter.setupType !== 'Physical' }; case 'leafstorm': const leafBladePossible = movePool.includes('leafblade') || moves.has('leafblade'); return { cull: // Virizion should always prefer Leaf Blade to Leaf Storm on Physical sets (counter.setupType === 'Physical' && (species.id === 'virizion' || leafBladePossible)) || (moves.has('gigadrain') && !!counter.get('Status')) || (isDoubles && moves.has('energyball')), }; case 'powerwhip': // Special case for Centiskorch, which doesn't want Assault Vest return { cull: moves.has('leechlife') }; case 'woodhammer': return { cull: moves.has('hornleech') && counter.get('Physical') < 4 }; case 'freezedry': const betterIceMove = ( (moves.has('blizzard') && !!counter.setupType) || (moves.has('icebeam') && counter.get('Special') < 4) ); const preferThunderWave = movePool.includes('thunderwave') && types.has('Electric'); return { cull: betterIceMove || preferThunderWave || movePool.includes('bodyslam') }; case 'bodypress': // Turtonator never wants Earthquake + Body Press, and wants EQ+Smash or Press+No Smash const turtonatorPressCull = species.id === 'turtonator' && moves.has('earthquake') && movePool.includes('shellsmash'); const pressIncompatible = ['shellsmash', 'mirrorcoat', 'whirlwind'].some(m => moves.has(m)); return { cull: turtonatorPressCull || pressIncompatible || counter.setupType === 'Special' }; case 'circlethrow': // Part of a special case for Throh to pick one specific Fighting move depending on its set return { cull: moves.has('stormthrow') && !moves.has('rest') }; case 'drainpunch': return { cull: moves.has('closecombat') || (!types.has('Fighting') && movePool.includes('swordsdance')) }; case 'dynamicpunch': case 'thunderouskick': // Dynamic Punch: Special case for Machamp to better split Guts and No Guard sets return { cull: moves.has('closecombat') || moves.has('facade') }; case 'focusblast': // Special cases for Blastoise and Regice; Blastoise wants Shell Smash, and Regice wants Thunderbolt return { cull: movePool.includes('shellsmash') || hasRestTalk }; case 'hammerarm': // Special case for Kangaskhan, which always wants Sucker Punch return { cull: moves.has('fakeout') }; case 'stormthrow': // Part of a special case for Throh to pick one specific Fighting move depending on its set return { cull: hasRestTalk }; case 'superpower': return { cull: moves.has('hydropump') || (counter.get('Physical') >= 4 && movePool.includes('uturn')) || (moves.has('substitute') && !abilities.includes('Contrary')), isSetup: abilities.includes('Contrary'), }; case 'poisonjab': return { cull: !types.has('Poison') && counter.get('Status') >= 2 }; case 'earthquake': const doublesCull = moves.has('earthpower') || moves.has('highhorsepower'); // Turtonator wants Body Press when it doesn't have Shell Smash const turtQuakeCull = species.id === 'turtonator' && movePool.includes('bodypress') && movePool.includes('shellsmash'); const subToxicPossible = moves.has('substitute') && movePool.includes('toxic'); return { cull: turtQuakeCull || (isDoubles && doublesCull) || subToxicPossible || moves.has('bonemerang') }; case 'scorchingsands': // Special cases for Ninetales and Palossand; prevents status redundancy return { cull: ( moves.has('willowisp') || moves.has('earthpower') || (moves.has('toxic') && movePool.includes('earthpower')) ) }; case 'airslash': return { cull: (species.id === 'naganadel' && moves.has('nastyplot')) || hasRestTalk || (abilities.includes('Simple') && !!counter.get('recovery')) || counter.setupType === 'Physical', }; case 'bravebird': // Special case for Mew, which only wants Brave Bird with Swords Dance return { cull: moves.has('dragondance') }; case 'hurricane': return { cull: counter.setupType === 'Physical' }; case 'futuresight': return { cull: moves.has('psyshock') || moves.has('trick') || movePool.includes('teleport') }; case 'photongeyser': // Special case for Necrozma-DM, which always wants Dragon Dance return { cull: moves.has('morningsun') }; case 'psychic': const alcremieCase = species.id === 'alcremiegmax' && counter.get('Status') < 2; return { cull: alcremieCase || (moves.has('psyshock') && (!!counter.setupType || isDoubles)) }; case 'psychicfangs': // Special case for Morpeko, which doesn't want 4 attacks Leftovers return { cull: moves.has('rapidspin') }; case 'psyshock': // Special case for Sylveon which only wants Psyshock if it gets a Choice item const sylveonCase = abilities.includes('Pixilate') && counter.get('Special') < 4; return { cull: moves.has('psychic') || (!counter.setupType && sylveonCase) || (isDoubles && moves.has('psychic')) }; case 'bugbuzz': return { cull: moves.has('uturn') && !counter.setupType }; case 'leechlife': return { cull: (isDoubles && moves.has('lunge')) || (moves.has('uturn') && !counter.setupType) || movePool.includes('spikes'), }; case 'stoneedge': const gutsCullCondition = abilities.includes('Guts') && (!moves.has('dynamicpunch') || moves.has('spikes')); const rockSlidePlusStatusPossible = counter.get('Status') && movePool.includes('rockslide'); const otherRockMove = moves.has('rockblast') || moves.has('rockslide'); const lucarioCull = species.id === 'lucario' && !!counter.setupType; return { cull: gutsCullCondition || (!isDoubles && rockSlidePlusStatusPossible) || otherRockMove || lucarioCull }; case 'poltergeist': // Special case for Dhelmise in Doubles, which doesn't want both return { cull: moves.has('knockoff') }; case 'shadowball': return { cull: (isDoubles && moves.has('phantomforce')) || // Special case for Sylveon, which never wants Shadow Ball as its only coverage move (abilities.includes('Pixilate') && (!!counter.setupType || counter.get('Status') > 1)) || (!types.has('Ghost') && movePool.includes('focusblast')), }; case 'shadowclaw': return { cull: types.has('Steel') && moves.has('shadowsneak') && counter.get('Physical') < 4 }; case 'dragonpulse': case 'spacialrend': return { cull: moves.has('dracometeor') && counter.get('Special') < 4 }; case 'darkpulse': const pulseIncompatible = ['foulplay', 'knockoff'].some(m => moves.has(m)) || ( species.id === 'shiftry' && (moves.has('defog') || moves.has('suckerpunch')) ); // Special clause to prevent bugged Shiftry sets with Sucker Punch + Nasty Plot const shiftryCase = movePool.includes('nastyplot') && !moves.has('defog'); return { cull: pulseIncompatible && !shiftryCase && counter.setupType !== 'Special' }; case 'suckerpunch': return { cull: // Shiftry in No Dynamax would otherwise get Choice Scarf Sucker Punch sometimes. (isNoDynamax && species.id === 'shiftry' && moves.has('defog')) || moves.has('rest') || counter.damagingMoves.size < 2 || (counter.setupType === 'Special') || (counter.get('Dark') > 1 && !types.has('Dark')), }; case 'dazzlinggleam': return { cull: ['fleurcannon', 'moonblast', 'petaldance'].some(m => moves.has(m)) }; // Status: case 'bodyslam': case 'clearsmog': const toxicCullCondition = moves.has('toxic') && !types.has('Normal'); return { cull: moves.has('sludgebomb') || moves.has('trick') || movePool.includes('recover') || toxicCullCondition }; case 'haze': // Special case for Corsola-Galar, which always wants Will-O-Wisp return { cull: !teamDetails.stealthRock && (moves.has('stealthrock') || movePool.includes('stealthrock')) }; case 'hypnosis': // Special case for Xurkitree to properly split Blunder Policy and Choice item sets return { cull: moves.has('voltswitch') }; case 'willowisp': case 'yawn': // Swords Dance is a special case for Rapidash return { cull: moves.has('thunderwave') || moves.has('toxic') || moves.has('swordsdance') }; case 'painsplit': case 'recover': case 'synthesis': return { cull: moves.has('rest') || moves.has('wish') || (move.id === 'synthesis' && moves.has('gigadrain')) }; case 'roost': return { cull: moves.has('throatchop') || // Hawlucha doesn't want Roost + 3 attacks (moves.has('stoneedge') && species.id === 'hawlucha') || // Special cases for Salamence, Dynaless Dragonite, and Scizor to help prevent sets with poor coverage or no setup. (moves.has('dualwingbeat') && (moves.has('outrage') || species.id === 'scizor')), }; case 'reflect': case 'lightscreen': return { cull: !!teamDetails.screens }; case 'slackoff': // Special case to prevent Scaldless Slowking return { cull: species.id === 'slowking' && !moves.has('scald') }; case 'substitute': const moveBasedCull = ['bulkup', 'nastyplot', 'painsplit', 'roost', 'swordsdance'].some(m => movePool.includes(m)); // Smaller formes of Gourgeist in Doubles don't want Poltergeist as their only attack const doublesGourgeist = isDoubles && movePool.includes('powerwhip'); // Calyrex wants Substitute + Leech Seed not Calm Mind + Leech Seed const calmMindCullCondition = !counter.get('recovery') && movePool.includes('calmmind') && species.id !== 'calyrex'; // Eiscue wants to always have Liquidation and Belly Drum const eiscue = species.id === 'eiscue' && moves.has('zenheadbutt'); return { cull: moves.has('rest') || moveBasedCull || doublesGourgeist || calmMindCullCondition || eiscue }; case 'helpinghand': // Special case for Shuckle in Doubles, which doesn't want sets with no method to harm foes return { cull: moves.has('acupressure') }; case 'wideguard': return { cull: moves.has('protect') }; case 'grassknot': // Special case for Raichu and Heliolisk return { cull: moves.has('surf') }; case 'icepunch': // Special case for Marshadow return { cull: moves.has('rocktomb') }; case 'leechseed': // Special case for Calyrex to prevent Leech Seed + Calm Mind return { cull: !!counter.setupType }; } return { cull: false }; } shouldCullAbility( ability: string, types: Set, moves: Set, abilities: string[], counter: MoveCounter, movePool: string[], teamDetails: RandomTeamsTypes.TeamDetails, species: Species, preferredType: string, role: RandomTeamsTypes.Role, isDoubles: boolean, isNoDynamax: boolean ): boolean { if ([ 'Flare Boost', 'Hydration', 'Ice Body', 'Immunity', 'Innards Out', 'Insomnia', 'Misty Surge', 'Moody', 'Perish Body', 'Quick Feet', 'Rain Dish', 'Snow Cloak', 'Steadfast', 'Steam Engine', ].includes(ability)) return true; switch (ability) { // Abilities which are primarily useful for certain moves case 'Contrary': case 'Serene Grace': case 'Skill Link': case 'Strong Jaw': return !counter.get(toID(ability)); case 'Analytic': return (moves.has('rapidspin') || species.nfe || isDoubles); case 'Blaze': return (isDoubles && abilities.includes('Solar Power')) || (!isDoubles && !isNoDynamax && species.id === 'charizard'); // case 'Bulletproof': case 'Overcoat': // return !!counter.setupType; case 'Chlorophyll': return (species.baseStats.spe > 100 || !counter.get('Fire') && !moves.has('sunnyday') && !teamDetails.sun); case 'Cloud Nine': return (!isNoDynamax || species.id !== 'golduck'); case 'Competitive': return (counter.get('Special') < 2 || (moves.has('rest') && moves.has('sleeptalk'))); case 'Compound Eyes': case 'No Guard': return !counter.get('inaccurate'); case 'Cursed Body': return abilities.includes('Infiltrator'); case 'Defiant': return !counter.get('Physical'); case 'Download': return (counter.damagingMoves.size < 3 || moves.has('trick')); case 'Early Bird': return (types.has('Grass') && isDoubles); case 'Flash Fire': return (this.dex.getEffectiveness('Fire', species) < -1 || abilities.includes('Drought')); case 'Gluttony': return !moves.has('bellydrum'); case 'Guts': return (!moves.has('facade') && !moves.has('sleeptalk') && !species.nfe); case 'Harvest': return (abilities.includes('Frisk') && !isDoubles); case 'Hustle': case 'Inner Focus': return ((species.id !== 'glalie' && counter.get('Physical') < 2) || abilities.includes('Iron Fist')); case 'Infiltrator': return (moves.has('rest') && moves.has('sleeptalk')) || (isDoubles && abilities.includes('Clear Body')); case 'Intimidate': if (species.id === 'salamence' && moves.has('dragondance')) return true; return ['bodyslam', 'bounce', 'tripleaxel'].some(m => moves.has(m)); case 'Iron Fist': return (counter.get('ironfist') < 2 || moves.has('dynamicpunch')); case 'Justified': return (isDoubles && abilities.includes('Inner Focus')); case 'Lightning Rod': return (species.types.includes('Ground') || (!isNoDynamax && counter.setupType === 'Physical')); case 'Limber': return species.types.includes('Electric') || moves.has('facade'); case 'Liquid Voice': return !moves.has('hypervoice'); case 'Magic Guard': // For Sigilyph return (abilities.includes('Tinted Lens') && !counter.get('Status') && !isDoubles); case 'Mold Breaker': return ( abilities.includes('Adaptability') || abilities.includes('Scrappy') || (abilities.includes('Unburden') && !!counter.setupType) || (abilities.includes('Sheer Force') && !!counter.get('sheerforce')) ); case 'Moxie': return (counter.get('Physical') < 2 || moves.has('stealthrock') || moves.has('defog')); case 'Overgrow': return !counter.get('Grass'); case 'Own Tempo': return !moves.has('petaldance'); case 'Power Construct': return (species.forme === '10%' && !isDoubles); case 'Prankster': return !counter.get('Status'); case 'Pressure': return (!!counter.setupType || counter.get('Status') < 2 || isDoubles); case 'Refrigerate': return !counter.get('Normal'); case 'Regenerator': // For Reuniclus return abilities.includes('Magic Guard'); case 'Reckless': return !counter.get('recoil') || moves.has('curse'); case 'Rock Head': return !counter.get('recoil'); case 'Sand Force': case 'Sand Veil': return !teamDetails.sand; case 'Sand Rush': return (!teamDetails.sand && (isNoDynamax || !counter.setupType || !counter.get('Rock') || moves.has('rapidspin'))); case 'Sap Sipper': // For Drampa, which wants Berserk with Roost return moves.has('roost'); case 'Scrappy': return (moves.has('earthquake') && species.id === 'miltank'); case 'Screen Cleaner': return !!teamDetails.screens; case 'Shed Skin': // For Scrafty return moves.has('dragondance'); case 'Sheer Force': return (!counter.get('sheerforce') || abilities.includes('Guts') || (species.id === 'druddigon' && !isDoubles)); case 'Shell Armor': return (species.id === 'omastar' && (moves.has('spikes') || moves.has('stealthrock'))); case 'Slush Rush': return (!teamDetails.hail && !abilities.includes('Swift Swim')); case 'Sniper': // Inteleon wants Torrent unless it is Gmax return (species.name === 'Inteleon' || (counter.get('Water') > 1 && !moves.has('focusenergy'))); case 'Solar Power': return (isNoDynamax && !teamDetails.sun); case 'Speed Boost': return (isNoDynamax && species.id === 'ninjask'); case 'Steely Spirit': return (moves.has('fakeout') && !isDoubles); case 'Sturdy': return (moves.has('bulkup') || !!counter.get('recoil') || (!isNoDynamax && abilities.includes('Solid Rock'))); case 'Swarm': return (!counter.get('Bug') || !!counter.get('recovery')); case 'Sweet Veil': return types.has('Grass'); case 'Swift Swim': if (isNoDynamax) { const neverWantsSwim = !moves.has('raindance') && [ 'Intimidate', 'Rock Head', 'Water Absorb', ].some(m => abilities.includes(m)); const noSwimIfNoRain = !moves.has('raindance') && [ 'Cloud Nine', 'Lightning Rod', 'Intimidate', 'Rock Head', 'Sturdy', 'Water Absorb', 'Weak Armor', ].some(m => abilities.includes(m)); return teamDetails.rain ? neverWantsSwim : noSwimIfNoRain; } return (!moves.has('raindance') && ( ['Intimidate', 'Rock Head', 'Slush Rush', 'Water Absorb'].some(abil => abilities.includes(abil)) || (abilities.includes('Lightning Rod') && !counter.setupType) )); case 'Synchronize': return counter.get('Status') < 3; case 'Technician': return ( !counter.get('technician') || moves.has('tailslap') || abilities.includes('Punk Rock') || // For Doubles Alolan Persian movePool.includes('snarl') ); case 'Tinted Lens': return ( // For Sigilyph moves.has('defog') || // For Butterfree (moves.has('hurricane') && abilities.includes('Compound Eyes')) || (counter.get('Status') > 2 && !counter.setupType) ); case 'Torrent': // For Inteleon-Gmax and Primarina return (moves.has('focusenergy') || moves.has('hypervoice')); case 'Tough Claws': // For Perrserker return (types.has('Steel') && !moves.has('fakeout')); case 'Unaware': // For Swoobat and Clefable return (!!counter.setupType || moves.has('fireblast')); case 'Unburden': return (abilities.includes('Prankster') || !counter.setupType && !isDoubles); case 'Volt Absorb': return (this.dex.getEffectiveness('Electric', species) < -1); case 'Water Absorb': return ( moves.has('raindance') || ['Drizzle', 'Strong Jaw', 'Unaware', 'Volt Absorb'].some(abil => abilities.includes(abil)) ); case 'Weak Armor': // The Speed less than 50 case is intended for Cursola, but could apply to any slow Pokémon. return ( (!isNoDynamax && species.baseStats.spe > 50) || species.id === 'skarmory' || moves.has('shellsmash') || moves.has('rapidspin') ); } return false; } getAbility( types: Set, moves: Set, abilities: string[], counter: MoveCounter, movePool: string[], teamDetails: RandomTeamsTypes.TeamDetails, species: Species, preferredType: string, role: RandomTeamsTypes.Role, isDoubles: boolean, isNoDynamax: boolean ): string { const abilityData = Array.from(abilities).map(a => this.dex.abilities.get(a)); Utils.sortBy(abilityData, abil => -abil.rating); if (abilityData.length <= 1) return abilityData[0].name; // Hard-code abilities here // Lopunny, and other Facade users, don't want Limber, even if other abilities are poorly rated, // since paralysis would arguably be good for them. if (species.id === 'lopunny' && moves.has('facade')) return 'Cute Charm'; if (species.id === 'copperajahgmax') return 'Heavy Metal'; if (abilities.includes('Guts') && // for Ursaring in BDSP !abilities.includes('Quick Feet') && ( species.id === 'gurdurr' || species.id === 'throh' || moves.has('facade') || (moves.has('rest') && moves.has('sleeptalk')) )) return 'Guts'; if (abilities.includes('Moxie') && (counter.get('Physical') > 3 || moves.has('bounce')) && !isDoubles) return 'Moxie'; if (isDoubles) { if (abilities.includes('Competitive') && species.id !== 'boltund' && species.id !== 'gothitelle') return 'Competitive'; if (abilities.includes('Friend Guard')) return 'Friend Guard'; if (abilities.includes('Gluttony') && moves.has('recycle')) return 'Gluttony'; if (abilities.includes('Guts')) return 'Guts'; if (abilities.includes('Harvest')) return 'Harvest'; if (abilities.includes('Healer') && ( abilities.includes('Natural Cure') || (abilities.includes('Aroma Veil') && this.randomChance(1, 2)) )) return 'Healer'; if (abilities.includes('Intimidate')) return 'Intimidate'; if (species.id === 'lopunny') return 'Klutz'; if (abilities.includes('Magic Guard') && !abilities.includes('Unaware')) return 'Magic Guard'; if (abilities.includes('Ripen')) return 'Ripen'; if (abilities.includes('Stalwart')) return 'Stalwart'; if (abilities.includes('Storm Drain')) return 'Storm Drain'; if (abilities.includes('Telepathy') && ( abilities.includes('Pressure') || abilities.includes('Analytic') )) return 'Telepathy'; } let abilityAllowed: Ability[] = []; // Obtain a list of abilities that are allowed (not culled) for (const ability of abilityData) { if (ability.rating >= 1 && !this.shouldCullAbility( ability.name, types, moves, abilities, counter, movePool, teamDetails, species, '', '', isDoubles, isNoDynamax )) { abilityAllowed.push(ability); } } // If all abilities are rejected, re-allow all abilities if (!abilityAllowed.length) { for (const ability of abilityData) { if (ability.rating > 0) abilityAllowed.push(ability); } if (!abilityAllowed.length) abilityAllowed = abilityData; } if (abilityAllowed.length === 1) return abilityAllowed[0].name; // Sort abilities by rating with an element of randomness // All three abilities can be chosen if (abilityAllowed[2] && abilityAllowed[0].rating - 0.5 <= abilityAllowed[2].rating) { if (abilityAllowed[1].rating <= abilityAllowed[2].rating) { if (this.randomChance(1, 2)) [abilityAllowed[1], abilityAllowed[2]] = [abilityAllowed[2], abilityAllowed[1]]; } else { if (this.randomChance(1, 3)) [abilityAllowed[1], abilityAllowed[2]] = [abilityAllowed[2], abilityAllowed[1]]; } if (abilityAllowed[0].rating <= abilityAllowed[1].rating) { if (this.randomChance(2, 3)) [abilityAllowed[0], abilityAllowed[1]] = [abilityAllowed[1], abilityAllowed[0]]; } else { if (this.randomChance(1, 2)) [abilityAllowed[0], abilityAllowed[1]] = [abilityAllowed[1], abilityAllowed[0]]; } } else { // Third ability cannot be chosen if (abilityAllowed[0].rating <= abilityAllowed[1].rating) { if (this.randomChance(1, 2)) [abilityAllowed[0], abilityAllowed[1]] = [abilityAllowed[1], abilityAllowed[0]]; } else if (abilityAllowed[0].rating - 0.5 <= abilityAllowed[1].rating) { if (this.randomChance(1, 3)) [abilityAllowed[0], abilityAllowed[1]] = [abilityAllowed[1], abilityAllowed[0]]; } } // After sorting, choose the first ability return abilityAllowed[0].name; } getHighPriorityItem( ability: string, types: Set, moves: Set, counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean ) { // not undefined — we want "no item" not "go find a different item" if (moves.has('acrobatics') && ability !== 'Ripen') return ability === 'Grassy Surge' ? 'Grassy Seed' : ''; if (moves.has('geomancy') || moves.has('meteorbeam')) return 'Power Herb'; if (moves.has('shellsmash')) { if (ability === 'Sturdy' && !isLead && !isDoubles) return 'Heavy-Duty Boots'; // Shell Smash + Solid Rock is intended for Carracosta, but I think // any Pokémon which can take a SE hit via Solid Rock deserves to have // its Shell Smash considered a good enough speed setup move for WP. if (ability === 'Solid Rock') return 'Weakness Policy'; return 'White Herb'; } // Techno Blast should always be Water-type if (moves.has('technoblast')) return 'Douse Drive'; // Species-specific logic if ( ['Corsola', 'Garchomp', 'Tangrowth'].includes(species.name) && counter.get('Status') && !counter.setupType && !isDoubles ) return 'Rocky Helmet'; if (species.name === 'Eternatus' && counter.get('Status') < 2) return 'Metronome'; if (species.name === 'Farfetch\u2019d') return 'Leek'; if (species.name === 'Froslass' && !isDoubles) return 'Wide Lens'; if (species.name === 'Latios' && counter.get('Special') === 2 && !isDoubles) return 'Soul Dew'; if (species.name === 'Lopunny') return isDoubles ? 'Iron Ball' : 'Toxic Orb'; if (species.baseSpecies === 'Marowak') return 'Thick Club'; if (species.baseSpecies === 'Pikachu') return 'Light Ball'; if (species.name === 'Regieleki' && !isDoubles) return 'Magnet'; if (species.name === 'Shedinja') { const noSash = !teamDetails.defog && !teamDetails.rapidSpin && !isDoubles; return noSash ? 'Heavy-Duty Boots' : 'Focus Sash'; } if (species.name === 'Shuckle' && moves.has('stickyweb')) return 'Mental Herb'; if (species.name === 'Unfezant' || moves.has('focusenergy')) return 'Scope Lens'; if (species.name === 'Pincurchin') return 'Shuca Berry'; if (species.name === 'Wobbuffet' && moves.has('destinybond')) return 'Custap Berry'; if (species.name === 'Scyther' && counter.damagingMoves.size > 3) return 'Choice Band'; if (species.name === 'Cinccino' && !moves.has('uturn')) return 'Life Orb'; if (moves.has('bellydrum') && moves.has('substitute')) return 'Salac Berry'; // Misc item generation logic const HDBBetterThanEviolite = ( !isDoubles && (!isLead || moves.has('uturn')) && this.dex.getEffectiveness('Rock', species) >= 2 ); if (species.nfe) return HDBBetterThanEviolite ? 'Heavy-Duty Boots' : 'Eviolite'; // Ability based logic and miscellaneous logic if (species.name === 'Wobbuffet' || ['Cheek Pouch', 'Harvest', 'Ripen'].includes(ability)) return 'Sitrus Berry'; if (ability === 'Gluttony') return this.sample(['Aguav', 'Figy', 'Iapapa', 'Mago', 'Wiki']) + ' Berry'; if ( ability === 'Imposter' || (ability === 'Magnet Pull' && moves.has('bodypress') && !isDoubles) ) return 'Choice Scarf'; if ( ability === 'Guts' && (counter.get('Physical') > 2 || isDoubles) ) { return types.has('Fire') ? 'Toxic Orb' : 'Flame Orb'; } if (ability === 'Magic Guard' && counter.damagingMoves.size > 1) { return moves.has('counter') ? 'Focus Sash' : 'Life Orb'; } if (ability === 'Sheer Force' && counter.get('sheerforce')) return 'Life Orb'; if (ability === 'Unburden') return (moves.has('closecombat') || moves.has('curse')) ? 'White Herb' : 'Sitrus Berry'; if (moves.has('trick') || (moves.has('switcheroo') && !isDoubles) || ability === 'Gorilla Tactics') { if (species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && !counter.get('priority') && ability !== 'Triage') { return 'Choice Scarf'; } else { return (counter.get('Physical') > counter.get('Special')) ? 'Choice Band' : 'Choice Specs'; } } if (moves.has('auroraveil') || moves.has('lightscreen') && moves.has('reflect')) return 'Light Clay'; if (moves.has('rest') && !moves.has('sleeptalk') && ability !== 'Shed Skin') return 'Chesto Berry'; if (moves.has('hypnosis') && ability === 'Beast Boost') return 'Blunder Policy'; if (moves.has('bellydrum')) return 'Sitrus Berry'; if (this.dex.getEffectiveness('Rock', species) >= 2 && !isDoubles) { return 'Heavy-Duty Boots'; } } /** Item generation specific to Random Doubles */ getDoublesItem( ability: string, types: Set, moves: Set, abilities: string[], counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, ): string | undefined { const defensiveStatTotal = species.baseStats.hp + species.baseStats.def + species.baseStats.spd; if ( (['dragonenergy', 'eruption', 'waterspout'].some(m => moves.has(m))) && counter.damagingMoves.size >= 4 ) return 'Choice Scarf'; if (moves.has('blizzard') && ability !== 'Snow Warning' && !teamDetails.hail) return 'Blunder Policy'; if (this.dex.getEffectiveness('Rock', species) >= 2 && !types.has('Flying')) return 'Heavy-Duty Boots'; if (counter.get('Physical') >= 4 && ['fakeout', 'feint', 'rapidspin', 'suckerpunch'].every(m => !moves.has(m)) && ( types.has('Dragon') || types.has('Fighting') || types.has('Rock') || moves.has('flipturn') || moves.has('uturn') )) { return ( !counter.get('priority') && !abilities.includes('Speed Boost') && species.baseStats.spe >= 60 && species.baseStats.spe <= 100 && this.randomChance(1, 2) ) ? 'Choice Scarf' : 'Choice Band'; } if ( ( counter.get('Special') >= 4 && (types.has('Dragon') || types.has('Fighting') || types.has('Rock') || moves.has('voltswitch')) ) || ( (counter.get('Special') >= 3 && (moves.has('flipturn') || moves.has('uturn'))) && !moves.has('acidspray') && !moves.has('electroweb') ) ) { return ( species.baseStats.spe >= 60 && species.baseStats.spe <= 100 && this.randomChance(1, 2) ) ? 'Choice Scarf' : 'Choice Specs'; } // This one is intentionally below the Choice item checks. if ((defensiveStatTotal < 250 && ability === 'Regenerator') || species.name === 'Pheromosa') return 'Life Orb'; if (counter.damagingMoves.size >= 4 && defensiveStatTotal >= 275) return 'Assault Vest'; if ( counter.damagingMoves.size >= 3 && species.baseStats.spe >= 60 && ability !== 'Multiscale' && ability !== 'Sturdy' && [ 'acidspray', 'clearsmog', 'electroweb', 'fakeout', 'feint', 'icywind', 'incinerate', 'naturesmadness', 'rapidspin', 'snarl', 'uturn', ].every(m => !moves.has(m)) ) return (ability === 'Defeatist' || defensiveStatTotal >= 275) ? 'Sitrus Berry' : 'Life Orb'; } getMediumPriorityItem( ability: string, moves: Set, counter: MoveCounter, species: Species, isLead: boolean, isDoubles: boolean, isNoDynamax: boolean ): string | undefined { const defensiveStatTotal = species.baseStats.hp + species.baseStats.def + species.baseStats.spd; // Choice items if ( !isDoubles && counter.get('Physical') >= 4 && ability !== 'Serene Grace' && ['fakeout', 'flamecharge', 'rapidspin'].every(m => !moves.has(m)) ) { const scarfReqs = ( (species.baseStats.atk >= 100 || ability === 'Huge Power') && species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && ability !== 'Speed Boost' && !counter.get('priority') && (isNoDynamax || ['bounce', 'dualwingbeat'].every(m => !moves.has(m))) ); return (scarfReqs && this.randomChance(2, 3)) ? 'Choice Scarf' : 'Choice Band'; } if (!isDoubles && ( (counter.get('Special') >= 4 && !moves.has('futuresight')) || (counter.get('Special') >= 3 && ['flipturn', 'partingshot', 'uturn'].some(m => moves.has(m))) )) { const scarfReqs = ( species.baseStats.spa >= 100 && species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && ability !== 'Tinted Lens' && !counter.get('Physical') ); return (scarfReqs && this.randomChance(2, 3)) ? 'Choice Scarf' : 'Choice Specs'; } if ( !isDoubles && counter.get('Physical') >= 3 && !moves.has('rapidspin') && ['copycat', 'memento', 'partingshot'].some(m => moves.has(m)) ) return 'Choice Band'; if ( !isDoubles && ((counter.get('Physical') >= 3 && moves.has('defog')) || (counter.get('Special') >= 3 && moves.has('healingwish'))) && !counter.get('priority') && !moves.has('uturn') ) return 'Choice Scarf'; // Palkia sometimes wants Choice items instead if (species.name === 'Palkia') return 'Lustrous Orb'; // Other items if ( moves.has('raindance') || moves.has('sunnyday') || (ability === 'Speed Boost' && !counter.get('hazards')) || (ability === 'Stance Change' && counter.damagingMoves.size >= 3) ) return 'Life Orb'; if ( !isDoubles && this.dex.getEffectiveness('Rock', species) >= 1 && ( ['Defeatist', 'Emergency Exit', 'Multiscale'].includes(ability) || ['courtchange', 'defog', 'rapidspin'].some(m => moves.has(m)) ) ) return 'Heavy-Duty Boots'; if (species.name === 'Necrozma-Dusk-Mane' || ( this.dex.getEffectiveness('Ground', species) < 2 && counter.get('speedsetup') && counter.damagingMoves.size >= 3 && defensiveStatTotal >= 300 )) return 'Weakness Policy'; if (counter.damagingMoves.size >= 4 && defensiveStatTotal >= 235) return 'Assault Vest'; if ( ['clearsmog', 'curse', 'haze', 'healbell', 'protect', 'sleeptalk', 'strangesteam'].some(m => moves.has(m)) && !isDoubles ) return 'Leftovers'; } getLowPriorityItem( ability: string, types: Set, moves: Set, abilities: string[], counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, isNoDynamax: boolean ): string | undefined { const defensiveStatTotal = species.baseStats.hp + species.baseStats.def + species.baseStats.spd; if ( isLead && !isDoubles && !['Disguise', 'Sturdy'].includes(ability) && !moves.has('substitute') && !counter.get('drain') && !counter.get('recoil') && !counter.get('recovery') && ((defensiveStatTotal <= 250 && counter.get('hazards')) || defensiveStatTotal <= 210) ) return 'Focus Sash'; if ( moves.has('clangoroussoul') || // We manually check for speed-boosting moves, rather than using `counter.get('speedsetup')`, // because we want to check for ANY speed boosting move. // In particular, Shift Gear + Boomburst Toxtricity should get Throat Spray. (moves.has('boomburst') && Array.from(moves).some(m => Dex.moves.get(m).boosts?.spe)) ) return 'Throat Spray'; const rockWeaknessCase = ( this.dex.getEffectiveness('Rock', species) >= 1 && (!teamDetails.defog || ability === 'Intimidate' || moves.has('uturn') || moves.has('voltswitch')) ); const spinnerCase = (moves.has('rapidspin') && (ability === 'Regenerator' || !!counter.get('recovery'))); if (!isDoubles && (rockWeaknessCase || spinnerCase)) return 'Heavy-Duty Boots'; if ( !isDoubles && this.dex.getEffectiveness('Ground', species) >= 2 && !types.has('Poison') && ability !== 'Levitate' && !abilities.includes('Iron Barbs') ) return 'Air Balloon'; if ( !isDoubles && counter.damagingMoves.size >= 3 && !counter.get('damage') && ability !== 'Sturdy' && (species.baseStats.spe >= 90 || !moves.has('voltswitch')) && ['foulplay', 'rapidspin', 'substitute', 'uturn'].every(m => !moves.has(m)) && ( counter.get('speedsetup') || // No Dynamax Buzzwole doesn't want Life Orb with Bulk Up + 3 attacks (counter.get('drain') && (!isNoDynamax || species.id !== 'buzzwole' || moves.has('roost'))) || moves.has('trickroom') || moves.has('psystrike') || (species.baseStats.spe > 40 && defensiveStatTotal < 275) ) ) return 'Life Orb'; if ( !isDoubles && counter.damagingMoves.size >= 4 && !counter.get('Dragon') && !counter.get('Normal') ) { return 'Expert Belt'; } if ( !isDoubles && !moves.has('substitute') && (moves.has('dragondance') || moves.has('swordsdance')) && (moves.has('outrage') || ( ['Bug', 'Fire', 'Ground', 'Normal', 'Poison'].every(type => !types.has(type)) && !['Pastel Veil', 'Storm Drain'].includes(ability) )) ) return 'Lum Berry'; } getLevel( species: Species, isDoubles: boolean, isNoDynamax: boolean, ): number { const data = this.randomData[species.id]; // level set by rules if (this.adjustLevel) return this.adjustLevel; // doubles levelling if (isDoubles && data.doublesLevel) return data.doublesLevel; // No Dmax levelling if (isNoDynamax) { const tier = species.name.endsWith('-Gmax') ? this.dex.species.get(species.changesFrom).tier : species.tier; const tierScale: Partial> = { Uber: 76, OU: 80, UUBL: 81, UU: 82, RUBL: 83, RU: 84, NUBL: 85, NU: 86, PUBL: 87, PU: 88, "(PU)": 88, NFE: 88, }; const customScale: { [k: string]: number } = { // These Pokemon are too strong and need a lower level zaciancrowned: 65, calyrexshadow: 68, xerneas: 70, necrozmaduskmane: 72, zacian: 72, kyogre: 73, eternatus: 73, zekrom: 74, marshadow: 75, urshifurapidstrike: 79, haxorus: 80, inteleon: 80, cresselia: 83, jolteon: 84, swoobat: 84, dugtrio: 84, slurpuff: 84, polteageist: 84, wobbuffet: 86, scrafty: 86, // These Pokemon are too weak and need a higher level delibird: 100, vespiquen: 96, pikachu: 92, shedinja: 92, solrock: 90, arctozolt: 88, reuniclus: 87, decidueye: 87, noivern: 85, magnezone: 82, slowking: 81, }; return customScale[species.id] || tierScale[tier] || 80; } // BDSP tier levelling if (this.dex.currentMod === 'gen8bdsp') { const tierScale: Partial> = { Uber: 76, Unreleased: 76, OU: 80, UUBL: 81, UU: 82, RUBL: 83, RU: 84, NUBL: 85, NU: 86, PUBL: 87, PU: 88, "(PU)": 88, NFE: 88, }; const customScale: { [k: string]: number } = { delibird: 100, dugtrio: 76, glalie: 76, luvdisc: 100, spinda: 100, unown: 100, }; return customScale[species.id] || tierScale[species.tier] || 80; } // Arbitrary levelling base on data files (typically winrate-influenced) if (data.level) return data.level; // Finally default to level 80 return 80; } getForme(species: Species): string { if (typeof species.battleOnly === 'string') { // Only change the forme. The species has custom moves, and may have different typing and requirements. return species.battleOnly; } if (species.cosmeticFormes) return this.sample([species.name].concat(species.cosmeticFormes)); if (species.name.endsWith('-Gmax')) return species.name.slice(0, -5); // Consolidate mostly-cosmetic formes, at least for the purposes of Random Battles if (['Magearna', 'Polteageist', 'Zarude'].includes(species.baseSpecies)) { return this.sample([species.name].concat(species.otherFormes!)); } if (species.baseSpecies === 'Basculin') return 'Basculin' + this.sample(['', '-Blue-Striped']); if (species.baseSpecies === 'Keldeo' && this.gen <= 7) return 'Keldeo' + this.sample(['', '-Resolute']); if (species.baseSpecies === 'Pikachu' && this.dex.currentMod === 'gen8') { return 'Pikachu' + this.sample( ['', '-Original', '-Hoenn', '-Sinnoh', '-Unova', '-Kalos', '-Alola', '-Partner', '-World'] ); } return species.name; } randomSet( species: string | Species, teamDetails: RandomTeamsTypes.TeamDetails = {}, isLead = false, isDoubles = false, isNoDynamax = false ): RandomTeamsTypes.RandomSet { species = this.dex.species.get(species); const forme = this.getForme(species); const gmax = species.name.endsWith('-Gmax'); const data = this.randomData[species.id]; const randMoves = (isDoubles && data.doublesMoves) || (isNoDynamax && data.noDynamaxMoves) || data.moves; const movePool: string[] = [...(randMoves || this.dex.species.getMovePool(species.id))]; if (this.format.playerCount > 2) { // Random Multi Battle uses doubles move pools, but Ally Switch fails in multi battles // Random Free-For-All also uses doubles move pools, for now const allySwitch = movePool.indexOf('allyswitch'); if (allySwitch > -1) { if (movePool.length > this.maxMoveCount) { this.fastPop(movePool, allySwitch); } else { // Ideally, we'll never get here, but better to have a move that usually does nothing than one that always does movePool[allySwitch] = 'sleeptalk'; } } } const rejectedPool = []; let ability = ''; let item = undefined; const evs = { hp: 85, atk: 85, def: 85, spa: 85, spd: 85, spe: 85 }; const ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; const types = new Set(species.types); const abilitiesSet = new Set(Object.values(species.abilities)); if (species.unreleasedHidden) abilitiesSet.delete(species.abilities.H); const abilities = Array.from(abilitiesSet); const moves = new Set(); let counter: MoveCounter; // This is just for BDSP Unown; // it can be removed from this file if BDSP gets its own random-teams file in the future. let hasHiddenPower = false; do { // Choose next 4 moves from learnset/viable moves and add them to moves list: const pool = (movePool.length ? movePool : rejectedPool); while (moves.size < this.maxMoveCount && pool.length) { const moveid = this.sampleNoReplace(pool); if (moveid.startsWith('hiddenpower')) { if (hasHiddenPower) continue; hasHiddenPower = true; } moves.add(moveid); } counter = this.queryMoves(moves, species.types, abilities, movePool); const runEnforcementChecker = (checkerName: string) => { if (!this.moveEnforcementCheckers[checkerName]) return false; return this.moveEnforcementCheckers[checkerName]( movePool, moves, abilities, types, counter, species, teamDetails ); }; // Iterate through the moves again, this time to cull them: for (const moveid of moves) { const move = this.dex.moves.get(moveid); let { cull, isSetup } = this.shouldCullMove( move, types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, isNoDynamax ); if (move.id !== 'photongeyser' && ( (move.category === 'Physical' && counter.setupType === 'Special') || (move.category === 'Special' && counter.setupType === 'Physical') )) { // Reject STABs last in case the setup type changes later on const stabs = counter.get(species.types[0]) + (species.types[1] ? counter.get(species.types[1]) : 0); if (!types.has(move.type) || stabs > 1 || counter.get(move.category) < 2) cull = true; } // Pokemon should have moves that benefit their types, stats, or ability const isLowBP = move.basePower && move.basePower < 50; // Genesect-Douse should never reject Techno Blast const moveIsRejectable = ( !(species.id === 'genesectdouse' && move.id === 'technoblast') && !(species.id === 'togekiss' && move.id === 'nastyplot') && !(species.id === 'shuckle' && ['stealthrock', 'stickyweb'].includes(move.id)) && ( move.category === 'Status' || (!types.has(move.type) && move.id !== 'judgment') || (isLowBP && !move.multihit && !abilities.includes('Technician')) ) ); // Setup-supported moves should only be rejected under specific circumstances const notImportantSetup = ( !counter.setupType || counter.setupType === 'Mixed' || (counter.get(counter.setupType) + counter.get('Status') > 3 && !counter.get('hazards')) || (move.category !== counter.setupType && move.category !== 'Status') ); if (moveIsRejectable && ( !cull && !isSetup && !move.weather && !move.stallingMove && notImportantSetup && !move.damage && (isDoubles ? this.unrejectableMovesInDoubles(move) : this.unrejectableMovesInSingles(move)) )) { // There may be more important moves that this Pokemon needs if ( // Pokemon should have at least one STAB move (!counter.get('stab') && counter.get('physicalpool') + counter.get('specialpool') > 0 && move.id !== 'stickyweb') || // Swords Dance Mew should have Brave Bird (moves.has('swordsdance') && species.id === 'mew' && runEnforcementChecker('Flying')) || // Dhelmise should have Anchor Shot (abilities.includes('Steelworker') && runEnforcementChecker('Steel')) || // Check for miscellaneous important moves (!isDoubles && runEnforcementChecker('recovery') && move.id !== 'stickyweb') || runEnforcementChecker('screens') || runEnforcementChecker('misc') || ((isLead || species.id === 'shuckle') && runEnforcementChecker('lead')) || (moves.has('leechseed') && runEnforcementChecker('leechseed')) ) { cull = true; // Pokemon should have moves that benefit their typing // Don't cull Sticky Web in type-based enforcement, and make sure Azumarill always has Aqua Jet } else if (move.id !== 'stickyweb' && !(species.id === 'azumarill' && move.id === 'aquajet')) { for (const type of types) { if (runEnforcementChecker(type)) { cull = true; } } } } // Sleep Talk shouldn't be selected without Rest if (move.id === 'rest' && cull) { const sleeptalk = movePool.indexOf('sleeptalk'); if (sleeptalk >= 0) { if (movePool.length < 2) { cull = false; } else { this.fastPop(movePool, sleeptalk); } } } // Remove rejected moves from the move list if (cull && movePool.length) { if (moveid.startsWith('hiddenpower')) hasHiddenPower = false; if (move.category !== 'Status' && !move.damage) rejectedPool.push(moveid); moves.delete(moveid); break; } if (cull && rejectedPool.length) { if (moveid.startsWith('hiddenpower')) hasHiddenPower = false; moves.delete(moveid); break; } } } while (moves.size < this.maxMoveCount && (movePool.length || rejectedPool.length)); // for BD/SP only if (hasHiddenPower) { let hpType; for (const move of moves) { if (move.startsWith('hiddenpower')) hpType = move.substr(11); } if (!hpType) throw new Error(`hasHiddenPower is true, but no Hidden Power move was found.`); const HPivs = this.dex.types.get(hpType).HPivs; let iv: StatID; for (iv in HPivs) { ivs[iv] = HPivs[iv]!; } } ability = this.getAbility(types, moves, abilities, counter, movePool, teamDetails, species, '', '', isDoubles, isNoDynamax); if (species.requiredItems) { item = this.sample(species.requiredItems); // First, the extra high-priority items } else { item = this.getHighPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, isDoubles); if (item === undefined && isDoubles) { item = this.getDoublesItem(ability, types, moves, abilities, counter, teamDetails, species); } if (item === undefined) { item = this.getMediumPriorityItem(ability, moves, counter, species, isLead, isDoubles, isNoDynamax); } if (item === undefined) { item = this.getLowPriorityItem( ability, types, moves, abilities, counter, teamDetails, species, isLead, isDoubles, isNoDynamax ); } // fallback if (item === undefined) item = isDoubles ? 'Sitrus Berry' : 'Leftovers'; } // For Trick / Switcheroo if (item === 'Leftovers' && types.has('Poison')) { item = 'Black Sludge'; } const level: number = this.getLevel(species, isDoubles, isNoDynamax); // Prepare optimal HP const srImmunity = ability === 'Magic Guard' || item === 'Heavy-Duty Boots'; const srWeakness = srImmunity ? 0 : this.dex.getEffectiveness('Rock', species); while (evs.hp > 1) { const hp = Math.floor(Math.floor(2 * species.baseStats.hp + ivs.hp + Math.floor(evs.hp / 4) + 100) * level / 100 + 10); const multipleOfFourNecessary = (moves.has('substitute') && !['Leftovers', 'Black Sludge'].includes(item) && ( item === 'Sitrus Berry' || item === 'Salac Berry' || ability === 'Power Construct' )); if (multipleOfFourNecessary) { // Two Substitutes should activate Sitrus Berry if (hp % 4 === 0) break; } else if (moves.has('bellydrum') && (item === 'Sitrus Berry' || ability === 'Gluttony')) { // Belly Drum should activate Sitrus Berry if (hp % 2 === 0) break; } else if (moves.has('substitute') && moves.has('reversal')) { // Reversal users should be able to use four Substitutes if (hp % 4 > 0) break; } else { // Maximize number of Stealth Rock switch-ins if (srWeakness <= 0 || hp % (4 / srWeakness) > 0) break; } evs.hp -= 4; } if (moves.has('shellsidearm') && item === 'Choice Specs') evs.atk -= 8; // Minimize confusion damage const noAttackStatMoves = [...moves].every(m => { const move = this.dex.moves.get(m); if (move.damageCallback || move.damage) return true; return move.category !== 'Physical' || move.id === 'bodypress'; }); if (noAttackStatMoves && !moves.has('transform') && (!moves.has('shellsidearm') || !counter.get('Status'))) { evs.atk = 0; ivs.atk = 0; } // Ensure Nihilego's Beast Boost gives it Special Attack boosts instead of Special Defense if (forme === 'Nihilego') evs.spd -= 32; if (moves.has('gyroball') || moves.has('trickroom')) { evs.spe = 0; ivs.spe = 0; } return { name: species.baseSpecies, species: forme, gender: species.gender, shiny: this.randomChance(1, 1024), gigantamax: gmax, level, moves: Array.from(moves), ability, evs, ivs, item, }; } getPokemonPool( type: string, pokemonToExclude: RandomTeamsTypes.RandomSet[] = [], isMonotype = false, pokemonList: string[] ): [{ [k: string]: string[] }, string[]] { const exclude = pokemonToExclude.map(p => toID(p.species)); const pokemonPool: { [k: string]: string[] } = {}; const baseSpeciesPool = []; for (const pokemon of pokemonList) { let species = this.dex.species.get(pokemon); if (exclude.includes(species.id)) continue; if (isMonotype) { if (!species.types.includes(type)) continue; if (typeof species.battleOnly === 'string') { species = this.dex.species.get(species.battleOnly); if (!species.types.includes(type)) continue; } } if (species.baseSpecies in pokemonPool) { pokemonPool[species.baseSpecies].push(pokemon); } else { pokemonPool[species.baseSpecies] = [pokemon]; } } // Include base species 1x if 1-3 formes, 2x if 4-6 formes, 3x if 7+ formes for (const baseSpecies of Object.keys(pokemonPool)) { // Squawkabilly has 4 formes, but only 2 functionally different formes, so only include it 1x const weight = (baseSpecies === 'Squawkabilly') ? 1 : Math.min(Math.ceil(pokemonPool[baseSpecies].length / 3), 3); for (let i = 0; i < weight; i++) baseSpeciesPool.push(baseSpecies); } return [pokemonPool, baseSpeciesPool]; } randomTeam() { 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(); 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 = []; for (const poke of Object.keys(this.randomData)) { if (isDoubles && this.randomData[poke]?.doublesMoves || !isDoubles && this.randomData[poke]?.moves) { pokemonList.push(poke); } } const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, pokemonList); while (baseSpeciesPool.length && pokemon.length < this.maxTeamSize) { const baseSpecies = this.sampleNoReplace(baseSpeciesPool); let species = this.dex.species.get(this.sample(pokemonPool[baseSpecies])); if (!species.exists) continue; // Limit to one of each species (Species Clause) if (baseFormes[species.baseSpecies]) continue; // Illusion shouldn't be on the last slot if (species.name === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue; // The sixth slot should not be Zacian/Zamazenta/Eternatus if Zoroark is present, // as they make dynamax malfunction, regardless of level if ( pokemon.some(pkmn => pkmn.name === 'Zoroark') && pokemon.length >= (this.maxTeamSize - 1) && ['Zacian', 'Zacian-Crowned', 'Zamazenta', 'Zamazenta-Crowned', 'Eternatus'].includes(species.name) ) { 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 && numMaxLevelPokemon >= limitFactor && (this.getLevel(species, isDoubles, this.dex.formats.getRuleTable(this.format).has('dynamaxclause')) === 100) ) 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, pokemon.length === 0, isDoubles, this.dex.formats.getRuleTable(this.format).has('dynamaxclause')); // Okay, the set passes, add it to our team 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.moves.includes('sunnyday')) teamDetails.sun = 1; if (set.ability === 'Sand Stream') teamDetails.sand = 1; if (set.ability === 'Snow Warning') teamDetails.hail = 1; if (set.moves.includes('spikes')) teamDetails.spikes = (teamDetails.spikes || 0) + 1; if (set.moves.includes('stealthrock')) teamDetails.stealthRock = 1; if (set.moves.includes('stickyweb')) teamDetails.stickyWeb = 1; if (set.moves.includes('toxicspikes')) teamDetails.toxicSpikes = 1; if (set.moves.includes('defog')) teamDetails.defog = 1; if (set.moves.includes('rapidspin')) teamDetails.rapidSpin = 1; if (set.moves.includes('auroraveil') || (set.moves.includes('reflect') && set.moves.includes('lightscreen'))) { teamDetails.screens = 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; } randomCAP1v1Sets: AnyObject = require('./cap-1v1-sets.json'); randomCAP1v1Team() { this.enforceNoDirectCustomBanlistChanges(); const pokemon = []; const pokemonPool = Object.keys(this.randomCAP1v1Sets); while (pokemonPool.length && pokemon.length < this.maxTeamSize) { const species = this.dex.species.get(this.sampleNoReplace(pokemonPool)); if (!species.exists) throw new Error(`Invalid Pokemon "${species}" in ${this.format}`); if (this.forceMonotype && !species.types.includes(this.forceMonotype)) continue; const setData: AnyObject = this.sample(this.randomCAP1v1Sets[species.name]); const set = { name: species.baseSpecies, species: species.name, gender: species.gender, item: this.sampleIfArray(setData.item) || '', ability: (this.sampleIfArray(setData.ability)), shiny: this.randomChance(1, 1024), level: this.adjustLevel || 100, evs: { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, ...setData.evs }, nature: setData.nature, ivs: { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31, ...setData.ivs || {} }, moves: setData.moves.map((move: any) => this.sampleIfArray(move)), }; if (this.adjustLevel) set.level = this.adjustLevel; pokemon.push(set); } return pokemon; } randomFactorySets: { [format: string]: { [species: string]: BattleFactorySpecies } } = require('./factory-sets.json'); randomFactorySet( species: Species, teamData: RandomTeamsTypes.FactoryTeamDetails, tier: string ): RandomTeamsTypes.RandomFactorySet | null { const id = toID(species.name); const setList = this.randomFactorySets[tier][id].sets; const itemsMax: { [k: string]: number } = { choicespecs: 1, choiceband: 1, choicescarf: 1, }; const movesMax: { [k: string]: number } = { rapidspin: 1, batonpass: 1, stealthrock: 1, defog: 1, spikes: 1, toxicspikes: 1, }; const requiredMoves: { [k: string]: string } = { stealthrock: 'hazardSet', rapidspin: 'hazardClear', defog: 'hazardClear', }; const weatherAbilities = ['drizzle', 'drought', 'snowwarning', 'sandstream']; // Build a pool of eligible sets, given the team partners // Also keep track of sets with moves the team requires let effectivePool: { set: AnyObject, moveVariants?: number[], item?: string, ability?: string }[] = []; const priorityPool = []; for (const curSet of setList) { // if (this.forceMonotype && !species.types.includes(this.forceMonotype)) continue; // reject disallowed items, specifically a second of any given choice item const allowedItems: string[] = []; for (const itemString of curSet.item) { const item = this.dex.items.get(itemString); if (itemsMax[item.id] && teamData.has[item.id] >= itemsMax[item.id]) continue; allowedItems.push(itemString); } if (allowedItems.length === 0) continue; const curSetItem = this.sample(allowedItems); // reject 2+ weather setters const allowedAbilities: string[] = []; for (const abilityString of curSet.ability) { const ability = this.dex.abilities.get(abilityString); if (teamData.weather && weatherAbilities.includes(ability.id)) continue; allowedAbilities.push(abilityString); } if (allowedAbilities.length === 0) continue; const curSetAbility = this.sample(allowedAbilities); let reject = false; let hasRequiredMove = false; const curSetVariants = []; for (const move of curSet.moves) { const variantIndex = this.random(move.length); const moveId = toID(move[variantIndex]); if (movesMax[moveId] && teamData.has[moveId] >= movesMax[moveId]) { reject = true; break; } if (requiredMoves[moveId] && !teamData.has[requiredMoves[moveId]]) { hasRequiredMove = true; } curSetVariants.push(variantIndex); } if (reject) continue; const fullSetSpec = { set: curSet, moveVariants: curSetVariants, item: curSetItem, ability: curSetAbility }; effectivePool.push(fullSetSpec); if (hasRequiredMove) priorityPool.push(fullSetSpec); } if (priorityPool.length) effectivePool = priorityPool; if (!effectivePool.length) { if (!teamData.forceResult) return null; for (const curSet of setList) { effectivePool.push({ set: curSet }); } } const setData = this.sample(effectivePool); const moves = []; for (const [i, moveSlot] of setData.set.moves.entries()) { moves.push(setData.moveVariants ? moveSlot[setData.moveVariants[i]] : this.sample(moveSlot)); } const item = setData.item || this.sampleIfArray(setData.set.item); const ability = setData.ability || this.sampleIfArray(setData.set.ability); const nature = this.sampleIfArray(setData.set.nature); const level = this.adjustLevel || setData.set.level || (tier === "LC" ? 5 : 100); return { name: setData.set.name || species.baseSpecies, species: setData.set.species, gender: setData.set.gender || species.gender || (this.randomChance(1, 2) ? 'M' : 'F'), item: item || '', ability: ability || species.abilities['0'], shiny: typeof setData.set.shiny === 'undefined' ? this.randomChance(1, 1024) : setData.set.shiny, level, happiness: typeof setData.set.happiness === 'undefined' ? 255 : setData.set.happiness, evs: { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, ...setData.set.evs }, ivs: { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31, ...setData.set.ivs }, nature: nature || 'Serious', moves, }; } randomFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] { this.enforceNoDirectCustomBanlistChanges(); const forceResult = (depth >= 12); // Leaving Monotype code in comments in case it's used in the future // const isMonotype = !!this.forceMonotype || this.dex.formats.getRuleTable(this.format).has('sametypeclause'); // The teams generated depend on the tier choice in such a way that // no exploitable information is leaked from rolling the tier in getTeam(p1). if (!this.factoryTier) { // this.factoryTier = isMonotype ? 'Mono' : this.sample(['Uber', 'OU', 'UU', 'RU', 'NU', 'PU', 'LC']); this.factoryTier = this.sample(['Uber', 'OU', 'UU', 'RU', 'NU', 'PU', 'LC']); } /* } else if (isMonotype && this.factoryTier !== 'Mono') { // I don't think this can ever happen? throw new Error(`Can't generate a Monotype Battle Factory set in a battle with factory tier ${this.factoryTier}`); } */ const tierValues: { [k: string]: number } = { Uber: 5, OU: 4, UUBL: 4, UU: 3, RUBL: 3, RU: 2, NUBL: 2, NU: 1, PUBL: 1, PU: 0, }; const pokemon = []; const pokemonPool = Object.keys(this.randomFactorySets[this.factoryTier]); // const typePool = this.dex.types.names(); // const type = this.sample(typePool); const teamData: TeamData = { typeCount: {}, typeComboCount: {}, baseFormes: {}, has: {}, forceResult, weaknesses: {}, resistances: {}, }; const requiredMoveFamilies = ['hazardSet', 'hazardClear']; const requiredMoves: { [k: string]: string } = { stealthrock: 'hazardSet', rapidspin: 'hazardClear', defog: 'hazardClear', }; const weatherAbilitiesSet: { [k: string]: string } = { drizzle: 'raindance', drought: 'sunnyday', snowwarning: 'hail', sandstream: 'sandstorm', }; const resistanceAbilities: { [k: string]: string[] } = { dryskin: ['Water'], waterabsorb: ['Water'], stormdrain: ['Water'], flashfire: ['Fire'], heatproof: ['Fire'], lightningrod: ['Electric'], motordrive: ['Electric'], voltabsorb: ['Electric'], sapsipper: ['Grass'], thickfat: ['Ice', 'Fire'], levitate: ['Ground'], }; while (pokemonPool.length && pokemon.length < this.maxTeamSize) { const species = this.dex.species.get(this.sampleNoReplace(pokemonPool)); if (!species.exists) continue; // Lessen the need of deleting sets of Pokemon after tier shifts if ( this.factoryTier in tierValues && species.tier in tierValues && tierValues[species.tier] > tierValues[this.factoryTier] ) continue; // const speciesFlags = this.randomFactorySets[this.factoryTier][species.id].flags; // Limit to one of each species (Species Clause) if (teamData.baseFormes[species.baseSpecies]) continue; const set = this.randomFactorySet(species, teamData, this.factoryTier); if (!set) continue; const itemData = this.dex.items.get(set.item); const types = species.types; // Dynamically scale limits for different team sizes. The default and minimum value is 1. const limitFactor = Math.round(this.maxTeamSize / 6) || 1; /* // Enforce Monotype if (isMonotype) { // Prevents Mega Evolutions from breaking the type limits if (itemData.megaStone) { const megaSpecies = this.dex.species.get(itemData.megaStone); if (types.length > megaSpecies.types.length) types = [species.types[0]]; // Only check the second type because a Mega Evolution should always share the first type with its base forme. if (megaSpecies.types[1] && types[1] && megaSpecies.types[1] !== types[1]) { types = [megaSpecies.types[0]]; } } if (!types.includes(type)) continue; } else */ { // If not Monotype, limit to two of each type let skip = false; for (const typeName of types) { if (teamData.typeCount[typeName] >= 2 * limitFactor && this.randomChance(4, 5)) { skip = true; break; } } if (skip) continue; // Limit 1 of any type combination let typeCombo = types.slice().sort().join(); if (set.ability === 'Drought' || set.ability === 'Drizzle') { // Drought and Drizzle don't count towards the type combo limit typeCombo = set.ability; } if (teamData.typeComboCount[typeCombo] >= limitFactor) continue; } // Okay, the set passes, add it to our team pokemon.push(set); const typeCombo = types.slice().sort().join(); // Now that our Pokemon has passed all checks, we can update team data: for (const typeName of types) { if (typeName in teamData.typeCount) { teamData.typeCount[typeName]++; } else { teamData.typeCount[typeName] = 1; } } teamData.typeComboCount[typeCombo] = (teamData.typeComboCount[typeCombo] + 1) || 1; teamData.baseFormes[species.baseSpecies] = 1; if (itemData.id in teamData.has) { teamData.has[itemData.id]++; } else { teamData.has[itemData.id] = 1; } const abilityState = this.dex.abilities.get(set.ability); if (abilityState.id in weatherAbilitiesSet) { teamData.weather = weatherAbilitiesSet[abilityState.id]; } for (const move of set.moves) { const moveId = toID(move); if (moveId in teamData.has) { teamData.has[moveId]++; } else { teamData.has[moveId] = 1; } if (moveId in requiredMoves) { teamData.has[requiredMoves[moveId]] = 1; } } for (const typeName of this.dex.types.names()) { // Cover any major weakness (3+) with at least one resistance if (teamData.resistances[typeName] >= 1) continue; if (resistanceAbilities[abilityState.id]?.includes(typeName) || !this.dex.getImmunity(typeName, types)) { // Heuristic: assume that Pokémon with these abilities don't have (too) negative typing. teamData.resistances[typeName] = (teamData.resistances[typeName] || 0) + 1; if (teamData.resistances[typeName] >= 1) teamData.weaknesses[typeName] = 0; continue; } const typeMod = this.dex.getEffectiveness(typeName, types); if (typeMod < 0) { teamData.resistances[typeName] = (teamData.resistances[typeName] || 0) + 1; if (teamData.resistances[typeName] >= 1) teamData.weaknesses[typeName] = 0; } else if (typeMod > 0) { teamData.weaknesses[typeName] = (teamData.weaknesses[typeName] || 0) + 1; } } } if (pokemon.length < this.maxTeamSize) return this.randomFactoryTeam(side, ++depth); // Quality control if (!teamData.forceResult) { for (const requiredFamily of requiredMoveFamilies) { if (!teamData.has[requiredFamily]) return this.randomFactoryTeam(side, ++depth); } for (const typeName in teamData.weaknesses) { if (teamData.weaknesses[typeName] >= 3) return this.randomFactoryTeam(side, ++depth); } } return pokemon; } randomBSSFactorySets: AnyObject = require('./bss-factory-sets.json'); randomBSSFactorySet( species: Species, teamData: RandomTeamsTypes.FactoryTeamDetails ): RandomTeamsTypes.RandomFactorySet | null { const id = toID(species.name); const setList = this.randomBSSFactorySets[id].sets; const movesMax: { [k: string]: number } = { batonpass: 1, stealthrock: 1, toxicspikes: 1, trickroom: 1, auroraveil: 1, }; const requiredMoves: { [k: string]: number } = {}; // Build a pool of eligible sets, given the team partners // Also keep track of sets with moves the team requires let effectivePool: { set: AnyObject, moveVariants?: number[], itemVariants?: number, abilityVariants?: number }[] = []; const priorityPool = []; for (const curSet of setList) { let reject = false; let hasRequiredMove = false; const curSetMoveVariants = []; for (const move of curSet.moves) { const variantIndex = this.random(move.length); const moveId = toID(move[variantIndex]); if (movesMax[moveId] && teamData.has[moveId] >= movesMax[moveId]) { reject = true; break; } if (requiredMoves[moveId] && !teamData.has[requiredMoves[moveId]]) { hasRequiredMove = true; } curSetMoveVariants.push(variantIndex); } if (reject) continue; const set = { set: curSet, moveVariants: curSetMoveVariants }; effectivePool.push(set); if (hasRequiredMove) priorityPool.push(set); } if (priorityPool.length) effectivePool = priorityPool; if (!effectivePool.length) { if (!teamData.forceResult) return null; for (const curSet of setList) { effectivePool.push({ set: curSet }); } } const setData = this.sample(effectivePool); const moves = []; for (const [i, moveSlot] of setData.set.moves.entries()) { moves.push(setData.moveVariants ? moveSlot[setData.moveVariants[i]] : this.sample(moveSlot)); } const setDataAbility = this.sampleIfArray(setData.set.ability); return { name: setData.set.nickname || setData.set.name || species.baseSpecies, species: setData.set.species, gigantamax: setData.set.gigantamax, gender: setData.set.gender || species.gender || (this.randomChance(1, 2) ? 'M' : 'F'), item: this.sampleIfArray(setData.set.item) || '', ability: setDataAbility || species.abilities['0'], shiny: typeof setData.set.shiny === 'undefined' ? this.randomChance(1, 1024) : setData.set.shiny, level: setData.set.level || 50, happiness: typeof setData.set.happiness === 'undefined' ? 255 : setData.set.happiness, evs: { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, ...setData.set.evs }, ivs: { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31, ...setData.set.ivs }, nature: setData.set.nature || 'Serious', moves, }; } randomBSSFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] { this.enforceNoDirectCustomBanlistChanges(); const forceResult = (depth >= 4); const pokemon = []; const pokemonPool = Object.keys(this.randomBSSFactorySets); const teamData: TeamData = { typeCount: {}, typeComboCount: {}, baseFormes: {}, has: {}, forceResult, weaknesses: {}, resistances: {}, }; const weatherAbilitiesSet: { [k: string]: string } = { drizzle: 'raindance', drought: 'sunnyday', snowwarning: 'hail', sandstream: 'sandstorm', }; const resistanceAbilities: { [k: string]: string[] } = { waterabsorb: ['Water'], flashfire: ['Fire'], lightningrod: ['Electric'], voltabsorb: ['Electric'], thickfat: ['Ice', 'Fire'], levitate: ['Ground'], }; const limitFactor = Math.ceil(this.maxTeamSize / 6); /** * Weighted random shuffle * Uses the fact that for two uniform variables x1 and x2, x1^(1/w1) is larger than x2^(1/w2) * with probability equal to w1/(w1+w2), which is what we want. See e.g. here https://arxiv.org/pdf/1012.0256.pdf, * original paper is behind a paywall. */ const shuffledSpecies = []; for (const speciesName of pokemonPool) { const sortObject = { speciesName, score: this.prng.random() ** (1 / this.randomBSSFactorySets[speciesName].usage), }; shuffledSpecies.push(sortObject); } shuffledSpecies.sort((a, b) => a.score - b.score); while (shuffledSpecies.length && pokemon.length < this.maxTeamSize) { // repeated popping from weighted shuffle is equivalent to repeated weighted sampling without replacement const specie = shuffledSpecies.pop()!.speciesName; const species = this.dex.species.get(specie); if (!species.exists) continue; if (this.forceMonotype && !species.types.includes(this.forceMonotype)) continue; // Limit to one of each species (Species Clause) if (teamData.baseFormes[species.baseSpecies]) continue; // Limit 2 of any type (most of the time) const types = species.types; let skip = false; if (!this.forceMonotype) { for (const type of types) { if (teamData.typeCount[type] >= 2 * limitFactor && this.randomChance(4, 5)) { skip = true; break; } } } if (skip) continue; const set = this.randomBSSFactorySet(species, teamData); if (!set) continue; // Limit 1 of any type combination let typeCombo = types.slice().sort().join(); if (set.ability === 'Drought' || set.ability === 'Drizzle') { // Drought and Drizzle don't count towards the type combo limit typeCombo = set.ability; } if (!this.forceMonotype && teamData.typeComboCount[typeCombo] >= limitFactor) continue; const itemData = this.dex.items.get(set.item); if (teamData.has[itemData.id]) continue; // Item Clause // Okay, the set passes, add it to our team pokemon.push(set); // Now that our Pokemon has passed all checks, we can update team data: for (const type of types) { if (type in teamData.typeCount) { teamData.typeCount[type]++; } else { teamData.typeCount[type] = 1; } } if (typeCombo in teamData.typeComboCount) { teamData.typeComboCount[typeCombo]++; } else { teamData.typeComboCount[typeCombo] = 1; } teamData.baseFormes[species.baseSpecies] = 1; teamData.has[itemData.id] = 1; const abilityState = this.dex.abilities.get(set.ability); if (abilityState.id in weatherAbilitiesSet) { teamData.weather = weatherAbilitiesSet[abilityState.id]; } for (const move of set.moves) { const moveId = toID(move); if (moveId in teamData.has) { teamData.has[moveId]++; } else { teamData.has[moveId] = 1; } } for (const typeName of this.dex.types.names()) { // Cover any major weakness (3+) with at least one resistance if (teamData.resistances[typeName] >= 1) continue; if (resistanceAbilities[abilityState.id]?.includes(typeName) || !this.dex.getImmunity(typeName, types)) { // Heuristic: assume that Pokémon with these abilities don't have (too) negative typing. teamData.resistances[typeName] = (teamData.resistances[typeName] || 0) + 1; if (teamData.resistances[typeName] >= 1) teamData.weaknesses[typeName] = 0; continue; } const typeMod = this.dex.getEffectiveness(typeName, types); if (typeMod < 0) { teamData.resistances[typeName] = (teamData.resistances[typeName] || 0) + 1; if (teamData.resistances[typeName] >= 1) teamData.weaknesses[typeName] = 0; } else if (typeMod > 0) { teamData.weaknesses[typeName] = (teamData.weaknesses[typeName] || 0) + 1; } } } if (!teamData.forceResult && pokemon.length < this.maxTeamSize) return this.randomBSSFactoryTeam(side, ++depth); // Quality control we cannot afford for monotype if (!teamData.forceResult && !this.forceMonotype) { for (const type in teamData.weaknesses) { if (teamData.weaknesses[type] >= 3 * limitFactor) return this.randomBSSFactoryTeam(side, ++depth); } } return pokemon; } } export default RandomGen8Teams;