'use strict'; /** @typedef {import('../sim/prng').PRNG} PRNG */ /** @type {typeof import('../sim/dex').Dex} */ const Dex = require(/** @type {any} */ ('../.sim-dist/dex')).Dex; /** @type {typeof import('../sim/prng').PRNG} */ const PRNG = require(/** @type {any} */ ('../.sim-dist/prng')).PRNG; /** * @typedef {Object} TeamData * @property {{[k: string]: number}} typeCount * @property {{[k: string]: number}} typeComboCount * @property {{[k: string]: number}} baseFormes * @property {number} megaCount * @property {number} [zCount] * @property {{[k: string]: number}} has * @property {boolean} forceResult * @property {{[k: string]: number}} weaknesses * @property {{[k: string]: number}} resistances * @property {string} [weather] * @property {number} [eeveeLimCount] */ class RandomTeams { /** * @param {Format | string} format * @param {?PRNG | [number, number, number, number]} [prng] */ constructor(format, prng) { format = Dex.getFormat(format); this.dex = Dex.forFormat(format); this.gen = this.dex.gen; // this.randomFactorySets = randomFactorySets; // this.randomBSSFactorySets = randomBSSFactorySets; this.factoryTier = ''; this.format = format; this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); } /** * @param {?PRNG | [number, number, number, number]} [prng] */ setSeed(prng) { this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); } /** * @param {PlayerOptions | null} [options] * @return {PokemonSet[]} */ getTeam(options) { const generatorName = typeof this.format.team === 'string' && this.format.team.startsWith('random') ? this.format.team + 'Team' : ''; // @ts-ignore return this[generatorName || 'randomTeam'](options); } /** * @param {number} numerator - the top part of the probability fraction * @param {number} denominator - the bottom part of the probability fraction * @return {boolean} - randomly true or false */ randomChance(numerator, denominator) { return this.prng.randomChance(numerator, denominator); } /** * @param {ReadonlyArray} items - the items to choose from * @return {T} - a random item from items * @template T */ sample(items) { return this.prng.sample(items); } /** * @param {number} [m] * @param {number} [n] * @return {number} */ random(m, n) { return this.prng.next(m, n); } /** * Remove an element from an unsorted array significantly faster * than .splice * * @param {any[]} list * @param {number} index */ fastPop(list, index) { // 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). let length = list.length; let 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. * * @param {any[]} list */ sampleNoReplace(list) { let length = list.length; let index = this.random(length); return this.fastPop(list, index); } /** * @param {Template} template */ checkBattleForme(template) { // If the Pokémon has a Mega or Primal alt forme, that's its preferred battle forme. // No randomization, no choice. We are just checking its existence. // Returns a Pokémon template for further details. if (!template.otherFormes) return null; let firstForme = this.dex.getTemplate(template.otherFormes[0]); if (firstForme.isMega || firstForme.isPrimal) return firstForme; return null; } // checkAbilities(selectedAbilities, defaultAbilities) { // if (!selectedAbilities.length) return true; // let selectedAbility = selectedAbilities.pop(); // let isValid = false; // for (let i = 0; i < defaultAbilities.length; i++) { // let defaultAbility = defaultAbilities[i]; // if (!defaultAbility) break; // if (defaultAbility.includes(selectedAbility)) { // defaultAbilities.splice(i, 1); // isValid = this.checkAbilities(selectedAbilities, defaultAbilities); // if (isValid) break; // defaultAbilities.splice(i, 0, defaultAbility); // } // } // if (!isValid) selectedAbilities.push(selectedAbility); // return isValid; // } // hasMegaEvo(template) { // if (!template.otherFormes) return false; // let firstForme = this.dex.getTemplate(template.otherFormes[0]); // return !!firstForme.isMega; // } /** * @return {RandomTeamsTypes.RandomSet[]} */ randomCCTeam() { let team = []; let natures = Object.keys(this.dex.data.Natures); let items = Object.keys(this.dex.data.Items); let random6 = this.random6Pokemon(); for (let i = 0; i < 6; i++) { let species = random6[i]; let template = Dex.mod('gen' + this.gen).getTemplate(species); // Random legal item let item = ''; if (this.gen >= 2) { do { item = this.sample(items); } while (this.dex.getItem(item).gen > this.gen || this.dex.data.Items[item].isNonstandard); } // Make sure forme is legal if (template.battleOnly || template.requiredItems && !template.requiredItems.some(req => toID(req) === item)) { template = Dex.mod('gen' + this.gen).getTemplate(template.baseSpecies); species = template.name; } // Make sure that a base forme does not hold any forme-modifier items. let itemData = this.dex.getItem(item); if (itemData.forcedForme && species === this.dex.getTemplate(itemData.forcedForme).baseSpecies) { do { item = this.sample(items); itemData = this.dex.getItem(item); } while (itemData.gen > this.gen || itemData.isNonstandard || itemData.forcedForme && species === this.dex.getTemplate(itemData.forcedForme).baseSpecies); } // Random legal ability let abilities = Object.values(template.abilities).filter(a => this.dex.getAbility(a).gen <= this.gen); /**@type {string} */ // @ts-ignore let ability = this.gen <= 2 ? 'None' : this.sample(abilities); // Four random unique moves from the movepool let moves; let pool = ['struggle']; if (species === 'Smeargle') { pool = Object.keys(this.dex.data.Movedex).filter(moveid => !(['chatter', 'struggle', 'paleowave', 'shadowstrike', 'magikarpsrevenge'].includes(moveid) || this.dex.data.Movedex[moveid].isZ || this.dex.data.Movedex[moveid].id === 'hiddenpower' && moveid !== 'hiddenpower')); } else if (template.learnset) { // @ts-ignore pool = Object.keys(template.learnset).filter(moveid => template.learnset[moveid].find(learned => learned.startsWith(this.gen))); if (template.species.substr(0, 9) === 'Necrozma-' || template.species.substr(0, 6) === 'Rotom-') { const learnset = this.dex.getTemplate(template.baseSpecies).learnset; if (learnset) pool = [...new Set(pool.concat(Object.keys(learnset)))]; } } else { const learnset = this.dex.getTemplate(template.baseSpecies).learnset; // @ts-ignore if (learnset) pool = Object.keys(learnset).filter(moveid => learnset[moveid].find(learned => learned.startsWith(this.gen))); } if (pool.length <= 4) { moves = pool; } else { moves = [this.sampleNoReplace(pool), this.sampleNoReplace(pool), this.sampleNoReplace(pool), this.sampleNoReplace(pool)]; } // Random EVs let evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; let s = ["hp", "atk", "def", "spa", "spd", "spe"]; let evpool = 510; do { let x = this.sample(s); // @ts-ignore let y = this.random(Math.min(256 - evs[x], evpool + 1)); // @ts-ignore evs[x] += y; evpool -= y; } while (evpool > 0); // Random IVs let 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 let nature = this.sample(natures); // Level balance--calculate directly from stats rather than using some silly lookup table let mbstmin = 1307; // Sunkern has the lowest modified base stat total, and that total is 807 let stats = template.baseStats; // If Wishiwashi, use the school-forme's much higher stats if (template.baseSpecies === 'Wishiwashi') stats = Dex.getTemplate('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 = 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); mbst += Math.floor(((stats["atk"] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); // Since damage is roughly proportional to level 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 let happiness = this.random(256); // Random shininess let shiny = this.randomChance(1, 1024); team.push({ name: template.baseSpecies, species: template.species, gender: template.gender, item: item, ability: ability, moves: moves, evs: evs, ivs: ivs, nature: nature, level: level, happiness: happiness, shiny: shiny, }); } return team; } random6Pokemon() { // Pick six 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 let last = [0, 151, 251, 386, 493, 649, 721, 807][this.gen]; /**@type {{[k: string]: number}} */ let hasDexNumber = {}; for (let i = 0; i < 6; i++) { let num; do { num = this.random(last) + 1; } while (num in hasDexNumber); hasDexNumber[num] = i; } /**@type {string[][]} */ let formes = [[], [], [], [], [], []]; for (let id in this.dex.data.Pokedex) { if (!(this.dex.data.Pokedex[id].num in hasDexNumber)) continue; let template = this.dex.getTemplate(id); if (template.gen <= this.gen && !template.isNonstandard) { formes[hasDexNumber[template.num]].push(template.species); } } let sixPokemon = []; for (let i = 0; i < 6; i++) { if (!formes[i].length) { throw new Error("Invalid pokemon gen " + this.gen + ": " + JSON.stringify(formes) + " numbers " + JSON.stringify(hasDexNumber)); } sixPokemon.push(this.sample(formes[i])); } return sixPokemon; } randomHCTeam() { let team = []; let itemPool = Object.keys(this.dex.data.Items); let abilityPool = Object.keys(this.dex.data.Abilities); let movePool = Object.keys(this.dex.data.Movedex); let naturePool = Object.keys(this.dex.data.Natures); let random6 = this.random6Pokemon(); for (let i = 0; i < 6; i++) { // Choose forme let template = this.dex.getTemplate(random6[i]); // Random unique item let item = ''; if (this.gen >= 2) { do { item = this.sampleNoReplace(itemPool); } while (this.dex.getItem(item).gen > this.gen || this.dex.data.Items[item].isNonstandard); } // Random unique ability let ability = 'None'; if (this.gen >= 3) { do { ability = this.sampleNoReplace(abilityPool); } while (this.dex.getAbility(ability).gen > this.gen || this.dex.data.Abilities[ability].isNonstandard); } // Random unique moves let m = []; do { let moveid = this.sampleNoReplace(movePool); if (this.dex.getMove(moveid).gen <= this.gen && !this.dex.data.Movedex[moveid].isNonstandard && (moveid === 'hiddenpower' || moveid.substr(0, 11) !== 'hiddenpower')) { m.push(moveid); } } while (m.length < 4); // Random EVs let evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; let s = ['hp', 'atk', 'def', 'spa', 'spd', 'spe']; if (this.gen === 6) { let evpool = 510; do { let x = this.sample(s); // @ts-ignore let y = this.random(Math.min(256 - evs[x], evpool + 1)); // @ts-ignore evs[x] += y; evpool -= y; } while (evpool > 0); } else { for (const x of s) { // @ts-ignore evs[x] = this.random(256); } } // Random IVs let 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 let nature = this.sample(naturePool); // Level balance let mbstmin = 1307; let stats = template.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 = 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 let happiness = this.random(256); // Random shininess let shiny = this.randomChance(1, 1024); team.push({ name: template.baseSpecies, species: template.species, gender: template.gender, item: item, ability: ability, moves: m, evs: evs, ivs: ivs, nature: nature, level: level, happiness: happiness, shiny: shiny, }); } return team; } /** * @param {?string[]} moves * @param {{[k: string]: boolean}} [hasType] * @param {{[k: string]: boolean}} [hasAbility] * @param {string[]} [movePool] */ queryMoves(moves, hasType = {}, hasAbility = {}, movePool = []) { // This is primarily a helper function for random setbuilder functions. let counter = { Physical: 0, Special: 0, Status: 0, damage: 0, recovery: 0, stab: 0, inaccurate: 0, priority: 0, recoil: 0, drain: 0, sound: 0, adaptability: 0, bite: 0, contrary: 0, hustle: 0, ironfist: 0, serenegrace: 0, sheerforce: 0, skilllink: 0, technician: 0, physicalsetup: 0, specialsetup: 0, mixedsetup: 0, speedsetup: 0, physicalpool: 0, specialpool: 0, /**@type {Move[]} */ damagingMoves: [], /**@type {{[k: string]: number}} */ damagingMoveIndex: {}, setupType: '', // typescript Bug: 0, Dark: 0, Dragon: 0, Electric: 0, Fairy: 0, Fighting: 0, Fire: 0, Flying: 0, Ghost: 0, Grass: 0, Ground: 0, Ice: 0, Normal: 0, Poison: 0, Psychic: 0, Rock: 0, Steel: 0, Water: 0, }; for (let type in Dex.data.TypeChart) { // @ts-ignore counter[type] = 0; } if (!moves || !moves.length) return counter; // Moves that heal a fixed amount: let RecoveryMove = [ 'healorder', 'milkdrink', 'moonlight', 'morningsun', 'recover', 'roost', 'shoreup', 'slackoff', 'softboiled', 'synthesis', ]; // Moves which drop stats: let ContraryMove = [ 'closecombat', 'leafstorm', 'overheat', 'superpower', 'vcreate', ]; // Moves that boost Attack: let PhysicalSetup = [ 'bellydrum', 'bulkup', 'coil', 'curse', 'dragondance', 'honeclaws', 'howl', 'poweruppunch', 'swordsdance', ]; // Moves which boost Special Attack: let SpecialSetup = [ 'calmmind', 'chargebeam', 'geomancy', 'nastyplot', 'quiverdance', 'tailglow', ]; // Moves which boost Attack AND Special Attack: let MixedSetup = [ 'clangoroussoul', 'growth', 'happyhour', 'holdhands', 'noretreat', 'shellsmash', 'workup', ]; // Moves which boost Speed: let SpeedSetup = [ 'agility', 'autotomize', 'rockpolish', 'shiftgear', ]; // Moves that shouldn't be the only STAB moves: let NoStab = [ 'accelerock', 'aquajet', 'bounce', 'explosion', 'fakeout', 'firstimpression', 'flamecharge', 'iceshard', 'pluck', 'pursuit', 'quickattack', 'selfdestruct', 'suckerpunch', 'watershuriken', 'chargebeam', 'clearsmog', 'eruption', 'vacuumwave', 'waterspout', ]; // Iterate through all moves we've chosen so far and keep track of what they do: for (const [k, moveId] of moves.entries()) { let move = this.dex.getMove(moveId); let moveid = move.id; let movetype = move.type; if (['judgment', 'multiattack', 'revelationdance'].includes(moveid)) movetype = Object.keys(hasType)[0]; if (move.damage || move.damageCallback) { // Moves that do a set amount of damage: counter['damage']++; counter.damagingMoves.push(move); counter.damagingMoveIndex[moveid] = k; } else { // Are Physical/Special/Status moves: counter[move.category]++; } // Moves that have a low base power: if (moveid === 'lowkick' || (move.basePower && move.basePower <= 60 && moveid !== 'rapidspin')) counter['technician']++; // Moves that hit up to 5 times: if (move.multihit && Array.isArray(move.multihit) && move.multihit[1] === 5) counter['skilllink']++; if (move.recoil || move.hasCustomRecoil) counter['recoil']++; if (move.drain) counter['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' || moveid === 'naturepower') { // @ts-ignore counter[movetype]++; if (hasType[movetype] || movetype === 'Normal' && (hasAbility['Aerilate'] || hasAbility['Galvanize'] || hasAbility['Pixilate'] || hasAbility['Refrigerate'])) { counter['adaptability']++; // STAB: // Certain moves aren't acceptable as a Pokemon's only STAB attack if (!NoStab.includes(moveid) && (moveid !== 'hiddenpower' || Object.keys(hasType).length === 1)) { counter['stab']++; // Ties between Physical and Special setup should broken in favor of STABs counter[move.category] += 0.1; } } else if (move.priority === 0 && (hasAbility['Libero'] || hasAbility['Protean']) && !NoStab.includes(moveid)) { counter['stab']++; } else if (movetype === 'Steel' && hasAbility['Steelworker']) { counter['stab']++; } if (move.category === 'Physical') counter['hustle']++; if (move.flags['bite']) counter['bite']++; if (move.flags['punch']) counter['ironfist']++; if (move.flags['sound']) counter['sound']++; counter.damagingMoves.push(move); counter.damagingMoveIndex[moveid] = k; } // Moves with secondary effects: if (move.secondary) { counter['sheerforce']++; if (move.secondary.chance && move.secondary.chance >= 20 && move.secondary.chance < 100) { counter['serenegrace']++; } } // Moves with low accuracy: if (move.accuracy && move.accuracy !== true && move.accuracy < 90) counter['inaccurate']++; // Moves with non-zero priority: if (move.category !== 'Status' && move.priority !== 0) counter['priority']++; // Moves that change stats: if (RecoveryMove.includes(moveid)) counter['recovery']++; if (ContraryMove.includes(moveid)) counter['contrary']++; if (PhysicalSetup.includes(moveid)) { counter['physicalsetup']++; counter.setupType = 'Physical'; } else if (SpecialSetup.includes(moveid)) { counter['specialsetup']++; counter.setupType = 'Special'; } if (MixedSetup.includes(moveid)) counter['mixedsetup']++; if (SpeedSetup.includes(moveid)) counter['speedsetup']++; } // Keep track of the available moves for (const moveid of movePool) { let move = this.dex.getMove(moveid); if (move.damageCallback) continue; if (move.category === 'Physical') counter['physicalpool']++; if (move.category === 'Special') counter['specialpool']++; } // Choose a setup type: if (counter['mixedsetup']) { counter.setupType = 'Mixed'; } else if (counter.setupType) { let pool = { Physical: counter.Physical + counter['physicalpool'], Special: counter.Special + counter['specialpool'], }; if (counter['physicalsetup'] && counter['specialsetup']) { if (pool.Physical === pool.Special) { if (counter.Physical > counter.Special) counter.setupType = 'Physical'; if (counter.Special > counter.Physical) counter.setupType = 'Special'; } else { counter.setupType = pool.Physical > pool.Special ? 'Physical' : 'Special'; } // @ts-ignore } else if (!pool[counter.setupType] || pool[counter.setupType] === 1 && (!moves.includes('rest') || !moves.includes('sleeptalk'))) { counter.setupType = ''; } } counter['Physical'] = Math.floor(counter['Physical']); counter['Special'] = Math.floor(counter['Special']); return counter; } /** * @param {string | Template} template * @param {RandomTeamsTypes.TeamDetails} [teamDetails] * @param {boolean} [isLead] * @param {boolean} [isDoubles] * @return {RandomTeamsTypes.RandomSet} */ randomSet(template, teamDetails = {}, isLead = false, isDoubles = false) { template = this.dex.getTemplate(template); let baseTemplate = template; let species = template.species; if (!template.exists || ((!isDoubles || !template.randomDoubleBattleMoves) && !template.randomBattleMoves && !template.learnset)) { template = this.dex.getTemplate('pikachu'); let err = new Error('Template incompatible with random battles: ' + species); Monitor.crashlog(err, 'The randbat set generator'); } if (template.battleOnly) { // Only change the species. The template has custom moves, and may have different typing and requirements. species = template.baseSpecies; } let battleForme = this.checkBattleForme(template); if (battleForme && battleForme.randomBattleMoves && template.otherFormes && this.random(2)) { template = this.dex.getTemplate(template.otherFormes.length >= 2 ? this.sample(template.otherFormes) : template.otherFormes[0]); } const randMoves = !isDoubles ? template.randomBattleMoves : template.randomDoubleBattleMoves || template.randomBattleMoves; let movePool = (randMoves ? randMoves.slice() : template.learnset ? Object.keys(template.learnset) : []); let rejectedPool = []; /**@type {string[]} */ let moves = []; let ability = ''; let item = ''; let evs = { hp: 85, atk: 85, def: 85, spa: 85, spd: 85, spe: 85, }; let ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31, }; /**@type {{[k: string]: true}} */ let hasType = {}; hasType[template.types[0]] = true; if (template.types[1]) { hasType[template.types[1]] = true; } /**@type {{[k: string]: true}} */ let hasAbility = {}; hasAbility[template.abilities[0]] = true; if (template.abilities[1]) { // @ts-ignore hasAbility[template.abilities[1]] = true; } if (template.abilities['H']) { // @ts-ignore hasAbility[template.abilities['H']] = true; } // These moves can be used even if we aren't setting up to use them: let SetupException = ['closecombat', 'diamondstorm', 'extremespeed', 'superpower', 'clangingscales', 'dracometeor']; let counterAbilities = ['Adaptability', 'Contrary', 'Hustle', 'Iron Fist', 'Skill Link']; let ateAbilities = ['Aerilate', 'Galvanize', 'Pixilate', 'Refrigerate']; /**@type {{[k: string]: boolean}} */ let hasMove = {}; let counter; do { // Keep track of all moves we have: hasMove = {}; // Choose next 4 moves from learnset/viable moves and add them to moves list: let pool = (movePool.length ? movePool : rejectedPool); while (moves.length < 4 && pool.length) { let moveid = this.sampleNoReplace(pool); hasMove[moveid] = true; moves.push(moveid); } counter = this.queryMoves(moves, hasType, hasAbility, movePool); // Iterate through the moves again, this time to cull them: for (const [k, moveId] of moves.entries()) { let move = this.dex.getMove(moveId); let moveid = move.id; let rejected = false; let isSetup = false; switch (moveid) { // Not very useful without their supporting moves case 'cottonguard': case 'defendorder': if (!counter['recovery'] && !hasMove['rest']) rejected = true; break; case 'focuspunch': if (!hasMove['substitute'] || counter.damagingMoves.length < 2) rejected = true; break; case 'icebeam': if (hasAbility['Tinted Lens'] && !!counter['Status']) rejected = true; break; case 'perishsong': if (!hasMove['protect']) rejected = true; break; case 'reflect': if (!hasMove['calmmind'] && !hasMove['lightscreen']) rejected = true; if (movePool.length > 1) { let screen = movePool.indexOf('lightscreen'); if (screen >= 0) this.fastPop(movePool, screen); } break; case 'rest': if (movePool.includes('sleeptalk')) rejected = true; break; case 'sleeptalk': if (!hasMove['rest']) rejected = true; if (movePool.length > 1) { let rest = movePool.indexOf('rest'); if (rest >= 0) this.fastPop(movePool, rest); } break; case 'storedpower': if (!counter.setupType) rejected = true; break; case 'switcheroo': case 'trick': if (counter.Physical + counter.Special < 3 || hasMove['suckerpunch']) rejected = true; break; // 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' || counter['physicalsetup'] > 1) { if (!hasMove['growth'] || hasMove['sunnyday']) rejected = true; } if (counter.Physical + counter['physicalpool'] < 2 && (!hasMove['rest'] || !hasMove['sleeptalk'])) rejected = true; if (moveid === 'bellydrum' && !hasAbility['Unburden'] && !counter['priority']) rejected = true; isSetup = true; break; case 'calmmind': case 'geomancy': case 'nastyplot': case 'quiverdance': case 'tailglow': if (counter.setupType !== 'Special' || counter['specialsetup'] > 1) rejected = true; if (counter.Special + counter['specialpool'] < 2 && (!hasMove['rest'] || !hasMove['sleeptalk'])) rejected = true; if (hasType['Dark'] && hasMove['darkpulse']) { counter.setupType = 'Special'; rejected = false; } isSetup = true; break; case 'clangoroussoul': case 'growth': case 'shellsmash': case 'workup': if (counter.setupType !== 'Mixed' || counter['mixedsetup'] > 1) rejected = true; if (counter.damagingMoves.length + counter['physicalpool'] + counter['specialpool'] < 2) rejected = true; if (moveid === 'growth' && !hasMove['sunnyday']) rejected = true; isSetup = true; break; case 'agility': case 'autotomize': case 'rockpolish': case 'shiftgear': if (counter.damagingMoves.length < 2 || hasMove['rest'] && hasMove['sleeptalk']) rejected = true; if (!counter.setupType) isSetup = true; break; case 'flamecharge': if (counter.damagingMoves.length < 3 && !counter.setupType) rejected = true; if (hasMove['dracometeor'] || hasMove['overheat']) rejected = true; break; // Bad after setup case 'circlethrow': case 'dragontail': if (counter.setupType && ((!hasMove['rest'] && !hasMove['sleeptalk']) || hasMove['stormthrow'])) rejected = true; if (!!counter['speedsetup'] || hasMove['encore'] || hasMove['raindance'] || hasMove['roar'] || hasMove['trickroom'] || hasMove['whirlwind']) rejected = true; // @ts-ignore if ((counter[move.type] > 1 && counter.Status > 1) || (hasAbility['Sheer Force'] && !!counter['sheerforce'])) rejected = true; break; case 'defog': if (counter.setupType || hasMove['spikes'] || hasMove['stealthrock'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; break; case 'fakeout': case 'tailwind': if (counter.setupType || hasMove['substitute'] || hasMove['switcheroo'] || hasMove['trick']) rejected = true; break; case 'foulplay': if (counter.setupType || !!counter['speedsetup'] || counter['Dark'] > 2 || hasMove['clearsmog'] || hasMove['rest'] && hasMove['sleeptalk']) rejected = true; if (counter.damagingMoves.length - 1 === counter['priority']) rejected = true; break; case 'haze': case 'spikes': if (counter.setupType || !!counter['speedsetup'] || hasMove['trickroom']) rejected = true; break; case 'healbell': case 'technoblast': if (counter['speedsetup']) rejected = true; break; case 'healingwish': case 'memento': if (counter.setupType || !!counter['recovery'] || hasMove['substitute']) rejected = true; break; case 'leechseed': case 'roar': case 'whirlwind': if (counter.setupType || !!counter['speedsetup'] || hasMove['dragontail']) rejected = true; break; case 'nightshade': case 'seismictoss': case 'superfang': if (!hasAbility['Parental Bond'] && (counter.damagingMoves.length > 1 || counter.setupType)) rejected = true; break; case 'protect': if (counter.setupType && !hasMove['wish']) rejected = true; if (hasMove['rest'] || hasMove['lightscreen'] && hasMove['reflect']) rejected = true; break; case 'rapidspin': if (counter.setupType || teamDetails.hazardClear) rejected = true; break; case 'stealthrock': if (counter.setupType || !!counter['speedsetup'] || hasMove['rest'] || hasMove['substitute'] || hasMove['trickroom'] || teamDetails.stealthRock) rejected = true; break; case 'stickyweb': if (teamDetails.stickyWeb) rejected = true; break; case 'toxicspikes': if (counter.setupType || teamDetails.toxicSpikes) rejected = true; break; case 'trickroom': if (counter.setupType || !!counter['speedsetup'] || counter.damagingMoves.length < 2) rejected = true; if (hasMove['lightscreen'] || hasMove['reflect']) rejected = true; break; case 'uturn': if (counter.setupType || !!counter['speedsetup'] || hasType['Bug'] && counter.stab < 2 && counter.damagingMoves.length > 2 && !hasAbility['Adaptability'] && !hasAbility['Download']) rejected = true; if ((hasAbility['Speed Boost'] && hasMove['protect']) || (hasAbility['Libero'] && counter.Status > 2)) rejected = true; break; case 'voltswitch': if (counter.setupType || !!counter['speedsetup'] || hasMove['electricterrain'] || hasMove['raindance'] || hasMove['uturn']) rejected = true; break; // Bit redundant to have both // Attacks: case 'bugbite': case 'bugbuzz': case 'infestation': case 'signalbeam': if (hasMove['uturn'] && !counter.setupType && !hasAbility['Tinted Lens']) rejected = true; break; case 'darkestlariat': case 'nightslash': if (hasMove['knockoff']) rejected = true; break; case 'darkpulse': if ((hasMove['crunch'] || hasMove['knockoff'] || hasMove['hyperspacefury']) && counter.setupType !== 'Special') rejected = true; break; case 'suckerpunch': if (counter.damagingMoves.length < 2 || hasMove['glare'] || hasMove['rest'] && hasMove['sleeptalk']) rejected = true; if (counter['Dark'] > 1 && !hasType['Dark']) rejected = true; break; case 'dragonclaw': if (hasMove['dragontail'] || hasMove['outrage']) rejected = true; break; case 'dracometeor': if (hasMove['swordsdance'] || counter.setupType === 'Physical' && counter['Dragon'] > 1) rejected = true; break; case 'dragonpulse': case 'spacialrend': if (hasMove['dracometeor'] || hasMove['outrage'] || hasMove['dragontail'] && !counter.setupType) rejected = true; break; case 'outrage': if (hasMove['dracometeor'] && counter.damagingMoves.length < 3) rejected = true; if (hasMove['clangingscales'] && counter.setupType !== 'Physical') rejected = true; break; case 'thunderbolt': if (hasMove['discharge'] || (hasMove['voltswitch'] && hasMove['wildcharge'])) rejected = true; break; case 'thunderpunch': if (hasAbility['Galvanize'] && !!counter['Normal']) rejected = true; break; case 'moonblast': if (isDoubles && hasMove['dazzlinggleam']) rejected = true; break; case 'aurasphere': case 'focusblast': if ((hasMove['closecombat'] || hasMove['superpower']) && counter.setupType !== 'Special') rejected = true; if (hasMove['rest'] && hasMove['sleeptalk']) rejected = true; break; case 'drainpunch': if (!hasMove['bulkup'] && (hasMove['closecombat'] || hasMove['highjumpkick'])) rejected = true; if ((hasMove['focusblast'] || hasMove['superpower']) && counter.setupType !== 'Physical') rejected = true; break; case 'closecombat': case 'highjumpkick': if ((hasMove['aurasphere'] || hasMove['focusblast'] || movePool.includes('aurasphere') || movePool.includes('focusblast')) && counter.setupType === 'Special') rejected = true; if (hasMove['bulkup'] && hasMove['drainpunch']) rejected = true; break; case 'machpunch': if (hasType['Fighting'] && counter.stab < 2 && !hasAbility['Technician']) rejected = true; break; case 'stormthrow': if (hasMove['circlethrow'] && hasMove['rest'] && hasMove['sleeptalk']) rejected = true; break; case 'superpower': if (counter['Fighting'] > 1 && counter.setupType) rejected = true; if (hasMove['rest'] && hasMove['sleeptalk'] && !hasAbility['Contrary']) rejected = true; if (hasAbility['Contrary']) isSetup = true; break; case 'vacuumwave': if ((hasMove['closecombat'] || hasMove['machpunch']) && counter.setupType !== 'Special') rejected = true; break; case 'fierydance': case 'firefang': case 'firepunch': case 'flamethrower': if (hasMove['blazekick'] || hasMove['heatwave'] || hasMove['overheat']) rejected = true; if ((hasMove['fireblast'] || hasMove['lavaplume']) && counter.setupType !== 'Physical') rejected = true; break; case 'fireblast': case 'magmastorm': if (hasMove['flareblitz'] && counter.setupType !== 'Special') rejected = true; if (hasMove['lavaplume'] && !counter.setupType && !counter['speedsetup']) rejected = true; break; case 'lavaplume': if (hasMove['firepunch'] || hasMove['fireblast'] && (counter.setupType || !!counter['speedsetup'])) rejected = true; break; case 'overheat': if (hasMove['fireblast'] || hasMove['flareblitz'] || hasMove['lavaplume']) rejected = true; break; case 'hurricane': if (hasMove['airslash'] || hasMove['bravebird']) rejected = true; break; case 'hex': if (!hasMove['willowisp']) rejected = true; break; case 'shadowball': if (hasMove['darkpulse'] || hasMove['hex'] && hasMove['willowisp']) rejected = true; break; case 'shadowclaw': if (hasMove['shadowforce'] || hasMove['shadowsneak']) rejected = true; if (hasMove['shadowball'] && counter.setupType !== 'Physical') rejected = true; break; case 'shadowsneak': if (hasType['Ghost'] && template.types.length > 1 && counter.stab < 2) rejected = true; if (hasMove['rest'] && hasMove['sleeptalk']) rejected = true; break; case 'gigadrain': if (hasMove['petaldance'] || hasMove['powerwhip'] || (hasMove['seedbomb'] && !isDoubles)) rejected = true; if (hasMove['leafstorm'] && counter.Special < 4 && !counter.setupType && !hasMove['trickroom']) rejected = true; break; case 'leafblade': case 'woodhammer': if (hasMove['gigadrain'] && counter.setupType !== 'Physical') rejected = true; if (hasMove['hornleech'] && counter.Physical < 4) rejected = true; break; case 'leafstorm': if (hasMove['trickroom'] || counter['Grass'] > 1 && counter.setupType) rejected = true; break; case 'seedbomb': if (isDoubles && hasMove['gigadrain']) rejected = true; break; case 'solarbeam': if ((!hasAbility['Drought'] && !hasMove['sunnyday']) || hasMove['gigadrain'] || hasMove['leafstorm']) rejected = true; break; case 'bonemerang': case 'precipiceblades': if (hasMove['earthquake']) rejected = true; break; case 'earthpower': if (hasMove['earthquake'] && counter.setupType !== 'Special') rejected = true; break; case 'freezedry': if (hasMove['icebeam'] || hasMove['icywind'] || counter.stab < 2) rejected = true; break; case 'bodyslam': case 'return': if (hasMove['doubleedge'] || hasMove['glare'] && hasMove['headbutt']) rejected = true; if (moveid === 'return' && hasMove['bodyslam']) rejected = true; break; case 'endeavor': if (!isLead && !hasAbility['Defeatist']) rejected = true; break; case 'explosion': if (counter.setupType || (hasAbility['Refrigerate'] && hasMove['freezedry']) || hasMove['wish']) rejected = true; break; case 'extremespeed': if (counter.setupType !== 'Physical' && hasMove['vacuumwave']) rejected = true; break; case 'facade': if (hasMove['rest'] && hasMove['sleeptalk']) rejected = true; break; case 'hypervoice': if (hasMove['blizzard'] || hasMove['return']) rejected = true; break; case 'judgment': if (counter.setupType !== 'Special' && counter.stab > 1) rejected = true; break; case 'quickattack': if (hasType['Normal'] && (!counter.stab || counter['Normal'] > 2)) rejected = true; if (hasMove['feint'] || hasType['Rock'] && !!counter.Status) rejected = true; break; case 'weatherball': if (!hasMove['raindance'] && !hasMove['sunnyday']) rejected = true; break; case 'poisonjab': if (hasMove['gunkshot']) rejected = true; break; case 'acidspray': case 'sludgewave': if (hasMove['poisonjab'] || hasMove['sludgebomb']) rejected = true; break; case 'psychic': if (hasMove['psyshock']) rejected = true; break; case 'psychocut': case 'zenheadbutt': if ((hasMove['psychic'] || hasMove['psyshock']) && counter.setupType !== 'Physical') rejected = true; if (hasAbility['Contrary'] && !counter.setupType && !!counter['physicalpool']) rejected = true; break; case 'psyshock': let psychic = movePool.indexOf('psychic'); if (psychic >= 0) this.fastPop(movePool, psychic); break; case 'headsmash': if (hasMove['stoneedge'] || isDoubles && hasMove['rockslide']) rejected = true; break; case 'rockblast': case 'rockslide': if ((hasMove['headsmash'] || hasMove['stoneedge']) && !isDoubles) rejected = true; break; case 'stoneedge': if (isDoubles && hasMove['rockslide']) rejected = true; break; case 'bulletpunch': if (hasType['Steel'] && counter.stab < 2 && !hasAbility['Adaptability'] && !hasAbility['Technician']) rejected = true; break; case 'flashcannon': if ((hasMove['ironhead'] || hasMove['meteormash']) && counter.setupType !== 'Special') rejected = true; break; case 'hydropump': if (hasMove['liquidation'] || hasMove['razorshell'] || hasMove['waterfall'] || (hasMove['rest'] && hasMove['sleeptalk'])) rejected = true; if (hasMove['scald'] && ((counter.Special < 4 && !hasMove['uturn']) || (template.types.length > 1 && counter.stab < 3))) rejected = true; break; case 'surf': if (hasMove['hydropump'] || hasMove['scald']) rejected = true; break; case 'scald': if (hasMove['liquidation'] || hasMove['waterfall'] || hasMove['waterpulse']) rejected = true; break; // Status: case 'electroweb': case 'stunspore': case 'thunderwave': if (counter.setupType || !!counter['speedsetup'] || (hasMove['rest'] && hasMove['sleeptalk'])) rejected = true; if (hasMove['discharge'] || hasMove['spore'] || hasMove['toxic'] || hasMove['trickroom'] || hasMove['yawn']) rejected = true; break; case 'glare': case 'headbutt': if (hasMove['bodyslam'] || !hasMove['glare']) rejected = true; break; case 'toxic': if (hasMove['hypnosis'] || hasMove['sleeppowder'] || hasMove['willowisp'] || hasMove['yawn']) rejected = true; if (counter.setupType || hasMove['flamecharge'] || hasMove['raindance']) rejected = true; break; case 'willowisp': if (hasMove['scald']) rejected = true; break; case 'raindance': if (counter.Physical + counter.Special < 2 || hasMove['rest'] && hasMove['sleeptalk']) rejected = true; if (!hasType['Water'] && !counter['Water']) rejected = true; break; case 'sunnyday': if (counter.Physical + counter.Special < 2 || hasMove['rest'] && hasMove['sleeptalk']) rejected = true; if (!hasAbility['Chlorophyll'] && !hasAbility['Flower Gift'] && !hasMove['solarbeam']) rejected = true; if (rejected && movePool.length > 1) { let solarbeam = movePool.indexOf('solarbeam'); if (solarbeam >= 0) this.fastPop(movePool, solarbeam); if (movePool.length > 1) { let weatherball = movePool.indexOf('weatherball'); if (weatherball >= 0) this.fastPop(movePool, weatherball); } } break; case 'milkdrink': case 'moonlight': case 'painsplit': case 'recover': case 'roost': case 'synthesis': if (hasMove['leechseed'] || hasMove['rest'] || hasMove['wish']) rejected = true; break; case 'safeguard': if (hasMove['destinybond']) rejected = true; break; case 'substitute': if (hasMove['dracometeor'] || hasMove['leafstorm'] && !hasAbility['Contrary']) rejected = true; if (hasMove['rest'] || hasMove['taunt'] || hasMove['uturn'] || hasMove['voltswitch'] || hasMove['whirlwind']) rejected = true; if (movePool.includes('copycat')) rejected = true; break; case 'powersplit': if (hasMove['guardsplit']) rejected = true; break; case 'wideguard': if (hasMove['protect']) rejected = true; break; } // Increased/decreased priority moves are unneeded with moves that boost only speed if (move.priority !== 0 && !!counter['speedsetup']) { rejected = true; } // This move doesn't satisfy our setup requirements: if ((move.category === 'Physical' && counter.setupType === 'Special') || (move.category === 'Special' && counter.setupType === 'Physical')) { // Reject STABs last in case the setup type changes later on // @ts-ignore let stabs = counter[template.types[0]] + (counter[template.types[1]] || 0); if (!SetupException.includes(moveid) && (!hasType[move.type] || stabs > 1 || counter[move.category] < 2)) rejected = true; } // @ts-ignore if (counter.setupType && !isSetup && counter.setupType !== 'Mixed' && move.category !== counter.setupType && counter[counter.setupType] < 2 && (move.category !== 'Status' || !move.flags.heal) && moveid !== 'sleeptalk' && !hasType['Dark'] && !hasMove['darkpulse']) { // Reject Status moves only if there is nothing else to reject // @ts-ignore if (move.category !== 'Status' || counter[counter.setupType] + counter.Status > 3 && counter['physicalsetup'] + counter['specialsetup'] < 2) rejected = true; } // Pokemon should have moves that benefit their Type/Ability/Weather, as well as moves required by its forme // @ts-ignore if (!rejected && (counter['physicalsetup'] + counter['specialsetup'] < 2 && (!counter.setupType || counter.setupType === 'Mixed' || (move.category !== counter.setupType && move.category !== 'Status') || counter[counter.setupType] + counter.Status > 3)) && ((!counter.stab && !hasMove['nightshade'] && !hasMove['seismictoss'] && (template.types.length > 1 || (template.types[0] !== 'Normal' && template.types[0] !== 'Psychic') || !hasMove['icebeam'] || template.baseStats.spa >= template.baseStats.spd)) || (hasType['Bug'] && (movePool.includes('megahorn') || movePool.includes('pinmissile'))) || ((hasType['Dark'] && !counter['Dark']) || hasMove['suckerpunch'] && !hasAbility['Contrary'] && counter.stab < template.types.length) || (hasType['Dragon'] && !counter['Dragon'] && !hasAbility['Aerilate'] && !hasAbility['Pixilate'] && !hasMove['fly'] && !hasMove['rest'] && !hasMove['sleeptalk']) || (hasType['Electric'] && (!counter['Electric'] || (hasMove['voltswitch'] && counter.stab < 2)) && !hasAbility['Galvanize']) || (hasType['Fairy'] && !counter['Fairy'] && !hasAbility['Pixilate'] && (counter.setupType || !counter['Status'])) || (hasType['Fighting'] && !counter['Fighting'] && (template.baseStats.atk >= 110 || hasAbility['Justified'] || hasAbility['Unburden'] || counter.setupType || !counter['Status'])) || (hasType['Fire'] && !counter['Fire'] && !hasAbility['Libero']) || (hasType['Flying'] && !counter['Flying'] && (hasAbility['Gale Wings'] || hasAbility['Serene Grace'] || (hasType['Normal'] && (movePool.includes('beakblast') || movePool.includes('bravebird'))))) || (hasType['Ghost'] && !hasType['Dark'] && !counter['Ghost'] && !hasAbility['Steelworker']) || (hasType['Grass'] && !counter['Grass'] && !hasType['Fairy'] && !hasType['Poison'] && !hasType['Steel']) || (hasType['Ground'] && !counter['Ground'] && !hasMove['rest'] && !hasMove['sleeptalk']) || (hasType['Ice'] && !counter['Ice'] && !hasAbility['Refrigerate']) || (hasType['Normal'] && (movePool.includes('boomburst') || hasAbility['Guts'] && movePool.includes('facade'))) || (hasType['Psychic'] && !!counter['Psychic'] && (movePool.includes('psychicfangs') || !hasType['Flying'] && !hasAbility['Pixilate'] && counter.stab < template.types.length)) || (hasType['Rock'] && !counter['Rock'] && !hasType['Fairy'] && (template.baseStats.atk >= 105 || hasAbility['Rock Head'] || counter.setupType === 'Physical')) || (((hasType['Steel'] && (hasAbility['Technician'] || hasMove['trickroom'])) || hasAbility['Steelworker']) && !counter['Steel']) || (hasType['Water'] && (!counter['Water'] || !counter.stab)) || // @ts-ignore ((hasAbility['Adaptability'] && !counter.setupType && template.types.length > 1 && (!counter[template.types[0]] || !counter[template.types[1]])) || ((hasAbility['Aerilate'] || (hasAbility['Galvanize'] && !counter['Electric']) || hasAbility['Pixilate'] || (hasAbility['Refrigerate'] && !hasMove['blizzard'])) && !counter['Normal']) || (hasAbility['Contrary'] && !counter['contrary'] && template.species !== 'Shuckle') || (hasAbility['Psychic Surge'] && !counter['Psychic']) || (hasAbility['Slow Start'] && movePool.includes('substitute')) || (hasAbility['Stance Change'] && !counter.setupType && movePool.includes('kingsshield')) || (!counter.recovery && !counter.setupType && !hasMove['healingwish'] && (movePool.includes('recover') || movePool.includes('roost') || movePool.includes('softboiled')) && (counter.Status > 1 || (template.nfe && !!counter['Status']))) || (movePool.includes('stickyweb') && !counter.setupType && !teamDetails.stickyWeb) || (template.requiredMove && movePool.includes(toID(template.requiredMove)))))) { // Reject Status or non-STAB if (!isSetup && !move.weather && !move.damage && (move.category !== 'Status' || !move.flags.heal) && moveid !== 'judgment' && moveid !== 'sleeptalk') { if (move.category === 'Status' || !hasType[move.type] || move.selfSwitch || move.basePower && move.basePower < 40 && !move.multihit) rejected = true; } } // Sleep Talk shouldn't be selected without Rest if (moveid === 'rest' && rejected) { let sleeptalk = movePool.indexOf('sleeptalk'); if (sleeptalk >= 0) { if (movePool.length < 2) { rejected = false; } else { this.fastPop(movePool, sleeptalk); } } } // Remove rejected moves from the move list if (rejected && movePool.length) { if (move.category !== 'Status' && !move.damage && !move.flags.charge) rejectedPool.push(moves[k]); moves.splice(k, 1); break; } if (rejected && rejectedPool.length) { moves.splice(k, 1); break; } } } while (moves.length < 4 && (movePool.length || rejectedPool.length)); // Moveset modifications if (hasMove['raindance'] && hasMove['thunderbolt'] && !isDoubles) { moves[moves.indexOf('thunderbolt')] = 'thunder'; } /**@type {[string, string | undefined, string | undefined]} */ // @ts-ignore let abilities = Object.values(baseTemplate.abilities); abilities.sort((a, b) => this.dex.getAbility(b).rating - this.dex.getAbility(a).rating); let ability0 = this.dex.getAbility(abilities[0]); let ability1 = this.dex.getAbility(abilities[1]); let ability2 = this.dex.getAbility(abilities[2]); if (abilities[1]) { if (abilities[2] && ability1.rating <= ability2.rating && this.randomChance(1, 2)) { [ability1, ability2] = [ability2, ability1]; } if (ability0.rating <= ability1.rating && this.randomChance(1, 2)) { [ability0, ability1] = [ability1, ability0]; } else if (ability0.rating - 0.6 <= ability1.rating && this.randomChance(2, 3)) { [ability0, ability1] = [ability1, ability0]; } ability = ability0.name; let rejectAbility; do { rejectAbility = false; if (counterAbilities.includes(ability)) { // Adaptability, Contrary, Hustle, Iron Fist, Skill Link // @ts-ignore rejectAbility = !counter[toID(ability)]; } else if (ateAbilities.includes(ability)) { rejectAbility = !counter['Normal']; } else if (ability === 'Battle Armor' || ability === 'Sturdy') { rejectAbility = !!counter['recoil'] && !counter['recovery']; } else if (ability === 'Flare Boost' || ability === 'Moody') { rejectAbility = true; } else if (ability === 'Chlorophyll' || ability === 'Leaf Guard') { rejectAbility = template.baseStats.spe > 100 || abilities.includes('Harvest') || (!hasMove['sunnyday'] && !teamDetails['sun']); } else if (ability === 'Competitive') { rejectAbility = !counter['Special'] || (hasMove['rest'] && hasMove['sleeptalk']); } else if (ability === 'Compound Eyes' || ability === 'No Guard') { rejectAbility = !counter['inaccurate']; } else if (ability === 'Defiant' || ability === 'Moxie') { rejectAbility = !counter['Physical'] || hasMove['dragontail']; } else if (ability === 'Download' || ability === 'Hyper Cutter') { rejectAbility = template.nfe; } else if (ability === 'Gluttony') { rejectAbility = !hasMove['bellydrum']; } else if (ability === 'Harvest') { rejectAbility = abilities.includes('Frisk'); } else if (ability === 'Hydration' || ability === 'Rain Dish' || ability === 'Swift Swim') { rejectAbility = template.baseStats.spe > 100 || !hasMove['raindance'] && !teamDetails['rain']; } else if (ability === 'Ice Body' || ability === 'Slush Rush' || ability === 'Snow Cloak') { rejectAbility = !teamDetails['hail']; } else if (ability === 'Immunity' || ability === 'Snow Warning') { rejectAbility = hasMove['facade'] || hasMove['hypervoice']; } else if (ability === 'Intimidate') { rejectAbility = hasMove['bodyslam'] || hasMove['rest'] || abilities.includes('Reckless') && counter['recoil'] > 1; } else if (ability === 'Lightning Rod') { rejectAbility = template.types.includes('Ground'); } else if (ability === 'Limber') { rejectAbility = template.types.includes('Electric'); } else if (ability === 'Liquid Voice') { rejectAbility = !counter['sound']; } else if (ability === 'Magic Guard' || ability === 'Speed Boost') { rejectAbility = hasAbility['Tinted Lens'] && (!counter['Status'] || hasMove['uturn']); } else if (ability === 'Magnet Pull') { rejectAbility = !!counter['Normal'] || !hasType['Electric'] && !hasMove['earthpower']; } else if (ability === 'Mold Breaker') { rejectAbility = hasMove['acrobatics'] || abilities.includes('Adaptability') || abilities.includes('Sheer Force') && !!counter['sheerforce']; } else if (ability === 'Overgrow') { rejectAbility = !counter['Grass']; } else if (ability === 'Poison Heal') { rejectAbility = abilities.includes('Technician') && !!counter['technician']; } else if (ability === 'Power Construct') { rejectAbility = template.forme === '10%' && !hasMove['substitute']; } else if (ability === 'Prankster') { rejectAbility = !counter['Status']; } else if (ability === 'Pressure' || ability === 'Synchronize') { rejectAbility = counter.Status < 2 || !!counter['recoil']; } else if (ability === 'Regenerator') { rejectAbility = abilities.includes('Magic Guard'); } else if (ability === 'Quick Feet') { rejectAbility = hasMove['bellydrum']; } else if (ability === 'Reckless' || ability === 'Rock Head') { rejectAbility = !counter['recoil']; } else if (ability === 'Sand Force' || ability === 'Sand Rush' || ability === 'Sand Veil') { rejectAbility = !teamDetails['sand']; } else if (ability === 'Scrappy') { rejectAbility = !template.types.includes('Normal'); } else if (ability === 'Serene Grace') { rejectAbility = !counter['serenegrace'] || template.species === 'Blissey'; } else if (ability === 'Sheer Force') { rejectAbility = !counter['sheerforce'] || abilities.includes('Guts') || hasMove['doubleedge']; } else if (ability === 'Simple') { rejectAbility = !counter.setupType && !hasMove['flamecharge']; } else if (ability === 'Solar Power') { rejectAbility = !counter['Special'] || !teamDetails['sun']; } else if (ability === 'Strong Jaw') { rejectAbility = !counter['bite']; } else if (ability === 'Swarm') { rejectAbility = !counter['Bug']; } else if (ability === 'Sweet Veil') { rejectAbility = hasType['Grass']; } else if (ability === 'Technician') { rejectAbility = !counter['technician'] || hasMove['tailslap']; } else if (ability === 'Tinted Lens') { rejectAbility = (abilities.includes('Magic Guard') && !!counter['Status']) || abilities.includes('Prankster') || hasMove['protect'] || counter['damage'] >= counter.damagingMoves.length || (counter.Status > 2 && !counter.setupType); } else if (ability === 'Torrent') { rejectAbility = !counter['Water']; } else if (ability === 'Unaware') { rejectAbility = hasMove['stealthrock']; } else if (ability === 'Unburden') { rejectAbility = abilities.includes('Prankster') || (!counter.setupType && !hasMove['acrobatics']); } else if (ability === 'Water Absorb') { rejectAbility = abilities.includes('Volt Absorb') || hasMove['raindance']; } else if (ability === 'Weak Armor') { rejectAbility = counter.setupType !== 'Physical'; } if (rejectAbility) { if (ability === ability0.name && ability1.rating >= 1) { ability = ability1.name; } else if (ability === ability1.name && abilities[2] && ability2.rating >= 1) { ability = ability2.name; } else { // Default to the highest rated ability if all are rejected ability = abilities[0]; rejectAbility = false; } } } while (rejectAbility); if (abilities.includes('Guts') && ability !== 'Quick Feet' && (hasMove['facade'] || (hasMove['protect'] && !isDoubles) || (hasMove['rest'] && hasMove['sleeptalk']))) { ability = 'Guts'; } else if (isDoubles && abilities.includes('Intimidate')) { ability = 'Intimidate'; } } else { ability = ability0.name; } item = !isDoubles ? 'Leftovers' : 'Sitrus Berry'; if (template.requiredItems) { item = this.sample(template.requiredItems); // First, the extra high-priority items } else if (template.species === 'Farfetch\'d') { item = 'Stick'; } else if (template.baseSpecies === 'Pikachu') { item = 'Light Ball'; } else if (template.species === 'Shedinja') { item = 'Focus Sash'; } else if (template.species === 'Unfezant' && counter['Physical'] >= 2) { item = 'Scope Lens'; } else if (template.species === 'Wobbuffet') { if (hasMove['destinybond']) { item = 'Custap Berry'; } else { item = isDoubles || this.randomChance(1, 2) ? 'Sitrus Berry' : 'Leftovers'; } } else if (ability === 'Emergency Exit' && !!counter['Status']) { item = 'Sitrus Berry'; } else if (ability === 'Imposter') { item = 'Choice Scarf'; } else if (hasMove['switcheroo'] || hasMove['trick']) { if (template.baseStats.spe >= 60 && template.baseStats.spe <= 108) { item = 'Choice Scarf'; } else { item = (counter.Physical > counter.Special) ? 'Choice Band' : 'Choice Specs'; } } else if (template.evos.length) { item = 'Eviolite'; } else if (hasMove['bellydrum']) { if (ability === 'Gluttony') { item = this.sample(['Aguav', 'Figy', 'Iapapa', 'Mago', 'Wiki']) + ' Berry'; } else { item = 'Sitrus Berry'; } } else if (hasMove['copycat'] && counter.Physical >= 3) { item = 'Choice Band'; } else if (hasMove['shellsmash']) { item = (ability === 'Solid Rock' && !!counter['priority']) ? 'Weakness Policy' : 'White Herb'; } else if ((ability === 'Guts' || hasMove['facade']) && !hasMove['sleeptalk']) { item = (hasType['Fire'] || ability === 'Quick Feet' || ability === 'Toxic Boost') ? 'Toxic Orb' : 'Flame Orb'; } else if ((ability === 'Magic Guard' && counter.damagingMoves.length > 1) || (ability === 'Sheer Force' && !!counter['sheerforce'])) { item = 'Life Orb'; } else if (ability === 'Poison Heal') { item = 'Toxic Orb'; } else if (ability === 'Unburden') { item = hasMove['fakeout'] ? 'Normal Gem' : 'Sitrus Berry'; } else if (hasMove['acrobatics']) { item = ''; } else if (hasMove['solarbeam'] && ability !== 'Drought' && !hasMove['sunnyday'] && !teamDetails['sun']) { item = 'Power Herb'; } else if (hasMove['auroraveil'] || hasMove['lightscreen'] && hasMove['reflect']) { item = 'Light Clay'; } else if (hasMove['rest'] && !hasMove['sleeptalk'] && ability !== 'Natural Cure' && ability !== 'Shed Skin' && ability !== 'Shadow Tag') { item = 'Chesto Berry'; // Medium priority } else if (hasMove['raindance'] || hasMove['sunnyday'] || ability === 'Stance Change' && counter.Physical + counter.Special > 2) { item = 'Life Orb'; } else if (counter.Physical >= 4 && !hasMove['bodyslam'] && !hasMove['dragontail'] && !hasMove['fakeout'] && !hasMove['flamecharge'] && !hasMove['rapidspin'] && !hasMove['suckerpunch'] && !isDoubles) { item = (template.baseStats.atk >= 100 || ability === 'Huge Power') && template.baseStats.spe >= 60 && template.baseStats.spe <= 108 && !counter['priority'] && this.randomChance(2, 3) ? 'Choice Scarf' : 'Choice Band'; } else if (counter.Special >= 4 && !hasMove['acidspray'] && !hasMove['clearsmog'] && !isDoubles) { item = template.baseStats.spa >= 100 && template.baseStats.spe >= 60 && template.baseStats.spe <= 108 && ability !== 'Tinted Lens' && !counter['priority'] && this.randomChance(2, 3) ? 'Choice Scarf' : 'Choice Specs'; } else if (counter.Physical >= 3 && hasMove['defog'] && template.baseStats.spe >= 60 && template.baseStats.spe <= 108 && !counter['priority'] && !hasMove['foulplay'] && !isDoubles) { item = 'Choice Scarf'; } else if (counter.Special >= 3 && hasMove['uturn'] && !hasMove['acidspray'] && !isDoubles) { item = 'Choice Specs'; } else if ((ability === 'Drizzle' || hasMove['bite'] || hasMove['clearsmog'] || hasMove['curse'] || hasMove['protect'] || hasMove['sleeptalk'] || template.species.includes('Rotom-')) && !isDoubles) { item = 'Leftovers'; } else if ((hasMove['endeavor'] || hasMove['flail'] || hasMove['reversal']) && ability !== 'Sturdy') { item = 'Focus Sash'; } else if (hasMove['outrage'] && counter.setupType) { item = 'Lum Berry'; } else if (isDoubles && counter.damagingMoves.length >= 4 && template.baseStats.spe >= 60 && !hasMove['fakeout'] && !hasMove['flamecharge'] && !hasMove['suckerpunch'] && ability !== 'Multiscale' && ability !== 'Sturdy') { item = 'Life Orb'; } else if (isDoubles && this.dex.getEffectiveness('Ice', template) >= 2) { item = 'Yache Berry'; } else if (isDoubles && this.dex.getEffectiveness('Rock', template) >= 2) { item = 'Charti Berry'; } else if (isDoubles && this.dex.getEffectiveness('Fire', template) >= 2) { item = 'Occa Berry'; } else if (isDoubles && this.dex.getImmunity('Fighting', template) && this.dex.getEffectiveness('Fighting', template) >= 2) { item = 'Chople Berry'; } else if (hasMove['substitute']) { item = counter.damagingMoves.length > 2 && !!counter['drain'] ? 'Life Orb' : 'Leftovers'; } else if (this.dex.getEffectiveness('Ground', template) >= 2 && ability !== 'Levitate') { item = 'Air Balloon'; } else if ((ability === 'Iron Barbs' || ability === 'Rough Skin') && this.randomChance(1, 2)) { item = 'Rocky Helmet'; } else if (counter.Physical + counter.Special >= 4 && template.baseStats.spd >= 50 && template.baseStats.hp + template.baseStats.def + template.baseStats.spd >= 235) { item = 'Assault Vest'; } else if (counter.damagingMoves.length >= 4) { item = (!!counter['Dragon'] || !!counter['Dark'] || !!counter['Normal']) ? 'Life Orb' : 'Expert Belt'; } else if (counter.damagingMoves.length >= 3 && !!counter['speedsetup'] && template.baseStats.hp + template.baseStats.def + template.baseStats.spd >= 300) { item = 'Weakness Policy'; } else if (isLead && ability !== 'Regenerator' && ability !== 'Sturdy' && !counter['recoil'] && !counter['recovery'] && template.baseStats.hp + template.baseStats.def + template.baseStats.spd <= 275) { item = 'Focus Sash'; // This is the "REALLY can't think of a good item" cutoff } else if (hasMove['stickyweb'] && ability === 'Sturdy') { item = 'Mental Herb'; } else if (ability === 'Serene Grace' && hasMove['airslash'] && template.baseStats.spe > 100) { item = 'Metronome'; } else if (ability === 'Sturdy' && hasMove['explosion'] && !counter['speedsetup']) { item = 'Custap Berry'; } else if (ability === 'Super Luck') { item = 'Scope Lens'; } else if (counter.damagingMoves.length >= 3 && ability !== 'Sturdy' && !hasMove['acidspray'] && !hasMove['dragontail'] && !hasMove['foulplay'] && !hasMove['rapidspin'] && !hasMove['superfang'] && !hasMove['uturn']) { if (!!counter['speedsetup'] || hasMove['trickroom'] || template.baseStats.spe > 40 && template.baseStats.hp + template.baseStats.def + template.baseStats.spd <= 275) item = 'Life Orb'; } // For Trick / Switcheroo if (item === 'Leftovers' && hasType['Poison']) { item = 'Black Sludge'; } let level; if (!isDoubles) { /** @type {{[tier: string]: number}} */ let levelScale = { PU: 88, PUBL: 87, NU: 86, NUBL: 85, RU: 84, RUBL: 83, UU: 82, UUBL: 81, OU: 80, New: 80, Unreleased: 80, Uber: 78, }; /** @type {{[species: string]: number}} */ let customScale = { // Banned Ability Dugtrio: 82, Gothitelle: 82, Pelipper: 84, Wobbuffet: 82, // Holistic judgement Delibird: 100, }; level = levelScale[template.tier] || 90; if (customScale[species]) level = customScale[species]; } else { // We choose level based on BST. Min level is 70, max level is 99. 600+ BST is 70, less than 300 is 99. Calculate with those values. // Every 10.34 BST adds a level from 70 up to 99. Results are floored. Uses the Mega's stats if holding a Mega Stone let baseStats = template.baseStats; // If Wishiwashi, use the school-forme's much higher stats if (template.baseSpecies === 'Wishiwashi') baseStats = this.dex.getTemplate('wishiwashischool').baseStats; let bst = baseStats.hp + baseStats.atk + baseStats.def + baseStats.spa + baseStats.spd + baseStats.spe; // Adjust levels of mons based on abilities (Pure Power, Sheer Force, etc.) and also Eviolite // For the stat boosted, treat the Pokemon's base stat as if it were multiplied by the boost. (Actual effective base stats are higher.) let templateAbility = (baseTemplate === template ? ability : template.abilities[0]); if (templateAbility === 'Huge Power' || templateAbility === 'Pure Power') { bst += baseStats.atk; } if (item === 'Eviolite') { bst += 0.5 * (baseStats.def + baseStats.spd); } else if (item === 'Light Ball') { bst += baseStats.atk + baseStats.spa; } level = 70 + Math.floor(((600 - this.dex.clampIntRange(bst, 300, 600)) / 10.34)); } // Prepare optimal HP let srWeakness = this.dex.getEffectiveness('Rock', template); while (evs.hp > 1) { let hp = Math.floor(Math.floor(2 * template.baseStats.hp + ivs.hp + Math.floor(evs.hp / 4) + 100) * level / 100 + 10); if (hasMove['substitute'] && hasMove['reversal']) { // Reversal users should be able to use four Substitutes if (hp % 4 > 0) break; } else if (hasMove['substitute'] && (item === 'Petaya Berry' || item === 'Sitrus Berry' || ability === 'Power Construct' && item !== 'Leftovers')) { // Three Substitutes should activate Petaya Berry for Dedenne // Two Substitutes should activate Sitrus Berry or Power Construct if (hp % 4 === 0) break; } else if (hasMove['bellydrum'] && (item === 'Sitrus Berry' || ability === 'Gluttony')) { // Belly Drum should activate Sitrus Berry if (hp % 2 === 0) break; } else { // Maximize number of Stealth Rock switch-ins if (srWeakness <= 0 || hp % (4 / srWeakness) > 0) break; } evs.hp -= 4; } // Minimize confusion damage if (!counter['Physical'] && !hasMove['copycat'] && !hasMove['transform']) { evs.atk = 0; ivs.atk = 0; } if (ability === 'Beast Boost' && counter.Special < 1) { evs.spa = 0; ivs.spa = 0; } if (hasMove['gyroball'] || hasMove['metalburst'] || hasMove['trickroom']) { evs.spe = 0; ivs.spe = 0; } return { name: template.baseSpecies, species: species, gender: template.gender, moves: moves, ability: ability, evs: evs, ivs: ivs, item: item, level: level, shiny: this.randomChance(1, 1024), }; } /** * @param {string} type * @param {RandomTeamsTypes.RandomSet[]} pokemon * @param {boolean=} isMonotype */ getPokemonPool(type, pokemon = [], isMonotype = false) { const allowedNFE = ['Chansey', 'Doublade', 'Gligar', 'Pikachu', 'Porygon2', 'Scyther', 'Type: Null']; const exclude = pokemon.map(p => toID(p.species)); const pokemonPool = []; for (let id in this.dex.data.FormatsData) { let template = this.dex.getTemplate(id); if (exclude.includes(template.id)) continue; if (isMonotype) { let types = template.types; if (template.battleOnly) types = this.dex.getTemplate(template.baseSpecies).types; if (types.indexOf(type) < 0) continue; } if (template.gen <= this.gen && (!template.nfe || allowedNFE.includes(template.species)) && !template.isMega && !template.isPrimal && !template.isNonstandard && template.randomBattleMoves) { pokemonPool.push(id); } } return pokemonPool; } randomTeam() { const seed = this.prng.seed; let pokemon = []; // For Monotype let isMonotype = this.format.id === 'gen7monotyperandombattle'; let typePool = Object.keys(this.dex.data.TypeChart); let type = this.sample(typePool); // PotD stuff let potd; if (global.Config && Config.potd && this.dex.getRuleTable(this.format).has('potd')) { potd = this.dex.getTemplate(Config.potd); } /**@type {{[k: string]: number}} */ let baseFormes = {}; /**@type {{[k: string]: number}} */ let tierCount = {}; /**@type {{[k: string]: number}} */ let typeCount = {}; /**@type {{[k: string]: number}} */ let typeComboCount = {}; /**@type {RandomTeamsTypes.TeamDetails} */ let teamDetails = {}; // We make at most two passes through the potential Pokemon pool when creating a team - if the first pass doesn't // result in a team of six Pokemon we perform a second iteration relaxing as many restrictions as possible. for (const restrict of [true, false]) { if (pokemon.length >= 6) break; const pokemonPool = this.getPokemonPool(type, pokemon, isMonotype); while (pokemonPool.length && pokemon.length < 6) { let template = this.dex.getTemplate(this.sampleNoReplace(pokemonPool)); if (!template.exists) continue; // Limit to one of each species (Species Clause) if (baseFormes[template.baseSpecies]) continue; let tier = template.tier; let types = template.types; let typeCombo = types.slice().sort().join(); // Adjust rate for species with multiple formes switch (template.baseSpecies) { case 'Arceus': case 'Silvally': if (this.randomChance(17, 18)) continue; break; case 'Rotom': if (this.randomChance(5, 6)) continue; break; case 'Deoxys': case 'Gourgeist': case 'Oricorio': if (this.randomChance(3, 4)) continue; break; case 'Castform': case 'Kyurem': case 'Lycanroc': case 'Necrozma': case 'Wormadam': if (this.randomChance(2, 3)) continue; break; case 'Basculin': case 'Cherrim': case 'Floette': case 'Giratina': case 'Hoopa': case 'Landorus': case 'Meloetta': case 'Meowstic': case 'Shaymin': case 'Thundurus': case 'Tornadus': if (this.randomChance(1, 2)) continue; break; case 'Dugtrio': case 'Exeggutor': case 'Golem': case 'Greninja': case 'Marowak': case 'Muk': case 'Ninetales': case 'Persian': case 'Raichu': case 'Sandslash': case 'Zygarde': if (this.gen >= 7 && this.randomChance(1, 2)) continue; break; case 'Darmanitan': case 'Indeedee': case 'Linoone': case 'Rapidash': case 'Stunfisk': case 'Weezing': if (this.gen >= 8 && this.randomChance(1, 2)) continue; break; } if (restrict) { // Limit two Pokemon per tier, three for Monotype if (!tierCount[tier]) { tierCount[tier] = 1; } else { tierCount[tier]++; if (this.gen < 8 && (!isMonotype || tierCount[tier] > 2)) continue; } if (!isMonotype) { // Limit two of any type let skip = false; for (const type of types) { if (typeCount[type] > 1 && this.randomChance(4, 5)) { skip = true; break; } } if (skip) continue; } } // The Pokemon of the Day if (potd && potd.exists && pokemon.length < 1) template = potd; let set = this.randomSet(template, teamDetails, pokemon.length === 5, this.format.gameType !== 'singles'); // Limit 1 of any type combination, 2 in Monotype if (restrict) { if (set.ability === 'Drought' || set.ability === 'Drizzle' || set.ability === 'Sand Stream') { // Drought, Drizzle and Sand Stream don't count towards the type combo limit typeCombo = set.ability; if (typeCombo in typeComboCount) continue; } else { if (typeComboCount[typeCombo] >= (isMonotype ? 2 : 1)) continue; } } let item = this.dex.getItem(set.item); // Limit 1 Z-Move per team if (item.zMove && teamDetails['zMove']) continue; // Zoroark copies the last Pokemon if (set.ability === 'Illusion') { if (pokemon.length < 1) continue; set.level = pokemon[pokemon.length - 1].level; } // Okay, the set passes, add it to our team pokemon.unshift(set); // Don't bother tracking details for the 6th Pokemon if (pokemon.length === 6) break; // Now that our Pokemon has passed all checks, we can increment our counters baseFormes[template.baseSpecies] = 1; tierCount[tier]++; // Increment type counters for (const type of types) { if (type in typeCount) { typeCount[type]++; } else { typeCount[type] = 1; } } if (typeCombo in typeComboCount) { typeComboCount[typeCombo]++; } else { typeComboCount[typeCombo] = 1; } // Team has Mega/weather/hazards if (item.megaStone) teamDetails['megaStone'] = 1; if (item.zMove) teamDetails['zMove'] = 1; if (set.ability === 'Snow Warning' || set.moves.includes('hail')) teamDetails['hail'] = 1; if (set.moves.includes('raindance') || set.ability === 'Drizzle' && !item.onPrimal) teamDetails['rain'] = 1; if (set.ability === 'Sand Stream') teamDetails['sand'] = 1; if (set.moves.includes('sunnyday') || set.ability === 'Drought' && !item.onPrimal) teamDetails['sun'] = 1; if (set.moves.includes('stealthrock')) teamDetails['stealthRock'] = 1; if (set.moves.includes('toxicspikes')) teamDetails['toxicSpikes'] = 1; if (set.moves.includes('stickyweb')) teamDetails['stickyWeb'] = 1; if (set.moves.includes('defog') || set.moves.includes('rapidspin')) teamDetails['hazardClear'] = 1; } } if (pokemon.length < 6) throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`); return pokemon; } } module.exports = RandomTeams;