import {Dex, toID} from '../sim/dex'; import {Utils} from '../lib'; import {PRNG, PRNGSeed} from '../sim/prng'; import {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[]; } interface BattleFactorySet { species: string; item: string; ability: string; nature: string; moves: string[]; evs?: Partial; ivs?: Partial; } export class MoveCounter extends Utils.Multiset { damagingMoves: Set; stabCounter: number; ironFist: number; constructor() { super(); this.damagingMoves = new Set(); this.stabCounter = 0; this.ironFist = 0; } get(key: string): number { return super.get(key) || 0; } } type MoveEnforcementChecker = ( movePool: string[], moves: Set, abilities: Set, types: string[], counter: MoveCounter, species: Species, teamDetails: RandomTeamsTypes.TeamDetails, isLead: boolean, isDoubles: boolean, teraType: string, role: string, ) => boolean; // Moves that restore HP: const RecoveryMove = [ 'healorder', 'milkdrink', 'moonlight', 'morningsun', 'recover', 'roost', 'shoreup', 'slackoff', 'softboiled', 'strengthsap', 'synthesis', ]; // Moves that drop stats: const ContraryMoves = [ 'armorcannon', 'closecombat', 'leafstorm', 'makeitrain', 'overheat', 'spinout', 'superpower', 'vcreate', ]; // Moves that boost Attack: const PhysicalSetup = [ 'bellydrum', 'bulkup', 'coil', 'curse', 'dragondance', 'honeclaws', 'howl', 'meditate', 'poweruppunch', 'swordsdance', 'tidyup', ]; // Moves which boost Special Attack: const SpecialSetup = [ 'calmmind', 'chargebeam', 'geomancy', 'nastyplot', 'quiverdance', 'tailglow', 'torchsong', ]; // Moves that boost Attack AND Special Attack: const MixedSetup = [ 'clangoroussoul', 'growth', 'happyhour', 'holdhands', 'noretreat', 'shellsmash', 'workup', ]; // Some moves that only boost Speed: const SpeedSetup = [ 'agility', 'autotomize', 'rockpolish', ]; // Conglomerate for ease of access const Setup = [ 'acidarmor', 'agility', 'autotomize', 'bellydrum', 'bulkup', 'calmmind', 'coil', 'curse', 'dragondance', 'flamecharge', 'growth', 'honeclaws', 'howl', 'irondefense', 'meditate', 'nastyplot', 'noretreat', 'poweruppunch', 'quiverdance', 'rockpolish', 'shellsmash', 'shiftgear', 'swordsdance', 'tailglow', 'tidyup', 'trailblaze', 'workup', ]; // Moves that shouldn't be the only STAB moves: const NoStab = [ 'accelerock', 'aquajet', 'beakblast', 'bounce', 'breakingswipe', 'chatter', 'chloroblast', 'clearsmog', 'dragontail', 'eruption', 'explosion', 'fakeout', 'flamecharge', 'flipturn', 'iceshard', 'icywind', 'incinerate', 'machpunch', 'meteorbeam', 'mortalspin', 'pluck', 'pursuit', 'quickattack', 'rapidspin', 'reversal', 'saltcure', 'selfdestruct', 'shadowsneak', 'skydrop', 'snarl', 'steelbeam', 'suckerpunch', 'uturn', 'watershuriken', 'vacuumwave', 'voltswitch', 'waterspout', ]; // Hazard-setting moves const Hazards = [ 'spikes', 'stealthrock', 'stickyweb', 'toxicspikes', ]; // Moves that should be paired together when possible const MovePairs = [ ['lightscreen', 'reflect'], ['sleeptalk', 'rest'], ['protect', 'wish'], ]; function sereneGraceBenefits(move: Move) { return move.secondary?.chance && move.secondary.chance >= 20 && move.secondary.chance < 100; } export class RandomTeams { dex: ModdedDex; gen: number; factoryTier: string; format: Format; prng: PRNG; noStab: string[]; readonly maxTeamSize: number; readonly adjustLevel: number | null; readonly maxMoveCount: number; readonly forceMonotype: string | undefined; /** * Checkers for move enforcement based on types or other factors * * returns true to try to force the move type, false otherwise. */ moveEnforcementCheckers: {[k: string]: MoveEnforcementChecker}; 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 = NoStab; 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 && !Array.isArray(prng) ? prng : new PRNG(prng); this.moveEnforcementCheckers = { Bug: (movePool) => movePool.includes('megahorn'), Dark: (movePool, moves, abilities, types, counter) => !counter.get('Dark'), Dragon: (movePool, moves, abilities, types, counter) => ( !counter.get('Dragon') && !movePool.includes('dualwingbeat') ), Electric: (movePool, moves, abilities, types, counter) => !counter.get('Electric'), Fairy: (movePool, moves, abilities, types, counter) => !counter.get('Fairy'), Fighting: (movePool, moves, abilities, types, counter) => !counter.get('Fighting'), Fire: (movePool, moves, abilities, types, counter, species) => !counter.get('Fire'), Flying: (movePool, moves, abilities, types, counter) => !counter.get('Flying'), Ghost: (movePool, moves, abilities, types, counter) => !counter.get('Ghost'), Grass: (movePool, moves, abilities, types, counter, species) => ( !counter.get('Grass') && ( movePool.includes('leafstorm') || species.baseStats.atk >= 100 || types.includes('Electric') || abilities.has('Seed Sower') ) ), Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'), Ice: (movePool, moves, abilities, types, counter) => !counter.get('Ice'), Normal: (movePool, moves, abilities, types, counter) => { if (movePool.includes('boomburst')) return true; return (!counter.get('Normal') && movePool.includes('futuresight')); }, Poison: (movePool, moves, abilities, types, counter) => { if (types.includes('Ground')) return false; return !counter.get('Poison'); }, Psychic: (movePool, moves, abilities, types, counter) => { if (counter.get('Psychic')) return false; if (movePool.includes('calmmind') || movePool.includes('psychicfangs') || movePool.includes('psychocut')) return true; return abilities.has('Psychic Surge') || types.includes('Fire') || types.includes('Electric'); }, 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 && !movePool.includes('makeitrain')) return false; return !counter.get('Steel'); }, Water: (movePool, moves, abilities, types, counter, species) => { if (species.id === 'quagsire') return false; return !counter.get('Water'); }, }; } setSeed(prng?: PRNG | PRNGSeed) { this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); } getTeam(options?: PlayerOptions | null): PokemonSet[] { const generatorName = ( typeof this.format.team === 'string' && this.format.team.startsWith('random') ) ? this.format.team + 'Team' : ''; // @ts-ignore 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.next(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}).`); } queryMoves( moves: Set | null, types: string[], teraType: string, abilities: Set = new Set(), ): 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) { const move = this.dex.moves.get(moveid); let moveType = move.type; if (['judgment', 'revelationdance'].includes(moveid)) moveType = types[0]; if (moveType === 'Normal') { if (abilities.has('Aerilate')) moveType = 'Flying'; if (abilities.has('Galvanize')) moveType = 'Electric'; if (abilities.has('Pixilate')) moveType = 'Fairy'; if (abilities.has('Refrigerate')) moveType = 'Ice'; } if (moveid === 'terablast') moveType = teraType; 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: if (move.basePower > 30 || move.multihit || move.basePowerCallback) { if (!this.noStab.includes(moveid) || abilities.has('Technician') && moveid === 'machpunch') { counter.add(moveType); if (types.includes(moveType)) counter.stabCounter++; if (teraType === moveType) counter.add('stabtera'); } if (move.flags['bite']) counter.add('strongjaw'); if (move.flags['punch']) counter.ironFist++; if (move.flags['sound']) counter.add('sound'); if (move.priority !== 0 || (moveid === 'grassyglide' && abilities.has('Grassy Surge'))) { counter.add('priority'); } counter.damagingMoves.add(move); } // Moves with secondary effects: if (move.secondary || move.hasSheerForce) { 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 (RecoveryMove.includes(moveid)) counter.add('recovery'); if (ContraryMoves.includes(moveid)) counter.add('contrary'); if (PhysicalSetup.includes(moveid)) counter.add('physicalsetup'); if (SpecialSetup.includes(moveid)) counter.add('specialsetup'); if (MixedSetup.includes(moveid)) counter.add('mixedsetup'); if (SpeedSetup.includes(moveid)) counter.add('speedsetup'); if (Setup.includes(moveid)) counter.add('setup'); if (Hazards.includes(moveid)) counter.add('hazards'); } counter.set('Physical', Math.floor(categories['Physical'])); counter.set('Special', Math.floor(categories['Special'])); counter.set('Status', categories['Status']); return counter; } cullMovePool( types: string[], moves: Set, abilities: Set, counter: MoveCounter, movePool: string[], teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, teraType: string, role: string, ): void { if (moves.size + movePool.length <= this.maxMoveCount) return; // If we have two unfilled moves and only one unpaired move, cull the unpaired move. if (moves.size === this.maxMoveCount - 2) { const unpairedMoves = [...movePool]; for (const pair of MovePairs) { if (movePool.includes(pair[0]) && movePool.includes(pair[1])) { this.fastPop(unpairedMoves, unpairedMoves.indexOf(pair[0])); this.fastPop(unpairedMoves, unpairedMoves.indexOf(pair[1])); } } if (unpairedMoves.length === 1) { this.fastPop(movePool, movePool.indexOf(unpairedMoves[0])); } } // These moves are paired, and shouldn't appear if there is not room for them both. if (moves.size === this.maxMoveCount - 1) { for (const pair of MovePairs) { if (movePool.includes(pair[0]) && movePool.includes(pair[1])) { this.fastPop(movePool, movePool.indexOf(pair[0])); this.fastPop(movePool, movePool.indexOf(pair[1])); } } } // Develop additional move lists const pivotingMoves = ['chillyreception', 'flipturn', 'partingshot', 'shedtail', 'teleport', 'uturn', 'voltswitch']; const statusMoves = this.dex.moves.all() .filter(move => move.category === 'Status') .map(move => move.id); const magnezoneMoves = ['bodypress', 'mirrorcoat', 'steelbeam']; // Team-based move culls if (teamDetails.stealthRock) { if (movePool.includes('stealthrock')) this.fastPop(movePool, movePool.indexOf('stealthrock')); } if (moves.size + movePool.length <= this.maxMoveCount) return; if (teamDetails.defog || teamDetails.rapidSpin) { if (movePool.includes('defog')) this.fastPop(movePool, movePool.indexOf('defog')); if (movePool.includes('rapidspin')) this.fastPop(movePool, movePool.indexOf('rapidspin')); } if (teamDetails.stickyWeb) { if (movePool.includes('stickyweb')) this.fastPop(movePool, movePool.indexOf('stickyweb')); } // These moves don't mesh well with other aspects of the set this.incompatibleMoves(moves, movePool, statusMoves, ['healingwish', 'switcheroo', 'trick']); if (species.id !== "scyther" && species.id !== "scizor") { this.incompatibleMoves(moves, movePool, Setup, pivotingMoves); } this.incompatibleMoves(moves, movePool, Setup, Hazards); this.incompatibleMoves(moves, movePool, Setup, ['defog', 'nuzzle', 'toxic', 'waterspout', 'yawn']); this.incompatibleMoves(moves, movePool, PhysicalSetup, PhysicalSetup); this.incompatibleMoves(moves, movePool, SpecialSetup, 'thunderwave'); this.incompatibleMoves(moves, movePool, 'substitute', pivotingMoves); this.incompatibleMoves(moves, movePool, SpeedSetup, ['aquajet', 'rest', 'trickroom']); this.incompatibleMoves(moves, movePool, 'curse', 'rapidspin'); this.incompatibleMoves(moves, movePool, 'dragondance', 'dracometeor'); // These attacks are redundant with each other this.incompatibleMoves(moves, movePool, 'psychic', 'psyshock'); this.incompatibleMoves(moves, movePool, 'surf', 'hydropump'); this.incompatibleMoves(moves, movePool, 'wavecrash', 'liquidation'); this.incompatibleMoves(moves, movePool, ['airslash', 'bravebird', 'hurricane'], ['airslash', 'bravebird', 'hurricane']); this.incompatibleMoves(moves, movePool, 'knockoff', 'foulplay'); this.incompatibleMoves(moves, movePool, 'doubleedge', 'headbutt'); this.incompatibleMoves(moves, movePool, 'fireblast', ['fierydance', 'flamethrower']); this.incompatibleMoves(moves, movePool, 'lavaplume', 'magmastorm'); this.incompatibleMoves(moves, movePool, 'thunderpunch', 'wildcharge'); this.incompatibleMoves(moves, movePool, 'gunkshot', ['direclaw', 'poisonjab']); this.incompatibleMoves(moves, movePool, 'aurasphere', 'focusblast'); this.incompatibleMoves(moves, movePool, 'closecombat', 'drainpunch'); this.incompatibleMoves(moves, movePool, 'bugbite', 'pounce'); this.incompatibleMoves(moves, movePool, 'bittermalice', 'shadowball'); this.incompatibleMoves(moves, movePool, ['dragonpulse', 'spacialrend'], 'dracometeor'); // These status moves are redundant with each other this.incompatibleMoves(moves, movePool, ['taunt', 'strengthsap'], 'encore'); this.incompatibleMoves(moves, movePool, 'toxic', 'willowisp'); this.incompatibleMoves(moves, movePool, ['thunderwave', 'toxic', 'willowisp'], 'toxicspikes'); this.incompatibleMoves(moves, movePool, 'thunderwave', 'yawn'); // This space reserved for assorted hardcodes that otherwise make little sense out of context if (species.id === "dugtrio") { this.incompatibleMoves(moves, movePool, statusMoves, 'memento'); } // Landorus this.incompatibleMoves(moves, movePool, 'nastyplot', 'rockslide'); // Persian and Grafaiai this.incompatibleMoves(moves, movePool, 'switcheroo', ['fakeout', 'superfang']); // Beartic this.incompatibleMoves(moves, movePool, 'snowscape', 'swordsdance'); // Cryogonal if (!teamDetails.defog && !teamDetails.rapidSpin && species.id === 'cryogonal') { this.fastPop(movePool, movePool.indexOf('haze')); } // Magnezone this.incompatibleMoves(moves, movePool, magnezoneMoves, magnezoneMoves); // Amoonguss, though this can work well as a general rule later this.incompatibleMoves(moves, movePool, 'toxic', 'clearsmog'); } // Checks for and removes incompatible moves, starting with the first move in movesA. incompatibleMoves( moves: Set, movePool: string[], movesA: string | string[], movesB: string | string[], ): void { const moveArrayA = (Array.isArray(movesA)) ? movesA : [movesA]; const moveArrayB = (Array.isArray(movesB)) ? movesB : [movesB]; if (moves.size + movePool.length <= this.maxMoveCount) return; for (const moveid1 of moves) { if (moveArrayB.includes(moveid1)) { for (const moveid2 of moveArrayA) { if (moveid1 !== moveid2 && movePool.includes(moveid2)) { this.fastPop(movePool, movePool.indexOf(moveid2)); if (moves.size + movePool.length <= this.maxMoveCount) return; } } } if (moveArrayA.includes(moveid1)) { for (const moveid2 of moveArrayB) { if (moveid1 !== moveid2 && movePool.includes(moveid2)) { this.fastPop(movePool, movePool.indexOf(moveid2)); if (moves.size + movePool.length <= this.maxMoveCount) return; } } } } } // Adds a move to the moveset, returns the MoveCounter addMove( move: string, moves: Set, types: string[], abilities: Set, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, movePool: string[], teraType: string, role: string, ): MoveCounter { moves.add(move); this.fastPop(movePool, movePool.indexOf(move)); const counter = this.queryMoves(moves, species.types, teraType, abilities); this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, teraType, role); return counter; } // Generate random moveset for a given species, role, tera type. randomMoveset( types: string[], abilities: Set, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, movePool: string[], teraType: string, role: string, ): Set { const moves = new Set(); let counter = this.queryMoves(moves, species.types, teraType, abilities); this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, teraType, role); // If there are only four moves, add all moves and return early if (movePool.length <= this.maxMoveCount) { for (const moveid of movePool) { moves.add(moveid); } return moves; } const runEnforcementChecker = (checkerName: string) => { if (!this.moveEnforcementCheckers[checkerName]) return false; return this.moveEnforcementCheckers[checkerName]( movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles, teraType, role ); }; if (role === "Tera Blast user") { counter = this.addMove('terablast', moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } // Add required move (e.g. Relic Song for Meloetta-P) if (species.requiredMove) { const move = this.dex.moves.get(species.requiredMove).id; counter = this.addMove(move, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } // Add other moves you really want to have, e.g. STAB, recovery, setup, depending on role. // Enforce STAB for (const type of types) { // Check if a STAB move of that type should be required if (runEnforcementChecker(type)) { const stabMoves = []; for (const moveid of movePool) { const move = this.dex.moves.get(moveid); let moveType = move.type; if (['judgment', 'revelationdance'].includes(moveid)) moveType = types[0]; if (moveType === 'Normal') { if (abilities.has('Aerilate')) moveType = 'Flying'; if (abilities.has('Galvanize')) moveType = 'Electric'; if (abilities.has('Pixilate')) moveType = 'Fairy'; if (abilities.has('Refrigerate')) moveType = 'Ice'; } if (moveid === 'terablast') moveType = teraType; if (type === moveType && (move.basePower > 30 || move.multihit || move.basePowerCallback) && (!this.noStab.includes(moveid) || abilities.has('Technician') && moveid === 'machpunch')) { stabMoves.push(moveid); } } if (stabMoves.length) { const moveid = this.sample(stabMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } } // If no STAB move was added in the previous step, add a STAB move if (!counter.stabCounter) { const stabMoves = []; for (const moveid of movePool) { const move = this.dex.moves.get(moveid); let moveType = move.type; if (['judgment', 'revelationdance'].includes(moveid)) moveType = types[0]; if (moveType === 'Normal') { if (abilities.has('Aerilate')) moveType = 'Flying'; if (abilities.has('Galvanize')) moveType = 'Electric'; if (abilities.has('Pixilate')) moveType = 'Fairy'; if (abilities.has('Refrigerate')) moveType = 'Ice'; } if (moveid === 'terablast') moveType = teraType; if (!this.noStab.includes(moveid) && (move.basePower > 30 || move.multihit || move.basePowerCallback)) { if (types.includes(moveType)) { stabMoves.push(moveid); } } } if (stabMoves.length) { const moveid = this.sample(stabMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } // Enforce Tera STAB if (!counter.get('stabtera') && role !== "Bulky Support") { const stabMoves = []; for (const moveid of movePool) { const move = this.dex.moves.get(moveid); let moveType = move.type; if (['judgment', 'revelationdance'].includes(moveid)) moveType = types[0]; if (!this.noStab.includes(moveid) && (move.basePower > 30 || move.multihit || move.basePowerCallback)) { if (teraType === moveType) { stabMoves.push(moveid); } } } if (stabMoves.length) { const moveid = this.sample(stabMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } // Enforce Facade if Guts is a possible ability if (movePool.includes('facade') && abilities.has('Guts')) { counter = this.addMove('facade', moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } // Enforce Sticky Web if (movePool.includes('stickyweb')) { counter = this.addMove('stickyweb', moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } // Enforce Revival Blessing if (movePool.includes('revivalblessing')) { counter = this.addMove('revivalblessing', moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } // Enforce Toxic on Grafaiai if (movePool.includes('toxic') && species.id === 'grafaiai') { counter = this.addMove('toxic', moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } // Enforce recovery if (["Bulky Support", "Bulky Attacker", "Bulky Setup"].includes(role)) { const recoveryMoves = movePool.filter(moveid => RecoveryMove.includes(moveid)); if (recoveryMoves.length) { const moveid = this.sample(recoveryMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } // Enforce setup if (role.includes('Setup') || role === 'Tera Blast user') { // First, try to add a non-Speed setup move const nonSpeedSetupMoves = movePool.filter(moveid => Setup.includes(moveid) && !SpeedSetup.includes(moveid)); if (nonSpeedSetupMoves.length) { const moveid = this.sample(nonSpeedSetupMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } else { // No non-Speed setup moves, so add any (Speed) setup move const setupMoves = movePool.filter(moveid => Setup.includes(moveid)); if (setupMoves.length) { const moveid = this.sample(setupMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } } // Enforce coverage move if (!['AV Pivot', 'Fast Support', 'Bulky Support'].includes(role)) { if (counter.damagingMoves.size <= 1) { // Find the type of the current attacking move let currentAttackType: string; for (const moveid of moves) { const move = this.dex.moves.get(moveid); if (move.basePower > 30 || move.multihit || move.basePowerCallback) { let moveType = move.type; if (['judgment', 'revelationdance'].includes(moveid)) moveType = types[0]; if (moveType === 'Normal') { if (abilities.has('Aerilate')) moveType = 'Flying'; if (abilities.has('Galvanize')) moveType = 'Electric'; if (abilities.has('Pixilate')) moveType = 'Fairy'; if (abilities.has('Refrigerate')) moveType = 'Ice'; } if (moveid === 'terablast') moveType = teraType; currentAttackType = move.type; } } // Choose an attacking move that is of different type to the current single attack const coverageMoves = []; for (const moveid of movePool) { const move = this.dex.moves.get(moveid); let moveType = move.type; if (['judgment', 'revelationdance'].includes(moveid)) moveType = types[0]; if (moveType === 'Normal') { if (abilities.has('Aerilate')) moveType = 'Flying'; if (abilities.has('Galvanize')) moveType = 'Electric'; if (abilities.has('Pixilate')) moveType = 'Fairy'; if (abilities.has('Refrigerate')) moveType = 'Ice'; } if (!this.noStab.includes(moveid) && (move.basePower > 30 || move.multihit || move.basePowerCallback)) { if (currentAttackType! !== moveType) coverageMoves.push(moveid); } } if (coverageMoves.length) { const moveid = this.sample(coverageMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } } // Enforce STAB priority if (role === 'Bulky Attacker' || role === 'Bulky Setup' || species.id === 'breloom') { const priorityMoves = []; for (const moveid of movePool) { const move = this.dex.moves.get(moveid); let moveType = move.type; if (moveType === 'Normal') { if (abilities.has('Aerilate')) moveType = 'Flying'; if (abilities.has('Galvanize')) moveType = 'Electric'; if (abilities.has('Pixilate')) moveType = 'Fairy'; if (abilities.has('Refrigerate')) moveType = 'Ice'; } if (types.includes(moveType) && move.priority > 0 && move.category !== 'Status') { priorityMoves.push(moveid); } } if (priorityMoves.length) { const moveid = this.sample(priorityMoves); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } // Add (moves.size < this.maxMoveCount) as a condition if moves is getting larger than 4 moves. // If you want moves to be favored but not required, add something like && this.randomChance(1, 2) to your condition. // Choose remaining moves randomly from movepool and add them to moves list: while (moves.size < this.maxMoveCount && movePool.length) { if (moves.size + movePool.length <= this.maxMoveCount) { for (const moveid of movePool) { moves.add(moveid); } break; } const moveid = this.sample(movePool); counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); for (const pair of MovePairs) { if (moveid === pair[0] && movePool.includes(pair[1])) { counter = this.addMove(pair[1], moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } if (moveid === pair[1] && movePool.includes(pair[0])) { counter = this.addMove(pair[0], moves, types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); } } } return moves; } shouldCullAbility( ability: string, types: string[], moves: Set, abilities: Set, counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, teraType: string, role: string, ): boolean { if ([ 'Armor Tail', 'Battle Bond', 'Early Bird', 'Flare Boost', 'Gluttony', 'Harvest', 'Hydration', 'Ice Body', 'Immunity', 'Own Tempo', 'Pressure', 'Quick Feet', 'Rain Dish', 'Sand Veil', '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 'Chlorophyll': return (!moves.has('sunnyday') && !teamDetails.sun && species.id !== 'lilligant'); case 'Cloud Nine': return (species.id !== 'golduck'); case 'Competitive': return (species.id === 'kilowattrel'); case 'Compound Eyes': case 'No Guard': return !counter.get('inaccurate'); case 'Cursed Body': return abilities.has('Infiltrator'); case 'Defiant': return (!counter.get('Physical') || (abilities.has('Prankster') && (moves.has('thunderwave') || moves.has('taunt')))); case 'Flash Fire': return ( ['Flame Body', 'Intimidate', 'Rock Head', 'Weak Armor'].some(m => abilities.has(m)) && this.dex.getEffectiveness('Fire', species) < 0 ); case 'Guts': return (!moves.has('facade') && !moves.has('sleeptalk')); case 'Hustle': return (counter.get('Physical') < 2); case 'Infiltrator': return (moves.has('rest') && moves.has('sleeptalk')) || (isDoubles && abilities.has('Clear Body')); case 'Insomnia': return (role === 'Wallbreaker'); case 'Intimidate': if (abilities.has('Hustle')) return true; if (abilities.has('Sheer Force') && !!counter.get('sheerforce')) return true; return (abilities.has('Stakeout')); case 'Iron Fist': return !counter.ironFist; case 'Justified': return !counter.get('Physical'); case 'Mold Breaker': return (abilities.has('Sharpness') || abilities.has('Unburden')); case 'Moxie': return (!counter.get('Physical') || moves.has('stealthrock')); case 'Overgrow': return !counter.get('Grass'); case 'Prankster': return !counter.get('Status'); case 'Reckless': return !counter.get('recoil'); case 'Rock Head': return !counter.get('recoil'); case 'Sand Force': case 'Sand Rush': return !teamDetails.sand; case 'Sap Sipper': return species.id === 'wyrdeer'; case 'Seed Sower': return role === 'Bulky Support'; case 'Shed Skin': return species.id === 'seviper'; case 'Sheer Force': if (species.id === 'braviaryhisui' && role === 'Wallbreaker') return true; return (!counter.get('sheerforce') || ['Guts', 'Sharpness', 'Slush Rush'].some(m => abilities.has(m))); case 'Slush Rush': return !teamDetails.snow; case 'Solar Power': return (!teamDetails.sun || !counter.get('Special')); case 'Stakeout': return (counter.damagingMoves.size < 1); case 'Sturdy': return !!counter.get('recoil'); case 'Swarm': return (!counter.get('Bug') || !!counter.get('recovery')); case 'Sweet Veil': return types.includes('Grass'); case 'Swift Swim': return (!moves.has('raindance') && !teamDetails.rain); case 'Synchronize': return (species.id !== 'umbreon' && species.id !== 'rabsca'); case 'Technician': return (!counter.get('technician') || abilities.has('Punk Rock')); case 'Tinted Lens': return (species.id === 'braviaryhisui' && role === 'Fast Support'); case 'Unburden': return (abilities.has('Prankster') || !counter.get('setup')); case 'Volt Absorb': if (abilities.has('Iron Fist') && counter.ironFist >= 2) return true; return (this.dex.getEffectiveness('Electric', species) < -1); case 'Water Absorb': return species.id === 'quagsire'; case 'Weak Armor': return moves.has('shellsmash'); } return false; } getAbility( types: string[], moves: Set, abilities: Set, counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, teraType: string, role: string, ): 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 if (species.id === 'arcaninehisui') return 'Rock Head'; if (species.id === 'staraptor') return 'Reckless'; if (species.id === 'enamorus' && moves.has('calmmind')) return 'Cute Charm'; if (abilities.has('Corrosion') && moves.has('toxic') && !moves.has('earthpower')) return 'Corrosion'; if (abilities.has('Cud Chew') && moves.has('substitute')) return 'Cud Chew'; if (abilities.has('Guts') && (moves.has('facade') || moves.has('sleeptalk'))) return 'Guts'; if (abilities.has('Harvest') && moves.has('substitute')) return 'Harvest'; if (species.id === 'hypno') return 'Insomnia'; if (abilities.has('Pressure') && role === 'Bulky Setup') return 'Pressure'; if (abilities.has('Serene Grace') && moves.has('headbutt')) return 'Serene Grace'; if (abilities.has('Technician') && counter.get('technician')) return 'Technician'; if (abilities.has('Own Tempo') && moves.has('petaldance')) return 'Own Tempo'; if (abilities.has('Slush Rush') && moves.has('snowscape')) return 'Slush Rush'; 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, teamDetails, species, isLead, isDoubles, teraType, role )) { abilityAllowed.push(ability); } } // If all abilities are culled, re-allow all 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; } getPriorityItem( ability: string, types: string[], moves: Set, counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, teraType: string, role: string, ) { if (species.requiredItems) { // Z-Crystals aren't available in Gen 9, so require Plates if (species.baseSpecies === 'Arceus') { return species.requiredItems[0]; } return this.sample(species.requiredItems); } if (role === 'AV Pivot') return 'Assault Vest'; if ( !isLead && role === 'Bulky Setup' && (ability === 'Quark Drive' || ability === 'Protosynthesis') ) { return 'Booster Energy'; } if (species.id === 'pikachu') return 'Light Ball'; if (species.id === 'regieleki') return 'Magnet'; if (species.id === 'pincurchin') return 'Shuca Berry'; if (species.id === 'cyclizar' && role === 'Fast Attacker') return 'Choice Scarf'; if (species.id === 'lokix' && role === 'Wallbreaker') return 'Life Orb'; if (species.id === 'toxtricity' && moves.has('shiftgear')) return 'Throat Spray'; if (ability === 'Imposter' || (species.id === 'magnezone' && moves.has('bodypress'))) return 'Choice Scarf'; if (moves.has('bellydrum') && moves.has('substitute')) return 'Salac Berry'; if ( ['Cheek Pouch', 'Cud Chew', 'Harvest'].some(m => ability === m) || moves.has('bellydrum') || moves.has('filletaway') ) { return 'Sitrus Berry'; } if (['healingwish', 'switcheroo', 'trick'].some(m => moves.has(m))) { if (species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && role !== 'Wallbreaker') { return 'Choice Scarf'; } else { return (counter.get('Physical') > counter.get('Special')) ? 'Choice Band' : 'Choice Specs'; } } if ((ability === 'Guts' || moves.has('facade')) && !moves.has('sleeptalk')) { return (types.includes('Fire') || ability === 'Toxic Boost') ? 'Toxic Orb' : 'Flame Orb'; } if ( (ability === 'Magic Guard' && counter.damagingMoves.size > 1) || (ability === 'Sheer Force' && counter.get('sheerforce')) ) { return 'Life Orb'; } if (moves.has('shellsmash')) return 'White Herb'; if (moves.has('populationbomb')) return 'Wide Lens'; if (moves.has('courtchange')) return 'Heavy-Duty Boots'; if (moves.has('stuffcheeks')) return this.randomChance(1, 2) ? 'Liechi Berry' : 'Salac Berry'; if (ability === 'Unburden') return moves.has('closecombat') ? 'White Herb' : 'Sitrus Berry'; if (moves.has('acrobatics')) return ability === 'Grassy Surge' ? 'Grassy Seed' : ''; if (moves.has('auroraveil') || moves.has('lightscreen') && moves.has('reflect')) return 'Light Clay'; if ( moves.has('rest') && !moves.has('sleeptalk') && ability !== 'Natural Cure' && ability !== 'Shed Skin' ) { return 'Chesto Berry'; } if (species.id === 'scyther') return (isLead && !moves.has('uturn')) ? 'Eviolite' : 'Heavy-Duty Boots'; if (species.nfe) return 'Eviolite'; if (this.dex.getEffectiveness('Rock', species) >= 2) return 'Heavy-Duty Boots'; } /** Item generation specific to Random Doubles */ // This will be changed and used later, once doubles is actually coming out. getDoublesItem( ability: string, types: string[], moves: Set, counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, teraType: string, role: string, ) { 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.snow) return 'Blunder Policy'; if (this.dex.getEffectiveness('Rock', species) >= 2 && !types.includes('Flying')) return 'Heavy-Duty Boots'; if (counter.get('Physical') >= 4 && ['fakeout', 'feint', 'rapidspin', 'suckerpunch'].every(m => !moves.has(m)) && ( types.includes('Dragon') || types.includes('Fighting') || types.includes('Rock') || moves.has('flipturn') || moves.has('uturn') )) { return ( !counter.get('priority') && ability !== 'Speed Boost' && species.baseStats.spe >= 60 && species.baseStats.spe <= 100 && this.randomChance(1, 2) ) ? 'Choice Scarf' : 'Choice Band'; } if ( ( counter.get('Special') >= 4 && (types.includes('Dragon') || types.includes('Fighting') || types.includes('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'; } getItem( ability: string, types: string[], moves: Set, counter: MoveCounter, teamDetails: RandomTeamsTypes.TeamDetails, species: Species, isLead: boolean, isDoubles: boolean, teraType: string, role: string, ): string | undefined { if ( (counter.get('Physical') >= 4 || (counter.get('Physical') >= 3 && moves.has('memento'))) && ['fakeout', 'firstimpression', 'flamecharge', 'rapidspin', 'ruination', 'superfang'].every(m => !moves.has(m)) ) { const scarfReqs = ( role !== 'Wallbreaker' && (species.baseStats.atk >= 100 || ability === 'Huge Power' || ability === 'Pure Power') && species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && ability !== 'Speed Boost' && !counter.get('priority') && !moves.has('aquastep') ); return (scarfReqs && this.randomChance(1, 2)) ? 'Choice Scarf' : 'Choice Band'; } if ( (counter.get('Special') >= 4) || (counter.get('Special') >= 3 && ['flipturn', 'partingshot', 'uturn'].some(m => moves.has(m))) ) { const scarfReqs = ( role !== 'Wallbreaker' && species.baseStats.spa >= 100 && species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && ability !== 'Speed Boost' && ability !== 'Tinted Lens' && !counter.get('Physical') ); return (scarfReqs && this.randomChance(1, 2)) ? 'Choice Scarf' : 'Choice Specs'; } if (!counter.get('Status') && role !== 'Fast Attacker' && role !== 'Wallbreaker') return 'Assault Vest'; if (counter.get('speedsetup') && this.dex.getEffectiveness('Ground', species) < 1) return 'Weakness Policy'; if (species.id === 'urshifurapidstrike') return 'Punching Glove'; if (species.id === 'palkia') return 'Lustrous Orb'; if (moves.has('substitute') || ability === 'Moody') return 'Leftovers'; if (moves.has('stickyweb') && isLead) return 'Focus Sash'; if ( !teamDetails.defog && !teamDetails.rapidSpin && this.dex.getEffectiveness('Rock', species) >= 1 ) return 'Heavy-Duty Boots'; if ( role === 'Fast Support' && ['defog', 'rapidspin', 'uturn', 'voltswitch'].some(m => moves.has(m)) && !types.includes('Flying') && ability !== 'Levitate' ) return 'Heavy-Duty Boots'; // Low Priority if (moves.has('outrage')) return 'Lum Berry'; if ( (species.id === 'garchomp' && role === 'Fast Support') || (ability === 'Regenerator' && types.includes('Water') && species.baseStats.def >= 110 && this.randomChance(1, 3)) ) return 'Rocky Helmet'; if ( role === 'Fast Support' && isLead && !counter.get('recovery') && !counter.get('recoil') && (species.baseStats.hp + species.baseStats.def + species.baseStats.spd) < 300 ) return 'Focus Sash'; if ( role !== 'Fast Attacker' && role !== 'Tera Blast user' && ability !== 'Levitate' && this.dex.getEffectiveness('Ground', species) >= 2 ) return 'Air Balloon'; if (['Bulky Attacker', 'Bulky Support', 'Bulky Setup'].some(m => role === (m))) return 'Leftovers'; if (role === 'Fast Support' || role === 'Fast Bulky Setup') { return (counter.damagingMoves.size >= 3 && !moves.has('nuzzle')) ? 'Life Orb' : 'Leftovers'; } if ( ['flamecharge', 'rapidspin', 'trailblaze'].every(m => !moves.has(m)) && ['Fast Attacker', 'Setup Sweeper', 'Tera Blast user', 'Wallbreaker'].some(m => role === (m)) ) return 'Life Orb'; if (isDoubles) return 'Sitrus Berry'; return 'Leftovers'; } getLevel( species: Species, isDoubles: boolean, ): number { if (this.adjustLevel) return this.adjustLevel; // doubles levelling if (isDoubles && this.randomDoublesSets[species.id]["level"]) return this.randomDoublesSets[species.id]["level"]; if (!isDoubles && this.randomSets[species.id]["level"]) return this.randomSets[species.id]["level"]; // Default to tier-based levelling const 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, }; return tierScale[tier] || 80; } randomSet( species: string | Species, teamDetails: RandomTeamsTypes.TeamDetails = {}, isLead = false, isDoubles = false ): RandomTeamsTypes.RandomSet { species = this.dex.species.get(species); let forme = species.name; if (typeof species.battleOnly === 'string') { // Only change the forme. The species has custom moves, and may have different typing and requirements. forme = species.battleOnly; } if (species.cosmeticFormes) { forme = this.sample([species.name].concat(species.cosmeticFormes)); } const sets = (this as any)[`random${isDoubles ? 'Doubles' : ''}Sets`][species.id]["sets"]; const possibleSets = []; for (const set of sets) { if (teamDetails.teraBlast && set.role === "Tera Blast user") { continue; } possibleSets.push(set); } const set = this.sampleIfArray(possibleSets); const role = set.role; const movePool: string[] = []; for (const movename of set.movepool) { movePool.push(this.dex.moves.get(movename).id); } const teraTypes = set.teraTypes; const teraType = this.sampleIfArray(teraTypes); if (this.format.gameType === 'multi' || this.format.gameType === 'freeforall') { // 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'; } } } 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 = species.types; const abilities = new Set(Object.values(species.abilities)); if (species.unreleasedHidden) abilities.delete(species.abilities.H); // Get moves const moves = this.randomMoveset(types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); const counter = this.queryMoves(moves, species.types, teraType, abilities); // Get ability ability = this.getAbility(types, moves, abilities, counter, teamDetails, species, isLead, isDoubles, teraType, role); // Get items // First, the priority items item = this.getPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, isDoubles, teraType, role); if (item === undefined && isDoubles) { item = this.getDoublesItem(ability, types, moves, counter, teamDetails, species, teraType, role); } if (item === undefined) { item = this.getItem(ability, types, moves, counter, teamDetails, species, isLead, isDoubles, teraType, role); } // fallback if (item === undefined) item = isDoubles ? 'Sitrus Berry' : 'Leftovers'; if (species.baseSpecies === 'Pikachu') { forme = 'Pikachu' + this.sample(['', '-Original', '-Hoenn', '-Sinnoh', '-Unova', '-Kalos', '-Alola', '-Partner', '-World']); } // Get level const level = this.getLevel(species, isDoubles); // 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); if ((moves.has('substitute') && ['Sitrus Berry', 'Salac Berry'].includes(item))) { // Two Substitutes should activate Sitrus Berry if (hp % 4 === 0) break; } else if ((moves.has('bellydrum') || moves.has('filletaway')) && (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 || ['Leftovers', 'Life Orb'].includes(item)) break; } evs.hp -= 4; } // Minimize confusion damage const noAttackStatMoves = [...moves].every(m => { const move = this.dex.moves.get(m); if (move.damageCallback || move.damage) return true; if (move.id === 'shellsidearm') return false; return move.category !== 'Physical' || move.id === 'bodypress' || move.id === 'foulplay'; }); if (noAttackStatMoves && !moves.has('transform')) { evs.atk = 0; ivs.atk = 0; } 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), level, moves: Array.from(moves), ability, evs, ivs, item, teraType, role, }; } getPokemonPool( type: string, pokemonToExclude: RandomTeamsTypes.RandomSet[] = [], isMonotype = false, isDoubles = false, ) { const exclude = pokemonToExclude.map(p => toID(p.species)); const pokemonPool = []; const baseSpeciesPool: string[] = []; if (isDoubles) { for (const pokemon of Object.keys(this.randomDoublesSets)) { let species = this.dex.species.get(pokemon); if (species.gen > this.gen || 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; } } pokemonPool.push(pokemon); if (!baseSpeciesPool.includes(species.baseSpecies)) baseSpeciesPool.push(species.baseSpecies); } } else { for (const pokemon of Object.keys(this.randomSets)) { let species = this.dex.species.get(pokemon); if (species.gen > this.gen || 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; } } pokemonPool.push(pokemon); if (!baseSpeciesPool.includes(species.baseSpecies)) baseSpeciesPool.push(species.baseSpecies); } } return [pokemonPool, baseSpeciesPool]; } // TODO: Make types for this randomSets: AnyObject = require('./random-sets.json'); randomDoublesSets: AnyObject = require('./random-sets.json'); // Doubles sets are the same as singles for now randomTeam() { this.enforceNoDirectCustomBanlistChanges(); const seed = this.prng.seed; 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 tierCount: {[k: string]: number} = {}; const typeCount: {[k: string]: number} = {}; const typeComboCount: {[k: string]: number} = {}; const typeWeaknesses: {[k: string]: number} = {}; const teamDetails: RandomTeamsTypes.TeamDetails = {}; const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, isDoubles); while (baseSpeciesPool.length && pokemon.length < this.maxTeamSize) { const baseSpecies = this.sampleNoReplace(baseSpeciesPool); const currentSpeciesPool: Species[] = []; for (const poke of pokemonPool) { const species = this.dex.species.get(poke); if (species.baseSpecies === baseSpecies) currentSpeciesPool.push(species); } let species = this.sample(currentSpeciesPool); if (!species.exists) continue; // Illusion shouldn't be on the last slot if (species.baseSpecies === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue; // If Zoroark is in the team, the sixth slot should not be a Pokemon with extremely low level if ( pokemon.some(pkmn => pkmn.name === 'Zoroark') && pokemon.length >= (this.maxTeamSize - 1) && this.getLevel(species, isDoubles) < 72 && !this.adjustLevel ) { continue; } // Pokemon with Last Respects, Intrepid Sword, and Dauntless Shield shouldn't be leading if (['Basculegion', 'Houndstone', 'Zacian', 'Zamazenta'].includes(species.baseSpecies) && !pokemon.length) continue; const tier = species.tier; const types = species.types; const typeCombo = types.slice().sort().join(); // Dynamically scale limits for different team sizes. The default and minimum value is 1. const limitFactor = Math.round(this.maxTeamSize / 6) || 1; // Limit one Pokemon per tier, two for Monotype // Disable this for now, since it is still a new gen // Unless you want to have a lot of Ubers! // if ( // (tierCount[tier] >= (this.forceMonotype || isMonotype ? 2 : 1) * limitFactor) && // !this.randomChance(1, Math.pow(5, tierCount[tier])) // ) { // continue; // } 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 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 (skip) continue; } // Limit one of any type combination, two in Monotype if (!this.forceMonotype && typeComboCount[typeCombo] >= (isMonotype ? 2 : 1) * 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); // Okay, the set passes, add it to our team pokemon.push(set); if (pokemon.length === this.maxTeamSize) { // Set Zoroark's level to be the same as the last Pokemon const illusion = teamDetails.illusion; if (illusion) pokemon[illusion - 1].level = pokemon[this.maxTeamSize - 1].level; // Don't bother tracking details for the last Pokemon break; } // Now that our Pokemon has passed all checks, we can increment our counters baseFormes[species.baseSpecies] = 1; // Increment tier counter if (tierCount[tier]) { tierCount[tier]++; } else { tierCount[tier] = 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]++; } } // 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' || set.moves.includes('snowscape') || set.moves.includes('chillyreception')) { teamDetails.snow = 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('stoneaxe')) teamDetails.stealthRock = 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('mortalspin')) teamDetails.rapidSpin = 1; if (set.moves.includes('tidyup')) teamDetails.rapidSpin = 1; if (set.moves.includes('auroraveil') || (set.moves.includes('reflect') && set.moves.includes('lightscreen'))) { teamDetails.screens = 1; } if (set.role === 'Tera Blast user') teamDetails.teraBlast = 1; // For setting Zoroark's level if (set.ability === 'Illusion') teamDetails.illusion = pokemon.length; } if (pokemon.length < this.maxTeamSize && pokemon.length < 12) { // large teams sometimes cannot be built throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`); } return pokemon; } randomCCTeam(): RandomTeamsTypes.RandomSet[] { this.enforceNoDirectCustomBanlistChanges(); 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 = ''; let isIllegalItem; let isBadItem; if (this.gen >= 2) { do { item = this.sample(items).name; isIllegalItem = this.dex.items.get(item).gen > this.gen || this.dex.items.get(item).isNonstandard; isBadItem = item.startsWith("TR") || this.dex.items.get(item).isPokeball; } while (isIllegalItem || (isBadItem && this.randomChance(19, 20))); } // 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 { const formes = ['gastrodoneast', 'pumpkaboosuper', 'zygarde10']; let learnset = this.dex.species.getLearnset(species.id); let learnsetSpecies = species; if (formes.includes(species.id) || !learnset) { learnsetSpecies = this.dex.species.get(species.baseSpecies); learnset = this.dex.species.getLearnset(learnsetSpecies.id); } if (learnset) { pool = Object.keys(learnset).filter( moveid => learnset![moveid].find(learned => learned.startsWith(String(this.gen))) ); } if (learnset && learnsetSpecies === species && species.changesFrom) { learnset = this.dex.species.getLearnset(toID(species.changesFrom)); for (const moveid in learnset) { if (!pool.includes(moveid) && learnset[moveid].some(source => source.startsWith(String(this.gen)))) { pool.push(moveid); } } } const evoRegion = learnsetSpecies.evoRegion && learnsetSpecies.gen !== this.gen; while (learnsetSpecies.prevo) { learnsetSpecies = this.dex.species.get(learnsetSpecies.prevo); for (const moveid in learnset) { if (!pool.includes(moveid) && learnset[moveid].some(source => source.startsWith(String(this.gen)) && !evoRegion)) { pool.push(moveid); } } } } 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.all()).name; } team.push(set); } return team; } 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 const last = [0, 151, 251, 386, 493, 649, 721, 807, 898, 1010][this.gen]; if (n <= 0 || n > last) throw new Error(`n must be a number between 1 and ${last} (got ${n})`); if (requiredType && !this.dex.types.get(requiredType).exists) { throw new Error(`"${requiredType}" is not a valid type.`); } const isNotCustom = !ruleTable; const pool: number[] = []; let speciesPool: Species[] = []; if (isNotCustom) { speciesPool = [...this.dex.species.all()]; for (const species of speciesPool) { if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue; if (requireMoves) { const hasMovesInCurrentGen = Object.values(this.dex.species.getLearnset(species.id) || {}) .some(sources => sources.some(source => source.startsWith('9'))); 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; if (num > last) break; pool.push(num); } } 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); 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); } } 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 (!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)); } else { const hasAllMovesBan = ruleTable.check('pokemontag:allmoves'); for (const move of this.dex.moves.all()) { 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) { 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; let isBadItem; if (doItemsExist) { // We discard TRs and Balls with 95% probability because of their otherwise overwhelming presence do { itemData = this.sampleNoReplace(itemPool); item = itemData?.name; isBadItem = item.startsWith("TR") || itemData.isPokeball; } while (isBadItem && this.randomChance(19, 20) && itemPool.length > this.maxTeamSize); } // 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.all()).name; } team.push(set); } return team; } } export default RandomTeams;