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