Spaces:
Paused
Paused
| import { RandomTeams, type MoveCounter } from "../gen9/teams"; | |
| /** Pokemon who should never be in the lead slot */ | |
| const NO_LEAD_POKEMON = [ | |
| 'Zacian', 'Zamazenta', | |
| ]; | |
| export class RandomCAPTeams extends RandomTeams { | |
| getCAPAbility( | |
| types: string[], | |
| moves: Set<string>, | |
| abilities: string[], | |
| counter: MoveCounter, | |
| teamDetails: RandomTeamsTypes.TeamDetails, | |
| species: Species, | |
| isLead: boolean, | |
| teraType: string, | |
| role: RandomTeamsTypes.Role, | |
| ): string { | |
| // Hard-code abilities here | |
| if (species.id === 'fidgit') return moves.has('tailwind') ? 'Persistent' : 'Frisk'; | |
| if (species.id === 'tomohawk') return moves.has('haze') ? 'Prankster' : 'Intimidate'; | |
| // Default to regular ability selection | |
| return this.getAbility(types, moves, abilities, counter, teamDetails, species, isLead, false, teraType, role); | |
| } | |
| getCAPPriorityItem( | |
| ability: string, | |
| types: string[], | |
| moves: Set<string>, | |
| counter: MoveCounter, | |
| teamDetails: RandomTeamsTypes.TeamDetails, | |
| species: Species, | |
| isLead: boolean, | |
| teraType: string, | |
| role: RandomTeamsTypes.Role, | |
| ) { | |
| if (ability === 'Mountaineer') return 'Life Orb'; | |
| } | |
| getLevel( | |
| species: Species, | |
| isDoubles: boolean, | |
| ): number { | |
| if (this.adjustLevel) return this.adjustLevel; | |
| return (species.num > 0 ? this.randomSets[species.id]["level"] : this.randomCAPSets[species.id]["level"]) || 80; | |
| } | |
| randomCAPSet( | |
| s: string | Species, | |
| teamDetails: RandomTeamsTypes.TeamDetails = {}, | |
| isLead = false, | |
| isDoubles = false | |
| ): RandomTeamsTypes.RandomSet { | |
| const species = this.dex.species.get(s); | |
| // Generate Non-CAP Pokemon using the regular randomSet() method | |
| if (species.num > 0) return this.randomSet(s, teamDetails, isLead, isDoubles); | |
| const forme = this.getForme(species); | |
| const sets = this.randomCAPSets[species.id]["sets"]; | |
| const possibleSets = []; | |
| const ruleTable = this.dex.formats.getRuleTable(this.format); | |
| for (const set of sets) { | |
| // Prevent Fast Bulky Setup on lead Paradox Pokemon, since it generates Booster Energy. | |
| const abilities = new Set(Object.values(species.abilities)); | |
| if (isLead && (abilities.has('Protosynthesis') || abilities.has('Quark Drive')) && set.role === 'Fast Bulky Setup') { | |
| continue; | |
| } | |
| // Prevent Tera Blast user if the team already has one, or if Terastallizion is prevented. | |
| if ((teamDetails.teraBlast || ruleTable.has('terastalclause')) && 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; | |
| let teraType = this.sampleIfArray(teraTypes); | |
| 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 = set.abilities!; | |
| // Get moves | |
| const moves = this.randomMoveset(types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType!, role); | |
| const counter = this.queryMoves(moves, species, teraType!, abilities); | |
| // Get ability | |
| ability = this.getCAPAbility(types, moves, abilities, counter, teamDetails, species, isLead, teraType!, role); | |
| // Get items | |
| // First, the priority items | |
| item = this.getCAPPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, teraType!, role); | |
| if (item === undefined) { | |
| item = this.getPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, isDoubles, teraType!, role); | |
| } | |
| if (item === undefined) { | |
| item = this.getItem(ability, types, moves, counter, teamDetails, species, isLead, teraType!, role); | |
| } | |
| // Get level | |
| const level = this.getLevel(species, isDoubles); | |
| // Prepare optimal HP | |
| const srImmunity = ability === 'Magic Guard' || item === 'Heavy-Duty Boots'; | |
| let srWeakness = srImmunity ? 0 : this.dex.getEffectiveness('Rock', species); | |
| // Crash damage move users want an odd HP to survive two misses | |
| if (['axekick', 'highjumpkick', 'jumpkick'].some(m => moves.has(m))) srWeakness = 2; | |
| 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 if (moves.has('substitute') && moves.has('endeavor')) { | |
| // Luvdisc should be able to Substitute down to very low HP | |
| if (hp % 4 > 0) break; | |
| } else { | |
| // Maximize number of Stealth Rock switch-ins | |
| if (srWeakness <= 0 || ability === 'Regenerator' || ['Leftovers', 'Life Orb'].includes(item)) break; | |
| if (item !== 'Sitrus Berry' && hp % (4 / srWeakness) > 0) break; | |
| // Minimise number of Stealth Rock switch-ins to activate Sitrus Berry | |
| if (item === 'Sitrus Berry' && hp % (4 / srWeakness) === 0) 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; | |
| // Magearna and doubles Dragonite, though these can work well as a general rule | |
| if (move.id === 'terablast' && ( | |
| species.id === 'porygon2' || moves.has('shiftgear') || species.baseStats.atk > species.baseStats.spa) | |
| ) return false; | |
| return move.category !== 'Physical' || move.id === 'bodypress' || move.id === 'foulplay'; | |
| }); | |
| if (noAttackStatMoves && !moves.has('transform') && this.format.mod !== 'partnersincrime') { | |
| evs.atk = 0; | |
| ivs.atk = 0; | |
| } | |
| if (moves.has('gyroball') || moves.has('trickroom')) { | |
| evs.spe = 0; | |
| ivs.spe = 0; | |
| } | |
| // Enforce Tera Type after all set generation is done to prevent infinite generation | |
| if (this.forceTeraType) teraType = this.forceTeraType; | |
| // shuffle moves to add more randomness to camomons | |
| const shuffledMoves = Array.from(moves); | |
| this.prng.shuffle(shuffledMoves); | |
| return { | |
| name: species.baseSpecies, | |
| species: forme, | |
| gender: species.baseSpecies === 'Greninja' ? 'M' : species.gender, | |
| shiny: this.randomChance(1, 1024), | |
| level, | |
| moves: shuffledMoves, | |
| ability, | |
| evs, | |
| ivs, | |
| item, | |
| teraType, | |
| role, | |
| }; | |
| } | |
| randomCAPSets: { [species: string]: RandomTeamsTypes.RandomSpeciesData } = require('./sets.json'); | |
| randomTeam() { | |
| this.enforceNoDirectCustomBanlistChanges(); | |
| const seed = this.prng.getSeed(); | |
| const ruleTable = this.dex.formats.getRuleTable(this.format); | |
| const pokemon: RandomTeamsTypes.RandomSet[] = []; | |
| // For Monotype | |
| const isMonotype = !!this.forceMonotype || ruleTable.has('sametypeclause'); | |
| const isDoubles = false; | |
| const typePool = this.dex.types.names().filter(name => name !== "Stellar"); | |
| const type = this.forceMonotype || this.sample(typePool); | |
| const baseFormes: { [k: string]: number } = {}; | |
| const typeCount: { [k: string]: number } = {}; | |
| const typeComboCount: { [k: string]: number } = {}; | |
| const typeWeaknesses: { [k: string]: number } = {}; | |
| const typeDoubleWeaknesses: { [k: string]: number } = {}; | |
| const teamDetails: RandomTeamsTypes.TeamDetails = {}; | |
| let numMaxLevelPokemon = 0; | |
| const pokemonList = Object.keys(this.randomSets); | |
| const capPokemonList = Object.keys(this.randomCAPSets); | |
| const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, pokemonList); | |
| const [capPokemonPool, capBaseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, capPokemonList); | |
| let leadsRemaining = 1; | |
| while (baseSpeciesPool.length && pokemon.length < this.maxTeamSize) { | |
| let baseSpecies, species; | |
| // Always generate a CAP Pokemon in slot 2; other slots can randomly generate CAP Pokemon. | |
| if ((pokemon.length === 1 || this.randomChance(1, 5)) && capBaseSpeciesPool.length) { | |
| baseSpecies = this.sampleNoReplace(capBaseSpeciesPool); | |
| species = this.dex.species.get(this.sample(capPokemonPool[baseSpecies])); | |
| } else { | |
| baseSpecies = this.sampleNoReplace(baseSpeciesPool); | |
| species = this.dex.species.get(this.sample(pokemonPool[baseSpecies])); | |
| } | |
| if (!species.exists) continue; | |
| // Limit to one of each species (Species Clause) | |
| if (baseFormes[species.baseSpecies]) continue; | |
| // Treat Ogerpon formes and Terapagos like the Tera Blast user role; reject if team has one already | |
| if ((species.baseSpecies === 'Ogerpon' || species.baseSpecies === 'Terapagos') && teamDetails.teraBlast) continue; | |
| // Illusion shouldn't be on the last slot | |
| if (species.baseSpecies === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue; | |
| const types = species.types; | |
| const typeCombo = types.slice().sort().join(); | |
| const weakToFreezeDry = ( | |
| this.dex.getEffectiveness('Ice', species) > 0 || | |
| (this.dex.getEffectiveness('Ice', species) > -2 && types.includes('Water')) | |
| ); | |
| // Dynamically scale limits for different team sizes. The default and minimum value is 1. | |
| const limitFactor = Math.round(this.maxTeamSize / 6) || 1; | |
| if (!isMonotype && !this.forceMonotype) { | |
| let skip = false; | |
| // Limit two of any type | |
| for (const typeName of types) { | |
| if (typeCount[typeName] >= 2 * limitFactor) { | |
| skip = true; | |
| break; | |
| } | |
| } | |
| if (skip) continue; | |
| // Limit three weak to any type, and one double weak to any type | |
| for (const typeName of this.dex.types.names()) { | |
| // it's weak to the type | |
| if (this.dex.getEffectiveness(typeName, species) > 0) { | |
| if (!typeWeaknesses[typeName]) typeWeaknesses[typeName] = 0; | |
| if (typeWeaknesses[typeName] >= 3 * limitFactor) { | |
| skip = true; | |
| break; | |
| } | |
| } | |
| if (this.dex.getEffectiveness(typeName, species) > 1) { | |
| if (!typeDoubleWeaknesses[typeName]) typeDoubleWeaknesses[typeName] = 0; | |
| if (typeDoubleWeaknesses[typeName] >= limitFactor) { | |
| skip = true; | |
| break; | |
| } | |
| } | |
| } | |
| if (skip) continue; | |
| // Count Dry Skin/Fluffy as Fire weaknesses | |
| if ( | |
| this.dex.getEffectiveness('Fire', species) === 0 && | |
| Object.values(species.abilities).filter(a => ['Dry Skin', 'Fluffy'].includes(a)).length | |
| ) { | |
| if (!typeWeaknesses['Fire']) typeWeaknesses['Fire'] = 0; | |
| if (typeWeaknesses['Fire'] >= 3 * limitFactor) continue; | |
| } | |
| // Limit four weak to Freeze-Dry | |
| if (weakToFreezeDry) { | |
| if (!typeWeaknesses['Freeze-Dry']) typeWeaknesses['Freeze-Dry'] = 0; | |
| if (typeWeaknesses['Freeze-Dry'] >= 4 * limitFactor) continue; | |
| } | |
| // Limit one level 100 Pokemon | |
| if (!this.adjustLevel && (this.getLevel(species, isDoubles) === 100) && numMaxLevelPokemon >= limitFactor) { | |
| continue; | |
| } | |
| } | |
| // Limit three of any type combination in Monotype | |
| if (!this.forceMonotype && isMonotype && (typeComboCount[typeCombo] >= 3 * limitFactor)) continue; | |
| let set: RandomTeamsTypes.RandomSet; | |
| if (leadsRemaining) { | |
| if (NO_LEAD_POKEMON.includes(species.baseSpecies)) { | |
| if (pokemon.length + leadsRemaining === this.maxTeamSize) continue; | |
| set = this.randomCAPSet(species, teamDetails, false, isDoubles); | |
| pokemon.push(set); | |
| } else { | |
| set = this.randomCAPSet(species, teamDetails, true, isDoubles); | |
| pokemon.unshift(set); | |
| leadsRemaining--; | |
| } | |
| } else { | |
| set = this.randomCAPSet(species, teamDetails, false, isDoubles); | |
| pokemon.push(set); | |
| } | |
| // Don't bother tracking details for the last Pokemon | |
| if (pokemon.length === this.maxTeamSize) break; | |
| // Now that our Pokemon has passed all checks, we can increment our counters | |
| baseFormes[species.baseSpecies] = 1; | |
| // Increment type counters | |
| for (const typeName of types) { | |
| if (typeName in typeCount) { | |
| typeCount[typeName]++; | |
| } else { | |
| typeCount[typeName] = 1; | |
| } | |
| } | |
| if (typeCombo in typeComboCount) { | |
| typeComboCount[typeCombo]++; | |
| } else { | |
| typeComboCount[typeCombo] = 1; | |
| } | |
| // Increment weakness counter | |
| for (const typeName of this.dex.types.names()) { | |
| // it's weak to the type | |
| if (this.dex.getEffectiveness(typeName, species) > 0) { | |
| typeWeaknesses[typeName]++; | |
| } | |
| if (this.dex.getEffectiveness(typeName, species) > 1) { | |
| typeDoubleWeaknesses[typeName]++; | |
| } | |
| } | |
| // Count Dry Skin/Fluffy as Fire weaknesses | |
| if (['Dry Skin', 'Fluffy'].includes(set.ability) && this.dex.getEffectiveness('Fire', species) === 0) { | |
| typeWeaknesses['Fire']++; | |
| } | |
| if (weakToFreezeDry) typeWeaknesses['Freeze-Dry']++; | |
| // Increment level 100 counter | |
| if (set.level === 100) numMaxLevelPokemon++; | |
| // Track what the team has | |
| if (set.ability === 'Drizzle' || set.moves.includes('raindance')) teamDetails.rain = 1; | |
| if (set.ability === 'Drought' || set.ability === 'Orichalcum Pulse' || set.moves.includes('sunnyday')) { | |
| teamDetails.sun = 1; | |
| } | |
| if (set.ability === 'Sand Stream') teamDetails.sand = 1; | |
| if (set.ability === 'Snow Warning' || set.moves.includes('snowscape') || set.moves.includes('chillyreception')) { | |
| teamDetails.snow = 1; | |
| } | |
| if (set.moves.includes('healbell')) teamDetails.statusCure = 1; | |
| if (set.moves.includes('spikes') || set.moves.includes('ceaselessedge')) { | |
| teamDetails.spikes = (teamDetails.spikes || 0) + 1; | |
| } | |
| if (set.moves.includes('toxicspikes') || set.ability === 'Toxic Debris') teamDetails.toxicSpikes = 1; | |
| if (set.moves.includes('stealthrock') || set.moves.includes('stoneaxe')) teamDetails.stealthRock = 1; | |
| if (set.moves.includes('stickyweb')) teamDetails.stickyWeb = 1; | |
| if (set.moves.includes('defog')) teamDetails.defog = 1; | |
| if (set.moves.includes('rapidspin') || set.moves.includes('mortalspin')) teamDetails.rapidSpin = 1; | |
| if (set.moves.includes('auroraveil') || (set.moves.includes('reflect') && set.moves.includes('lightscreen'))) { | |
| teamDetails.screens = 1; | |
| } | |
| if (set.role === 'Tera Blast user' || species.baseSpecies === "Ogerpon" || species.baseSpecies === "Terapagos") { | |
| teamDetails.teraBlast = 1; | |
| } | |
| } | |
| if (pokemon.length < this.maxTeamSize && pokemon.length < 12) { // large teams sometimes cannot be built | |
| throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`); | |
| } | |
| return pokemon; | |
| } | |
| } | |
| export default RandomCAPTeams; | |