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'; | |
import { Teams } from '../../../sim/teams'; | |
export interface TeamData { | |
typeCount: { [k: string]: number }; | |
typeComboCount: { [k: string]: number }; | |
baseFormes: { [k: string]: number }; | |
megaCount?: number; | |
zCount?: number; | |
wantsTeraCount?: number; | |
has: { [k: string]: number }; | |
forceResult: boolean; | |
weaknesses: { [k: string]: number }; | |
resistances: { [k: string]: number }; | |
weather?: string; | |
eeveeLimCount?: number; | |
gigantamax?: boolean; | |
} | |
export interface BattleFactorySpecies { | |
sets: BattleFactorySet[]; | |
weight: number; | |
} | |
interface BattleFactorySet { | |
species: string; | |
weight: number; | |
item: string[]; | |
ability: string[]; | |
nature: string[]; | |
moves: string[][]; | |
teraType: string[]; | |
gender?: string; | |
wantsTera?: boolean; | |
evs?: Partial<StatsTable>; | |
ivs?: Partial<StatsTable>; | |
shiny?: boolean; | |
} | |
interface BSSFactorySet { | |
species: string; | |
weight: number; | |
item: string[]; | |
ability: string; | |
nature: string; | |
moves: string[][]; | |
teraType: string[]; | |
gender?: string; | |
wantsTera?: boolean; | |
evs: number[]; | |
ivs?: number[]; | |
} | |
export class MoveCounter extends Utils.Multiset<string> { | |
damagingMoves: Set<Move>; | |
constructor() { | |
super(); | |
this.damagingMoves = new Set(); | |
} | |
} | |
type MoveEnforcementChecker = ( | |
movePool: string[], moves: Set<string>, abilities: string[], types: string[], | |
counter: MoveCounter, species: Species, teamDetails: RandomTeamsTypes.TeamDetails, | |
isLead: boolean, isDoubles: boolean, teraType: string, role: RandomTeamsTypes.Role, | |
) => 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 = [ | |
'armorcannon', 'closecombat', 'leafstorm', 'makeitrain', 'overheat', 'spinout', 'superpower', 'vcreate', | |
]; | |
// Moves that boost Attack: | |
const PHYSICAL_SETUP = [ | |
'bellydrum', 'bulkup', 'coil', 'curse', 'dragondance', 'honeclaws', 'howl', 'meditate', 'poweruppunch', 'swordsdance', 'tidyup', 'victorydance', | |
]; | |
// Moves which boost Special Attack: | |
const SPECIAL_SETUP = [ | |
'calmmind', 'chargebeam', 'geomancy', 'nastyplot', 'quiverdance', 'tailglow', 'takeheart', 'torchsong', | |
]; | |
// 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', 'snowscape', 'trailblaze', | |
]; | |
// Conglomerate for ease of access | |
const SETUP = [ | |
'acidarmor', 'agility', 'autotomize', 'bellydrum', 'bulkup', 'calmmind', 'clangoroussoul', 'coil', 'cosmicpower', 'curse', 'dragondance', | |
'flamecharge', 'growth', 'honeclaws', 'howl', 'irondefense', 'meditate', 'nastyplot', 'noretreat', 'poweruppunch', 'quiverdance', | |
'rockpolish', 'shellsmash', 'shiftgear', 'swordsdance', 'tailglow', 'takeheart', 'tidyup', 'trailblaze', 'workup', 'victorydance', | |
]; | |
const SPEED_CONTROL = [ | |
'electroweb', 'glare', 'icywind', 'lowsweep', 'nuzzle', 'quash', 'tailwind', 'thunderwave', 'trickroom', | |
]; | |
// Moves that shouldn't be the only STAB moves: | |
const NO_STAB = [ | |
'accelerock', 'aquajet', 'bounce', 'breakingswipe', 'bulletpunch', 'chatter', 'chloroblast', 'circlethrow', 'clearsmog', 'covet', | |
'dragontail', 'doomdesire', 'electroweb', 'eruption', 'explosion', 'fakeout', 'feint', 'flamecharge', 'flipturn', 'futuresight', | |
'grassyglide', 'iceshard', 'icywind', 'incinerate', 'infestation', 'machpunch', 'meteorbeam', 'mortalspin', 'nuzzle', 'pluck', 'pursuit', | |
'quickattack', 'rapidspin', 'reversal', 'selfdestruct', 'shadowsneak', 'skydrop', 'snarl', 'strugglebug', 'suckerpunch', 'uturn', | |
'vacuumwave', 'voltswitch', 'watershuriken', 'waterspout', | |
]; | |
// Hazard-setting moves | |
const HAZARDS = [ | |
'spikes', 'stealthrock', 'stickyweb', 'toxicspikes', | |
]; | |
// Protect and its variants | |
const PROTECT_MOVES = [ | |
'banefulbunker', 'burningbulwark', 'protect', 'silktrap', 'spikyshield', | |
]; | |
// Moves that switch the user out | |
const PIVOT_MOVES = [ | |
'chillyreception', 'flipturn', 'partingshot', 'shedtail', 'teleport', 'uturn', 'voltswitch', | |
]; | |
// Moves that should be paired together when possible | |
const MOVE_PAIRS = [ | |
['lightscreen', 'reflect'], | |
['sleeptalk', 'rest'], | |
['protect', 'wish'], | |
['leechseed', 'protect'], | |
['leechseed', 'substitute'], | |
]; | |
/** Pokemon who always want priority STAB, and are fine with it as its only STAB move of that type */ | |
const PRIORITY_POKEMON = [ | |
'breloom', 'brutebonnet', 'cacturne', 'honchkrow', 'mimikyu', 'ragingbolt', 'scizor', | |
]; | |
/** Pokemon who should never be in the lead slot */ | |
const NO_LEAD_POKEMON = [ | |
'Zacian', 'Zamazenta', | |
]; | |
const DOUBLES_NO_LEAD_POKEMON = [ | |
'Basculegion', 'Houndstone', 'Iron Bundle', 'Roaring Moon', 'Zacian', 'Zamazenta', | |
]; | |
const DEFENSIVE_TERA_BLAST_USERS = [ | |
'alcremie', 'bellossom', 'comfey', 'fezandipiti', 'florges', 'raikou', | |
]; | |
function sereneGraceBenefits(move: Move) { | |
return move.secondary?.chance && move.secondary.chance > 20 && move.secondary.chance < 100; | |
} | |
export class RandomTeams { | |
readonly dex: ModdedDex; | |
gen: number; | |
factoryTier: string; | |
format: Format; | |
prng: PRNG; | |
noStab: string[]; | |
readonly maxTeamSize: number; | |
readonly adjustLevel: number | null; | |
readonly maxMoveCount: number; | |
readonly forceMonotype: string | undefined; | |
readonly forceTeraType: string | undefined; | |
/** | |
* Checkers for move enforcement based on types or other factors | |
* | |
* returns true to try to force the move type, false otherwise. | |
*/ | |
moveEnforcementCheckers: { [k: string]: MoveEnforcementChecker }; | |
/** Used by .getPools() */ | |
private poolsCacheKey: [string | undefined, number | undefined, RuleTable | undefined, boolean] | undefined; | |
private cachedPool: number[] | undefined; | |
private cachedSpeciesPool: Species[] | undefined; | |
protected cachedStatusMoves: ID[]; | |
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; | |
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; | |
const forceTeraType = ruleTable.valueRules.get('forceteratype'); | |
this.forceTeraType = forceTeraType && this.dex.types.get(forceTeraType).exists ? | |
this.dex.types.get(forceTeraType).name : undefined; | |
this.factoryTier = ''; | |
this.format = format; | |
this.prng = PRNG.get(prng); | |
this.moveEnforcementCheckers = { | |
Bug: (movePool, moves, abilities, types, counter) => ( | |
movePool.includes('megahorn') || movePool.includes('xscissor') || | |
(!counter.get('Bug') && (types.includes('Electric') || types.includes('Psychic'))) | |
), | |
Dark: ( | |
movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles, teraType, role | |
) => { | |
if ( | |
counter.get('Dark') < 2 && PRIORITY_POKEMON.includes(species.id) && role === 'Wallbreaker' | |
) return true; | |
return !counter.get('Dark'); | |
}, | |
Dragon: (movePool, moves, abilities, types, counter) => !counter.get('Dragon'), | |
Electric: (movePool, moves, abilities, types, counter) => !counter.get('Electric'), | |
Fairy: (movePool, moves, abilities, types, counter) => !counter.get('Fairy'), | |
Fighting: (movePool, moves, abilities, types, counter) => !counter.get('Fighting'), | |
Fire: (movePool, moves, abilities, types, counter, species) => !counter.get('Fire'), | |
Flying: (movePool, moves, abilities, types, counter) => !counter.get('Flying'), | |
Ghost: (movePool, moves, abilities, types, counter) => !counter.get('Ghost'), | |
Grass: (movePool, moves, abilities, types, counter, species) => ( | |
!counter.get('Grass') && ( | |
movePool.includes('leafstorm') || species.baseStats.atk >= 100 || | |
types.includes('Electric') || abilities.includes('Seed Sower') | |
) | |
), | |
Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'), | |
Ice: (movePool, moves, abilities, types, counter) => ( | |
movePool.includes('freezedry') || movePool.includes('blizzard') || !counter.get('Ice') | |
), | |
Normal: (movePool, moves, types, counter) => (movePool.includes('boomburst') || movePool.includes('hypervoice')), | |
Poison: (movePool, moves, abilities, types, counter) => { | |
if (types.includes('Ground')) return false; | |
return !counter.get('Poison'); | |
}, | |
Psychic: (movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles) => { | |
if ((isDoubles || species.id === 'bruxish') && movePool.includes('psychicfangs')) return true; | |
if (species.id === 'hoopaunbound' && movePool.includes('psychic')) return true; | |
if (['Dark', 'Steel', 'Water'].some(m => types.includes(m))) return false; | |
return !counter.get('Psychic'); | |
}, | |
Rock: (movePool, moves, abilities, types, counter, species) => !counter.get('Rock') && species.baseStats.atk >= 80, | |
Steel: (movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles) => ( | |
!counter.get('Steel') && | |
(isDoubles || species.baseStats.atk >= 90 || movePool.includes('gigatonhammer') || movePool.includes('makeitrain')) | |
), | |
Water: (movePool, moves, abilities, types, counter) => (!counter.get('Water') && !types.includes('Ground')), | |
}; | |
this.poolsCacheKey = undefined; | |
this.cachedPool = undefined; | |
this.cachedSpeciesPool = undefined; | |
this.cachedStatusMoves = this.dex.moves.all().filter(move => move.category === 'Status').map(move => move.id); | |
} | |
setSeed(prng?: PRNG | PRNGSeed) { | |
this.prng = PRNG.get(prng); | |
} | |
getTeam(options: PlayerOptions | null = 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.ruleTable?.has('+pokemontag:cap')) return false; | |
if (this.format.banlist.length || this.format.restricted.length || this.format.unbanlist.length) return true; | |
if (!this.format.customRules) return false; | |
for (const rule of this.format.customRules) { | |
for (const banlistOperator of ['-', '+', '*']) { | |
if (rule.startsWith(banlistOperator)) return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Inform user when custom bans are unsupported in a team generator. | |
*/ | |
protected enforceNoDirectCustomBanlistChanges() { | |
if (this.hasDirectCustomBanlistChanges()) { | |
throw new Error(`Custom bans are not currently supported in ${this.format.name}.`); | |
} | |
} | |
/** | |
* Inform user when complex bans are unsupported in a team generator. | |
*/ | |
protected enforceNoDirectComplexBans() { | |
if (!this.format.customRules) return false; | |
for (const rule of this.format.customRules) { | |
if (rule.includes('+') && !rule.startsWith('+')) { | |
throw new Error(`Complex bans are not currently supported in ${this.format.name}.`); | |
} | |
} | |
} | |
/** | |
* Validate set element pool size is sufficient to support size requirements after simple bans. | |
*/ | |
private enforceCustomPoolSizeNoComplexBans( | |
effectTypeName: string, | |
basicEffectPool: BasicEffect[], | |
requiredCount: number, | |
requiredCountExplanation: string | |
) { | |
if (basicEffectPool.length >= requiredCount) return; | |
throw new Error(`Legal ${effectTypeName} count is insufficient to support ${requiredCountExplanation} (${basicEffectPool.length} / ${requiredCount}).`); | |
} | |
queryMoves( | |
moves: Set<string> | null, | |
species: Species, | |
teraType: string, | |
abilities: string[], | |
): MoveCounter { | |
// This is primarily a helper function for random setbuilder functions. | |
const counter = new MoveCounter(); | |
const types = species.types; | |
if (!moves?.size) return counter; | |
const categories = { Physical: 0, Special: 0, Status: 0 }; | |
// Iterate through all moves we've chosen so far and keep track of what they do: | |
for (const moveid of moves) { | |
const move = this.dex.moves.get(moveid); | |
const moveType = this.getMoveType(move, species, abilities, teraType); | |
if (move.damage || move.damageCallback) { | |
// Moves that do a set amount of damage: | |
counter.add('damage'); | |
counter.damagingMoves.add(move); | |
} else { | |
// Are Physical/Special/Status moves: | |
categories[move.category]++; | |
} | |
// Moves that have a low base power: | |
if (moveid === 'lowkick' || (move.basePower && move.basePower <= 60 && moveid !== 'rapidspin')) { | |
counter.add('technician'); | |
} | |
// Moves that hit up to 5 times: | |
if (move.multihit && Array.isArray(move.multihit) && move.multihit[1] === 5) counter.add('skilllink'); | |
if (move.recoil || move.hasCrashDamage) counter.add('recoil'); | |
if (move.drain) counter.add('drain'); | |
// Moves which have a base power: | |
if (move.basePower || move.basePowerCallback) { | |
if (!this.noStab.includes(moveid) || PRIORITY_POKEMON.includes(species.id) && move.priority > 0) { | |
counter.add(moveType); | |
if (types.includes(moveType)) counter.add('stab'); | |
if (teraType === moveType) counter.add('stabtera'); | |
counter.damagingMoves.add(move); | |
} | |
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'); | |
} | |
} | |
// Moves with secondary effects: | |
if (move.secondary || move.hasSheerForce) { | |
counter.add('sheerforce'); | |
if (sereneGraceBenefits(move)) { | |
counter.add('serenegrace'); | |
} | |
} | |
// Moves with low accuracy: | |
if (move.accuracy && move.accuracy !== true && move.accuracy < 90) counter.add('inaccurate'); | |
// Moves that change stats: | |
if (RECOVERY_MOVES.includes(moveid)) counter.add('recovery'); | |
if (CONTRARY_MOVES.includes(moveid)) counter.add('contrary'); | |
if (PHYSICAL_SETUP.includes(moveid)) counter.add('physicalsetup'); | |
if (SPECIAL_SETUP.includes(moveid)) counter.add('specialsetup'); | |
if (MIXED_SETUP.includes(moveid)) counter.add('mixedsetup'); | |
if (SPEED_SETUP.includes(moveid)) counter.add('speedsetup'); | |
if (SETUP.includes(moveid)) counter.add('setup'); | |
if (HAZARDS.includes(moveid)) counter.add('hazards'); | |
} | |
counter.set('Physical', Math.floor(categories['Physical'])); | |
counter.set('Special', Math.floor(categories['Special'])); | |
counter.set('Status', categories['Status']); | |
return counter; | |
} | |
cullMovePool( | |
types: string[], | |
moves: Set<string>, | |
abilities: string[], | |
counter: MoveCounter, | |
movePool: string[], | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
isDoubles: boolean, | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): void { | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
// If we have two unfilled moves and only one unpaired move, cull the unpaired move. | |
if (moves.size === this.maxMoveCount - 2) { | |
const unpairedMoves = [...movePool]; | |
for (const pair of MOVE_PAIRS) { | |
if (movePool.includes(pair[0]) && movePool.includes(pair[1])) { | |
this.fastPop(unpairedMoves, unpairedMoves.indexOf(pair[0])); | |
this.fastPop(unpairedMoves, unpairedMoves.indexOf(pair[1])); | |
} | |
} | |
if (unpairedMoves.length === 1) { | |
this.fastPop(movePool, movePool.indexOf(unpairedMoves[0])); | |
} | |
} | |
// These moves are paired, and shouldn't appear if there is not room for them both. | |
if (moves.size === this.maxMoveCount - 1) { | |
for (const pair of MOVE_PAIRS) { | |
if (movePool.includes(pair[0]) && movePool.includes(pair[1])) { | |
this.fastPop(movePool, movePool.indexOf(pair[0])); | |
this.fastPop(movePool, movePool.indexOf(pair[1])); | |
} | |
} | |
} | |
// Develop additional move lists | |
const statusMoves = this.cachedStatusMoves; | |
// Team-based move culls | |
if (teamDetails.screens) { | |
if (movePool.includes('auroraveil')) this.fastPop(movePool, movePool.indexOf('auroraveil')); | |
if (movePool.length >= this.maxMoveCount + 2) { | |
if (movePool.includes('reflect')) this.fastPop(movePool, movePool.indexOf('reflect')); | |
if (movePool.includes('lightscreen')) this.fastPop(movePool, movePool.indexOf('lightscreen')); | |
} | |
} | |
if (teamDetails.stickyWeb) { | |
if (movePool.includes('stickyweb')) this.fastPop(movePool, movePool.indexOf('stickyweb')); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
if (teamDetails.stealthRock) { | |
if (movePool.includes('stealthrock')) this.fastPop(movePool, movePool.indexOf('stealthrock')); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
if (teamDetails.defog || teamDetails.rapidSpin) { | |
if (movePool.includes('defog')) this.fastPop(movePool, movePool.indexOf('defog')); | |
if (movePool.includes('rapidspin')) this.fastPop(movePool, movePool.indexOf('rapidspin')); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
if (teamDetails.toxicSpikes) { | |
if (movePool.includes('toxicspikes')) this.fastPop(movePool, movePool.indexOf('toxicspikes')); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
if (teamDetails.spikes && teamDetails.spikes >= 2) { | |
if (movePool.includes('spikes')) this.fastPop(movePool, movePool.indexOf('spikes')); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
if (teamDetails.statusCure) { | |
if (movePool.includes('healbell')) this.fastPop(movePool, movePool.indexOf('healbell')); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
if (isDoubles) { | |
const doublesIncompatiblePairs = [ | |
// In order of decreasing generalizability | |
[SPEED_CONTROL, SPEED_CONTROL], | |
[HAZARDS, HAZARDS], | |
['rockslide', 'stoneedge'], | |
[SETUP, ['fakeout', 'helpinghand']], | |
[PROTECT_MOVES, 'wideguard'], | |
[['fierydance', 'fireblast'], 'heatwave'], | |
['dazzlinggleam', ['fleurcannon', 'moonblast']], | |
['poisongas', ['toxicspikes', 'willowisp']], | |
[RECOVERY_MOVES, 'healpulse'], | |
['lifedew', 'healpulse'], | |
['haze', 'icywind'], | |
[['hydropump', 'muddywater'], ['muddywater', 'scald']], | |
['disable', 'encore'], | |
['freezedry', 'icebeam'], | |
['energyball', 'leafstorm'], | |
['wildcharge', 'thunderbolt'], | |
['earthpower', 'sandsearstorm'], | |
['coaching', ['helpinghand', 'howl']], | |
]; | |
for (const pair of doublesIncompatiblePairs) this.incompatibleMoves(moves, movePool, pair[0], pair[1]); | |
if (role !== 'Offensive Protect') this.incompatibleMoves(moves, movePool, PROTECT_MOVES, ['flipturn', 'uturn']); | |
} | |
// General incompatibilities | |
const incompatiblePairs = [ | |
// These moves don't mesh well with other aspects of the set | |
[statusMoves, ['healingwish', 'switcheroo', 'trick']], | |
[SETUP, PIVOT_MOVES], | |
[SETUP, HAZARDS], | |
[SETUP, ['defog', 'nuzzle', 'toxic', 'yawn', 'haze']], | |
[PHYSICAL_SETUP, PHYSICAL_SETUP], | |
[SPECIAL_SETUP, 'thunderwave'], | |
['substitute', PIVOT_MOVES], | |
[SPEED_SETUP, ['aquajet', 'rest', 'trickroom']], | |
['curse', ['irondefense', 'rapidspin']], | |
['dragondance', 'dracometeor'], | |
['yawn', 'roar'], | |
// These attacks are redundant with each other | |
[['psychic', 'psychicnoise'], ['psyshock', 'psychicnoise']], | |
['surf', 'hydropump'], | |
['liquidation', 'wavecrash'], | |
['aquajet', 'flipturn'], | |
['gigadrain', 'leafstorm'], | |
['powerwhip', 'hornleech'], | |
[['airslash', 'bravebird', 'hurricane'], ['airslash', 'bravebird', 'hurricane']], | |
['knockoff', 'foulplay'], | |
['throatchop', ['crunch', 'lashout']], | |
['doubleedge', ['bodyslam', 'headbutt']], | |
['fireblast', ['fierydance', 'flamethrower']], | |
['lavaplume', 'magmastorm'], | |
['thunderpunch', 'wildcharge'], | |
['thunderbolt', 'discharge'], | |
['gunkshot', ['direclaw', 'poisonjab', 'sludgebomb']], | |
['aurasphere', 'focusblast'], | |
['closecombat', 'drainpunch'], | |
['bugbite', 'pounce'], | |
[['dragonpulse', 'spacialrend'], 'dracometeor'], | |
['heavyslam', 'flashcannon'], | |
['alluringvoice', 'dazzlinggleam'], | |
// These status moves are redundant with each other | |
['taunt', 'disable'], | |
[['thunderwave', 'toxic'], ['thunderwave', 'willowisp']], | |
[['thunderwave', 'toxic', 'willowisp'], 'toxicspikes'], | |
// This space reserved for assorted hardcodes that otherwise make little sense out of context | |
// Landorus and Thundurus | |
['nastyplot', ['rockslide', 'knockoff']], | |
// Persian | |
['switcheroo', 'fakeout'], | |
// Amoonguss, though this can work well as a general rule later | |
['toxic', 'clearsmog'], | |
// Chansey and Blissey | |
['healbell', 'stealthrock'], | |
// Azelf and Zoroarks | |
['trick', 'uturn'], | |
// Araquanid | |
['mirrorcoat', 'hydropump'], | |
]; | |
for (const pair of incompatiblePairs) this.incompatibleMoves(moves, movePool, pair[0], pair[1]); | |
if (!types.includes('Ice')) this.incompatibleMoves(moves, movePool, 'icebeam', 'icywind'); | |
if (!isDoubles) this.incompatibleMoves(moves, movePool, 'taunt', 'encore'); | |
if (!types.includes('Dark') && teraType !== 'Dark') this.incompatibleMoves(moves, movePool, 'knockoff', 'suckerpunch'); | |
if (!abilities.includes('Prankster')) this.incompatibleMoves(moves, movePool, 'thunderwave', 'yawn'); | |
// This space reserved for assorted hardcodes that otherwise make little sense out of context | |
if (species.id === 'barraskewda') { | |
this.incompatibleMoves(moves, movePool, ['psychicfangs', 'throatchop'], ['poisonjab', 'throatchop']); | |
} | |
if (species.id === 'cyclizar') this.incompatibleMoves(moves, movePool, 'taunt', 'knockoff'); | |
if (species.id === 'camerupt') this.incompatibleMoves(moves, movePool, 'roar', 'willowisp'); | |
if (species.id === 'coalossal') this.incompatibleMoves(moves, movePool, 'flamethrower', 'overheat'); | |
} | |
// Checks for and removes incompatible moves, starting with the first move in movesA. | |
incompatibleMoves( | |
moves: Set<string>, | |
movePool: string[], | |
movesA: string | string[], | |
movesB: string | string[], | |
): void { | |
const moveArrayA = (Array.isArray(movesA)) ? movesA : [movesA]; | |
const moveArrayB = (Array.isArray(movesB)) ? movesB : [movesB]; | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
for (const moveid1 of moves) { | |
if (moveArrayB.includes(moveid1)) { | |
for (const moveid2 of moveArrayA) { | |
if (moveid1 !== moveid2 && movePool.includes(moveid2)) { | |
this.fastPop(movePool, movePool.indexOf(moveid2)); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
} | |
} | |
if (moveArrayA.includes(moveid1)) { | |
for (const moveid2 of moveArrayB) { | |
if (moveid1 !== moveid2 && movePool.includes(moveid2)) { | |
this.fastPop(movePool, movePool.indexOf(moveid2)); | |
if (moves.size + movePool.length <= this.maxMoveCount) return; | |
} | |
} | |
} | |
} | |
} | |
// Adds a move to the moveset, returns the MoveCounter | |
addMove( | |
move: string, | |
moves: Set<string>, | |
types: string[], | |
abilities: string[], | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
isDoubles: boolean, | |
movePool: string[], | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): MoveCounter { | |
moves.add(move); | |
this.fastPop(movePool, movePool.indexOf(move)); | |
const counter = this.queryMoves(moves, species, teraType, abilities); | |
this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, teraType, role); | |
return counter; | |
} | |
// Returns the type of a given move for STAB/coverage enforcement purposes | |
getMoveType(move: Move, species: Species, abilities: string[], teraType: string): string { | |
if (move.id === 'terablast') return teraType; | |
if (['judgment', 'revelationdance'].includes(move.id)) return species.types[0]; | |
if (move.name === "Raging Bull" && species.name.startsWith("Tauros-Paldea")) { | |
if (species.name.endsWith("Combat")) return "Fighting"; | |
if (species.name.endsWith("Blaze")) return "Fire"; | |
if (species.name.endsWith("Aqua")) return "Water"; | |
} | |
if (move.name === "Ivy Cudgel" && species.name.startsWith("Ogerpon")) { | |
if (species.name.endsWith("Wellspring")) return "Water"; | |
if (species.name.endsWith("Hearthflame")) return "Fire"; | |
if (species.name.endsWith("Cornerstone")) return "Rock"; | |
} | |
const moveType = move.type; | |
if (moveType === 'Normal') { | |
if (abilities.includes('Aerilate')) return 'Flying'; | |
if (abilities.includes('Galvanize')) return 'Electric'; | |
if (abilities.includes('Pixilate')) return 'Fairy'; | |
if (abilities.includes('Refrigerate')) return 'Ice'; | |
} | |
return moveType; | |
} | |
// Generate random moveset for a given species, role, tera type. | |
randomMoveset( | |
types: string[], | |
abilities: string[], | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
isDoubles: boolean, | |
movePool: string[], | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): Set<string> { | |
const moves = new Set<string>(); | |
let counter = this.queryMoves(moves, species, teraType, abilities); | |
this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, teraType, role); | |
// If there are only four moves, add all moves and return early | |
if (movePool.length <= this.maxMoveCount) { | |
for (const moveid of movePool) { | |
moves.add(moveid); | |
} | |
return moves; | |
} | |
const runEnforcementChecker = (checkerName: string) => { | |
if (!this.moveEnforcementCheckers[checkerName]) return false; | |
return this.moveEnforcementCheckers[checkerName]( | |
movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles, teraType, role | |
); | |
}; | |
if (role === 'Tera Blast user') { | |
counter = this.addMove('terablast', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Add required move (e.g. Relic Song for Meloetta-P) | |
if (species.requiredMove) { | |
const move = this.dex.moves.get(species.requiredMove).id; | |
counter = this.addMove(move, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Add other moves you really want to have, e.g. STAB, recovery, setup. | |
// Enforce Facade if Guts is a possible ability | |
if (movePool.includes('facade') && abilities.includes('Guts')) { | |
counter = this.addMove('facade', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Enforce Night Shade, Revelation Dance, Revival Blessing, and Sticky Web | |
for (const moveid of ['nightshade', 'revelationdance', 'revivalblessing', 'stickyweb']) { | |
if (movePool.includes(moveid)) { | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce Trick Room on Doubles Wallbreaker | |
if (movePool.includes('trickroom') && role === 'Doubles Wallbreaker') { | |
counter = this.addMove('trickroom', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Enforce hazard removal on Bulky Support if the team doesn't already have it | |
if (role === 'Bulky Support' && !teamDetails.defog && !teamDetails.rapidSpin) { | |
if (movePool.includes('rapidspin')) { | |
counter = this.addMove('rapidspin', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
if (movePool.includes('defog')) { | |
counter = this.addMove('defog', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce Aurora Veil if the team doesn't already have screens | |
if (!teamDetails.screens && movePool.includes('auroraveil')) { | |
counter = this.addMove('auroraveil', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Enforce Knock Off on pure Normal- and Fighting-types in singles | |
if (!isDoubles && types.length === 1 && (types.includes('Normal') || types.includes('Fighting'))) { | |
if (movePool.includes('knockoff')) { | |
counter = this.addMove('knockoff', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce Spore on Smeargle | |
if (species.id === 'smeargle') { | |
if (movePool.includes('spore')) { | |
counter = this.addMove('spore', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce moves in doubles | |
if (isDoubles) { | |
const doublesEnforcedMoves = ['mortalspin', 'spore']; | |
for (const moveid of doublesEnforcedMoves) { | |
if (movePool.includes(moveid)) { | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce Fake Out on slow Pokemon | |
if (movePool.includes('fakeout') && species.baseStats.spe <= 50) { | |
counter = this.addMove('fakeout', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Enforce Tailwind on Prankster and Gale Wings users | |
if (movePool.includes('tailwind') && (abilities.includes('Prankster') || abilities.includes('Gale Wings'))) { | |
counter = this.addMove('tailwind', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
// Enforce Thunder Wave on Prankster users as well | |
if (movePool.includes('thunderwave') && abilities.includes('Prankster')) { | |
counter = this.addMove('thunderwave', moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce STAB priority | |
if ( | |
['Bulky Attacker', 'Bulky Setup', 'Wallbreaker', 'Doubles Wallbreaker'].includes(role) || | |
PRIORITY_POKEMON.includes(species.id) | |
) { | |
const priorityMoves = []; | |
for (const moveid of movePool) { | |
const move = this.dex.moves.get(moveid); | |
const moveType = this.getMoveType(move, species, abilities, teraType); | |
if ( | |
types.includes(moveType) && (move.priority > 0 || (moveid === 'grassyglide' && abilities.includes('Grassy Surge'))) && | |
(move.basePower || move.basePowerCallback) | |
) { | |
priorityMoves.push(moveid); | |
} | |
} | |
if (priorityMoves.length) { | |
const moveid = this.sample(priorityMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce STAB | |
for (const type of types) { | |
// Check if a STAB move of that type should be required | |
const stabMoves = []; | |
for (const moveid of movePool) { | |
const move = this.dex.moves.get(moveid); | |
const moveType = this.getMoveType(move, species, abilities, teraType); | |
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && type === moveType) { | |
stabMoves.push(moveid); | |
} | |
} | |
while (runEnforcementChecker(type)) { | |
if (!stabMoves.length) break; | |
const moveid = this.sampleNoReplace(stabMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce Tera STAB | |
if (!counter.get('stabtera') && !['Bulky Support', 'Doubles Support'].includes(role)) { | |
const stabMoves = []; | |
for (const moveid of movePool) { | |
const move = this.dex.moves.get(moveid); | |
const moveType = this.getMoveType(move, species, abilities, teraType); | |
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && teraType === moveType) { | |
stabMoves.push(moveid); | |
} | |
} | |
if (stabMoves.length) { | |
const moveid = this.sample(stabMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// If no STAB move was added, add a STAB move | |
if (!counter.get('stab')) { | |
const stabMoves = []; | |
for (const moveid of movePool) { | |
const move = this.dex.moves.get(moveid); | |
const moveType = this.getMoveType(move, species, abilities, teraType); | |
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && types.includes(moveType)) { | |
stabMoves.push(moveid); | |
} | |
} | |
if (stabMoves.length) { | |
const moveid = this.sample(stabMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce recovery | |
if (['Bulky Support', 'Bulky Attacker', 'Bulky Setup'].includes(role)) { | |
const recoveryMoves = movePool.filter(moveid => RECOVERY_MOVES.includes(moveid)); | |
if (recoveryMoves.length) { | |
const moveid = this.sample(recoveryMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce pivoting moves on AV Pivot | |
if (role === 'AV Pivot') { | |
const pivotMoves = movePool.filter(moveid => ['uturn', 'voltswitch'].includes(moveid)); | |
if (pivotMoves.length) { | |
const moveid = this.sample(pivotMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce setup | |
if (role.includes('Setup') || role === 'Tera Blast user') { | |
// First, try to add a non-Speed setup move | |
const nonSpeedSetupMoves = movePool.filter(moveid => SETUP.includes(moveid) && !SPEED_SETUP.includes(moveid)); | |
if (nonSpeedSetupMoves.length) { | |
const moveid = this.sample(nonSpeedSetupMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} else { | |
// No non-Speed setup moves, so add any (Speed) setup move | |
const setupMoves = movePool.filter(moveid => SETUP.includes(moveid)); | |
if (setupMoves.length) { | |
const moveid = this.sample(setupMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
} | |
// Enforce redirecting moves and Fake Out on Doubles Support | |
if (role === 'Doubles Support') { | |
for (const moveid of ['fakeout', 'followme', 'ragepowder']) { | |
if (movePool.includes(moveid)) { | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
const speedControl = movePool.filter(moveid => SPEED_CONTROL.includes(moveid)); | |
if (speedControl.length) { | |
const moveid = this.sample(speedControl); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce Protect | |
if (role.includes('Protect')) { | |
const protectMoves = movePool.filter(moveid => PROTECT_MOVES.includes(moveid)); | |
if (protectMoves.length) { | |
const moveid = this.sample(protectMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce a move not on the noSTAB list | |
if (!counter.damagingMoves.size) { | |
// Choose an attacking move | |
const attackingMoves = []; | |
for (const moveid of movePool) { | |
const move = this.dex.moves.get(moveid); | |
if (!this.noStab.includes(moveid) && (move.category !== 'Status')) attackingMoves.push(moveid); | |
} | |
if (attackingMoves.length) { | |
const moveid = this.sample(attackingMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
// Enforce coverage move | |
if (!['AV Pivot', 'Fast Support', 'Bulky Support', 'Bulky Protect', 'Doubles Support'].includes(role)) { | |
if (counter.damagingMoves.size === 1) { | |
// Find the type of the current attacking move | |
const currentAttackType = counter.damagingMoves.values().next().value!.type; | |
// Choose an attacking move that is of different type to the current single attack | |
const coverageMoves = []; | |
for (const moveid of movePool) { | |
const move = this.dex.moves.get(moveid); | |
const moveType = this.getMoveType(move, species, abilities, teraType); | |
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback)) { | |
if (currentAttackType !== moveType) coverageMoves.push(moveid); | |
} | |
} | |
if (coverageMoves.length) { | |
const moveid = this.sample(coverageMoves); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
} | |
// Add (moves.size < this.maxMoveCount) as a condition if moves is getting larger than 4 moves. | |
// If you want moves to be favored but not required, add something like && this.randomChance(1, 2) to your condition. | |
// Choose remaining moves randomly from movepool and add them to moves list: | |
while (moves.size < this.maxMoveCount && movePool.length) { | |
if (moves.size + movePool.length <= this.maxMoveCount) { | |
for (const moveid of movePool) { | |
moves.add(moveid); | |
} | |
break; | |
} | |
const moveid = this.sample(movePool); | |
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
for (const pair of MOVE_PAIRS) { | |
if (moveid === pair[0] && movePool.includes(pair[1])) { | |
counter = this.addMove(pair[1], moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
if (moveid === pair[1] && movePool.includes(pair[0])) { | |
counter = this.addMove(pair[0], moves, types, abilities, teamDetails, species, isLead, isDoubles, | |
movePool, teraType, role); | |
} | |
} | |
} | |
return moves; | |
} | |
shouldCullAbility( | |
ability: string, | |
types: string[], | |
moves: Set<string>, | |
abilities: string[], | |
counter: MoveCounter, | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
isDoubles: boolean, | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): boolean { | |
switch (ability) { | |
// Abilities which are primarily useful for certain moves or with team support | |
case 'Chlorophyll': case 'Solar Power': | |
return !teamDetails.sun; | |
case 'Defiant': | |
return (species.id === 'thundurus' && !!counter.get('Status')); | |
case 'Hydration': case 'Swift Swim': | |
return !teamDetails.rain; | |
case 'Iron Fist': case 'Skill Link': | |
return !counter.get(toID(ability)); | |
case 'Overgrow': | |
return !counter.get('Grass'); | |
case 'Prankster': | |
return !counter.get('Status'); | |
case 'Sand Force': case 'Sand Rush': | |
return !teamDetails.sand; | |
case 'Slush Rush': | |
return !teamDetails.snow; | |
case 'Swarm': | |
return !counter.get('Bug'); | |
case 'Torrent': | |
return (!counter.get('Water') && !moves.has('flipturn')); | |
} | |
return false; | |
} | |
getAbility( | |
types: string[], | |
moves: Set<string>, | |
abilities: string[], | |
counter: MoveCounter, | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
isDoubles: boolean, | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): string { | |
// ffa abilities that differ from doubles | |
if (this.format.gameType === 'freeforall') { | |
if (species.id === 'bellossom') return 'Chlorophyll'; | |
if (species.id === 'sinistcha') return 'Heatproof'; | |
if (abilities.length === 1 && abilities[0] === 'Telepathy') { | |
return species.id === 'oranguru' ? 'Inner Focus' : 'Pressure'; | |
} | |
if (species.id === 'duraludon') return 'Light Metal'; | |
if (species.id === 'clefairy') return 'Magic Guard'; | |
if (species.id === 'blissey') return 'Natural Cure'; | |
if (species.id === 'barraskewda') return 'Swift Swim'; | |
} | |
if (abilities.length <= 1) return abilities[0]; | |
// Hard-code abilities here | |
if (species.id === 'drifblim') return moves.has('defog') ? 'Aftermath' : 'Unburden'; | |
if (abilities.includes('Flash Fire') && this.dex.getEffectiveness('Fire', teraType) >= 1) return 'Flash Fire'; | |
if (species.id === 'hitmonchan' && counter.get('ironfist')) return 'Iron Fist'; | |
if ((species.id === 'thundurus' || species.id === 'tornadus') && !counter.get('Physical')) return 'Prankster'; | |
if (species.id === 'swampert' && (counter.get('Water') || moves.has('flipturn'))) return 'Torrent'; | |
if (species.id === 'toucannon' && counter.get('skilllink')) return 'Skill Link'; | |
if (abilities.includes('Slush Rush') && moves.has('snowscape')) return 'Slush Rush'; | |
if (species.id === 'golduck' && teamDetails.rain) return 'Swift Swim'; | |
const abilityAllowed: string[] = []; | |
// Obtain a list of abilities that are allowed (not culled) | |
for (const ability of abilities) { | |
if (!this.shouldCullAbility( | |
ability, types, moves, abilities, counter, teamDetails, species, isLead, isDoubles, teraType, role | |
)) { | |
abilityAllowed.push(ability); | |
} | |
} | |
// Pick a random allowed ability | |
if (abilityAllowed.length >= 1) return this.sample(abilityAllowed); | |
// If all abilities are rejected, prioritize weather abilities over non-weather abilities | |
if (!abilityAllowed.length) { | |
const weatherAbilities = abilities.filter( | |
a => ['Chlorophyll', 'Hydration', 'Sand Force', 'Sand Rush', 'Slush Rush', 'Solar Power', 'Swift Swim'].includes(a) | |
); | |
if (weatherAbilities.length) return this.sample(weatherAbilities); | |
} | |
// Pick a random ability | |
return this.sample(abilities); | |
} | |
getPriorityItem( | |
ability: string, | |
types: string[], | |
moves: Set<string>, | |
counter: MoveCounter, | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
isDoubles: boolean, | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
) { | |
if (!isDoubles) { | |
if (role === 'Fast Bulky Setup' && (ability === 'Quark Drive' || ability === 'Protosynthesis')) { | |
return 'Booster Energy'; | |
} | |
if (species.id === 'lokix') { | |
return (role === 'Fast Attacker') ? 'Silver Powder' : 'Life Orb'; | |
} | |
} | |
if (species.requiredItems) { | |
// Z-Crystals aren't available in Gen 9, so require Plates | |
if (species.baseSpecies === 'Arceus') { | |
return species.requiredItems[0]; | |
} | |
return this.sample(species.requiredItems); | |
} | |
if (role === 'AV Pivot') return 'Assault Vest'; | |
if (species.id === 'pikachu') return 'Light Ball'; | |
if (species.id === 'regieleki') return 'Magnet'; | |
if (types.includes('Normal') && moves.has('doubleedge') && moves.has('fakeout')) return 'Silk Scarf'; | |
if ( | |
species.id === 'froslass' || moves.has('populationbomb') || | |
(ability === 'Hustle' && counter.get('setup') && !isDoubles && this.randomChance(1, 2)) | |
) return 'Wide Lens'; | |
if (species.id === 'smeargle' && !isDoubles) return 'Focus Sash'; | |
if (moves.has('clangoroussoul') || (species.id === 'toxtricity' && moves.has('shiftgear'))) return 'Throat Spray'; | |
if ( | |
(species.baseSpecies === 'Magearna' && role === 'Tera Blast user') || | |
species.id === 'necrozmaduskmane' || (species.id === 'calyrexice' && isDoubles) | |
) return 'Weakness Policy'; | |
if (['dragonenergy', 'lastrespects', 'waterspout'].some(m => moves.has(m))) return 'Choice Scarf'; | |
if ( | |
!isDoubles && (ability === 'Imposter' || (species.id === 'magnezone' && role === 'Fast Attacker')) | |
) return 'Choice Scarf'; | |
if (species.id === 'rampardos' && (role === 'Fast Attacker' || isDoubles)) return 'Choice Scarf'; | |
if (species.id === 'palkia' && counter.get('Special') < 4) return 'Lustrous Orb'; | |
if ( | |
moves.has('courtchange') || | |
!isDoubles && (species.id === 'luvdisc' || (species.id === 'terapagos' && !moves.has('rest'))) | |
) return 'Heavy-Duty Boots'; | |
if (moves.has('bellydrum') && moves.has('substitute')) return 'Salac Berry'; | |
if ( | |
['Cheek Pouch', 'Cud Chew', 'Harvest', 'Ripen'].some(m => ability === m) || | |
moves.has('bellydrum') || moves.has('filletaway') | |
) { | |
return 'Sitrus Berry'; | |
} | |
if (['healingwish', 'switcheroo', 'trick'].some(m => moves.has(m))) { | |
if ( | |
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && | |
role !== 'Wallbreaker' && role !== 'Doubles Wallbreaker' && !counter.get('priority') | |
) { | |
return 'Choice Scarf'; | |
} else { | |
return (counter.get('Physical') > counter.get('Special')) ? 'Choice Band' : 'Choice Specs'; | |
} | |
} | |
if (counter.get('Status') && (species.name === 'Latias' || species.name === 'Latios')) return 'Soul Dew'; | |
if (species.id === 'scyther' && !isDoubles) return (isLead && !moves.has('uturn')) ? 'Eviolite' : 'Heavy-Duty Boots'; | |
if (ability === 'Poison Heal' || ability === 'Quick Feet') return 'Toxic Orb'; | |
if (species.nfe) return 'Eviolite'; | |
if ((ability === 'Guts' || moves.has('facade')) && !moves.has('sleeptalk')) { | |
return (types.includes('Fire') || ability === 'Toxic Boost') ? 'Toxic Orb' : 'Flame Orb'; | |
} | |
if (ability === 'Magic Guard' || (ability === 'Sheer Force' && counter.get('sheerforce'))) return 'Life Orb'; | |
if (ability === 'Anger Shell') return this.sample(['Rindo Berry', 'Passho Berry', 'Scope Lens', 'Sitrus Berry']); | |
if (moves.has('dragondance') && isDoubles) return 'Clear Amulet'; | |
if (counter.get('skilllink') && ability !== 'Skill Link' && species.id !== 'breloom') return 'Loaded Dice'; | |
if (ability === 'Unburden') { | |
return (moves.has('closecombat') || moves.has('leafstorm')) ? 'White Herb' : 'Sitrus Berry'; | |
} | |
if (moves.has('shellsmash') && ability !== 'Weak Armor') return 'White Herb'; | |
if (moves.has('meteorbeam') || (moves.has('electroshot') && !teamDetails.rain)) return 'Power Herb'; | |
if (moves.has('acrobatics') && ability !== 'Protosynthesis') return ''; | |
if (moves.has('auroraveil') || moves.has('lightscreen') && moves.has('reflect')) return 'Light Clay'; | |
if (ability === 'Gluttony') return `${this.sample(['Aguav', 'Figy', 'Iapapa', 'Mago', 'Wiki'])} Berry`; | |
if ( | |
moves.has('rest') && !moves.has('sleeptalk') && | |
ability !== 'Natural Cure' && ability !== 'Shed Skin' | |
) { | |
return 'Chesto Berry'; | |
} | |
if ( | |
species.id !== 'yanmega' && | |
this.dex.getEffectiveness('Rock', species) >= 2 && (!types.includes('Flying') || !isDoubles) | |
) return 'Heavy-Duty Boots'; | |
} | |
/** Item generation specific to Random Doubles */ | |
getDoublesItem( | |
ability: string, | |
types: string[], | |
moves: Set<string>, | |
counter: MoveCounter, | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): string { | |
const scarfReqs = ( | |
!counter.get('priority') && ability !== 'Speed Boost' && role !== 'Doubles Wallbreaker' && | |
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && | |
this.randomChance(1, 2) | |
); | |
const offensiveRole = ( | |
['Doubles Fast Attacker', 'Doubles Wallbreaker', 'Doubles Setup Sweeper', 'Offensive Protect'].some(m => role === m) | |
); | |
const doublesLeftoversHardcodes = ( | |
moves.has('acidarmor') || species.id === 'eternatus' || species.id === 'regigigas' || moves.has('wish') | |
); | |
if (species.id === 'ursalunabloodmoon' && moves.has('protect')) return 'Silk Scarf'; | |
if ( | |
moves.has('flipturn') && moves.has('protect') && (moves.has('aquajet') || (moves.has('jetpunch'))) | |
) return 'Mystic Water'; | |
if (counter.get('speedsetup') && role === 'Doubles Bulky Setup') return 'Weakness Policy'; | |
if (species.id === 'toxapex') return 'Binding Band'; | |
if (moves.has('blizzard') && ability !== 'Snow Warning' && !teamDetails.snow) return 'Blunder Policy'; | |
if (role === 'Choice Item user') { | |
if (scarfReqs || (counter.get('Physical') < 4 && counter.get('Special') < 3 && !moves.has('memento'))) { | |
return 'Choice Scarf'; | |
} | |
return (counter.get('Physical') >= 3) ? 'Choice Band' : 'Choice Specs'; | |
} | |
if (counter.get('Physical') >= 4 && | |
['fakeout', 'feint', 'firstimpression', 'rapidspin', 'suckerpunch'].every(m => !moves.has(m)) && | |
(moves.has('flipturn') || moves.has('uturn') || role === 'Doubles Wallbreaker') | |
) { | |
return (scarfReqs) ? 'Choice Scarf' : 'Choice Band'; | |
} | |
if ( | |
((counter.get('Special') >= 4 && (moves.has('voltswitch') || role === 'Doubles Wallbreaker')) || ( | |
counter.get('Special') >= 3 && (moves.has('uturn') || moves.has('flipturn')) | |
)) && !moves.has('electroweb') | |
) { | |
return (scarfReqs) ? 'Choice Scarf' : 'Choice Specs'; | |
} | |
if ( | |
(role === 'Bulky Protect' && counter.get('setup')) || moves.has('substitute') || moves.has('irondefense') || | |
moves.has('coil') || doublesLeftoversHardcodes | |
) return 'Leftovers'; | |
if (species.id === 'sylveon') return 'Pixie Plate'; | |
if (ability === 'Intimidate' && this.dex.getEffectiveness('Rock', species) >= 1) return 'Heavy-Duty Boots'; | |
if ( | |
(offensiveRole || (role === 'Tera Blast user' && (species.baseStats.spe >= 80 || moves.has('trickroom')))) && | |
(!moves.has('fakeout') || species.id === 'ambipom') && !moves.has('incinerate') && | |
(!moves.has('uturn') || types.includes('Bug') || ability === 'Libero') && | |
((!moves.has('icywind') && !moves.has('electroweb')) || species.id === 'ironbundle') | |
) { | |
return ( | |
(ability === 'Quark Drive' || ability === 'Protosynthesis') && !isLead && species.id !== 'ironvaliant' && | |
['dracometeor', 'firstimpression', 'uturn', 'voltswitch'].every(m => !moves.has(m)) | |
) ? 'Booster Energy' : 'Life Orb'; | |
} | |
if (isLead && (species.id === 'glimmora' || | |
(['Doubles Fast Attacker', 'Doubles Wallbreaker', 'Offensive Protect'].includes(role) && | |
species.baseStats.hp + species.baseStats.def + species.baseStats.spd <= 230)) | |
) return 'Focus Sash'; | |
if ( | |
['Doubles Fast Attacker', 'Doubles Wallbreaker', 'Offensive Protect'].includes(role) && | |
moves.has('fakeout') || moves.has('incinerate') | |
) { | |
return (this.dex.getEffectiveness('Rock', species) >= 1) ? 'Heavy-Duty Boots' : 'Clear Amulet'; | |
} | |
if (!counter.get('Status')) return 'Assault Vest'; | |
return 'Sitrus Berry'; | |
} | |
getItem( | |
ability: string, | |
types: string[], | |
moves: Set<string>, | |
counter: MoveCounter, | |
teamDetails: RandomTeamsTypes.TeamDetails, | |
species: Species, | |
isLead: boolean, | |
teraType: string, | |
role: RandomTeamsTypes.Role, | |
): string { | |
if ( | |
species.id !== 'jirachi' && (counter.get('Physical') >= 4) && | |
['dragontail', 'fakeout', 'firstimpression', 'flamecharge', 'rapidspin'].every(m => !moves.has(m)) | |
) { | |
const scarfReqs = ( | |
role !== 'Wallbreaker' && | |
(species.baseStats.atk >= 100 || ability === 'Huge Power' || ability === 'Pure Power') && | |
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && | |
ability !== 'Speed Boost' && !counter.get('priority') && !moves.has('aquastep') | |
); | |
return (scarfReqs && this.randomChance(1, 2)) ? 'Choice Scarf' : 'Choice Band'; | |
} | |
if ( | |
(counter.get('Special') >= 4) || | |
(counter.get('Special') >= 3 && ['flipturn', 'uturn'].some(m => moves.has(m))) | |
) { | |
const scarfReqs = ( | |
role !== 'Wallbreaker' && | |
species.baseStats.spa >= 100 && | |
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && | |
ability !== 'Speed Boost' && ability !== 'Tinted Lens' && !moves.has('uturn') && !counter.get('priority') | |
); | |
return (scarfReqs && this.randomChance(1, 2)) ? 'Choice Scarf' : 'Choice Specs'; | |
} | |
if (counter.get('speedsetup') && role === 'Bulky Setup') return 'Weakness Policy'; | |
if ( | |
!counter.get('Status') && | |
!['Fast Attacker', 'Wallbreaker', 'Tera Blast user'].includes(role) | |
) { | |
return 'Assault Vest'; | |
} | |
if (species.id === 'golem') return (counter.get('speedsetup')) ? 'Weakness Policy' : 'Custap Berry'; | |
if (moves.has('substitute')) return 'Leftovers'; | |
if ( | |
moves.has('stickyweb') && isLead && | |
(species.baseStats.hp + species.baseStats.def + species.baseStats.spd) <= 235 | |
) return 'Focus Sash'; | |
if (this.dex.getEffectiveness('Rock', species) >= 1) return 'Heavy-Duty Boots'; | |
if ( | |
(moves.has('chillyreception') || ( | |
role === 'Fast Support' && | |
[...PIVOT_MOVES, 'defog', 'mortalspin', 'rapidspin'].some(m => moves.has(m)) && | |
!types.includes('Flying') && ability !== 'Levitate' | |
)) | |
) return 'Heavy-Duty Boots'; | |
// Low Priority | |
if ( | |
ability === 'Rough Skin' || ( | |
ability === 'Regenerator' && (role === 'Bulky Support' || role === 'Bulky Attacker') && | |
(species.baseStats.hp + species.baseStats.def) >= 180 && this.randomChance(1, 2) | |
) || ( | |
ability !== 'Regenerator' && !counter.get('setup') && counter.get('recovery') && | |
this.dex.getEffectiveness('Fighting', species) < 1 && | |
(species.baseStats.hp + species.baseStats.def) > 200 && this.randomChance(1, 2) | |
) | |
) return 'Rocky Helmet'; | |
if (moves.has('outrage') && counter.get('setup')) return 'Lum Berry'; | |
if (moves.has('protect') && ability !== 'Speed Boost') return 'Leftovers'; | |
if ( | |
role === 'Fast Support' && isLead && !counter.get('recovery') && !counter.get('recoil') && | |
(counter.get('hazards') || counter.get('setup')) && | |
(species.baseStats.hp + species.baseStats.def + species.baseStats.spd) < 258 | |
) return 'Focus Sash'; | |
if ( | |
!counter.get('setup') && ability !== 'Levitate' && this.dex.getEffectiveness('Ground', species) >= 2 | |
) return 'Air Balloon'; | |
if (['Bulky Attacker', 'Bulky Support', 'Bulky Setup'].some(m => role === (m))) return 'Leftovers'; | |
if (species.id === 'pawmot' && moves.has('nuzzle')) return 'Leppa Berry'; | |
if (role === 'Fast Support' || role === 'Fast Bulky Setup') { | |
return (counter.get('Physical') + counter.get('Special') >= 3 && !moves.has('nuzzle')) ? 'Life Orb' : 'Leftovers'; | |
} | |
if (role === 'Tera Blast user' && DEFENSIVE_TERA_BLAST_USERS.includes(species.id)) return 'Leftovers'; | |
if ( | |
['flamecharge', 'rapidspin', 'trailblaze'].every(m => !moves.has(m)) && | |
['Fast Attacker', 'Setup Sweeper', 'Tera Blast user', 'Wallbreaker'].some(m => role === (m)) | |
) return 'Life Orb'; | |
return 'Leftovers'; | |
} | |
getLevel( | |
species: Species, | |
isDoubles: boolean, | |
): number { | |
if (this.adjustLevel) return this.adjustLevel; | |
// doubles levelling | |
if (isDoubles && this.randomDoublesSets[species.id]["level"]) return this.randomDoublesSets[species.id]["level"]!; | |
if (!isDoubles && this.randomSets[species.id]["level"]) return this.randomSets[species.id]["level"]!; | |
// Default to tier-based levelling | |
const tier = species.tier; | |
const tierScale: Partial<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, | |
}; | |
return tierScale[tier] || 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)); | |
// Consolidate mostly-cosmetic formes, at least for the purposes of Random Battles | |
if (['Dudunsparce', 'Magearna', 'Maushold', 'Polteageist', 'Sinistcha', '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 === 'Pikachu') { | |
return 'Pikachu' + this.sample( | |
['', '-Original', '-Hoenn', '-Sinnoh', '-Unova', '-Kalos', '-Alola', '-Partner', '-World'] | |
); | |
} | |
return species.name; | |
} | |
randomSet( | |
s: string | Species, | |
teamDetails: RandomTeamsTypes.TeamDetails = {}, | |
isLead = false, | |
isDoubles = false | |
): RandomTeamsTypes.RandomSet { | |
const species = this.dex.species.get(s); | |
const forme = this.getForme(species); | |
const sets = this[`random${isDoubles ? 'Doubles' : ''}Sets`][species.id]["sets"]; | |
const possibleSets: RandomTeamsTypes.RandomSetData[] = []; | |
const ruleTable = this.dex.formats.getRuleTable(this.format); | |
for (const set of sets) { | |
// Prevent Fast Bulky Setup on lead Paradox Pokemon, since it generates Booster Energy. | |
const abilities = set.abilities!; | |
if ( | |
isLead && (abilities.includes('Protosynthesis') || abilities.includes('Quark Drive')) && | |
set.role === 'Fast Bulky Setup' | |
) continue; | |
// Prevent Tera Blast user if the team already has one, or if Terastallizion is prevented. | |
if ((teamDetails.teraBlast || ruleTable.has('terastalclause')) && set.role === 'Tera Blast user') { | |
continue; | |
} | |
possibleSets.push(set); | |
} | |
const set = this.sampleIfArray(possibleSets); | |
const role = set.role; | |
const movePool: string[] = []; | |
for (const movename of set.movepool) { | |
movePool.push(this.dex.moves.get(movename).id); | |
} | |
const teraTypes = set.teraTypes!; | |
let teraType = this.sampleIfArray(teraTypes); | |
let ability = ''; | |
let item = undefined; | |
const evs = { hp: 85, atk: 85, def: 85, spa: 85, spd: 85, spe: 85 }; | |
const ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; | |
const types = species.types; | |
const abilities = set.abilities!; | |
// Get moves | |
const moves = this.randomMoveset(types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role); | |
const counter = this.queryMoves(moves, species, teraType, abilities); | |
// Get ability | |
ability = this.getAbility(types, moves, abilities, counter, teamDetails, species, isLead, isDoubles, teraType, role); | |
// Get items | |
// First, the priority items | |
item = this.getPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, isDoubles, teraType, role); | |
if (item === undefined) { | |
if (isDoubles) { | |
item = this.getDoublesItem(ability, types, moves, counter, teamDetails, species, isLead, teraType, role); | |
} else { | |
item = this.getItem(ability, types, moves, counter, teamDetails, species, isLead, teraType, role); | |
} | |
} | |
// Get level | |
const level = this.getLevel(species, isDoubles); | |
// Prepare optimal HP | |
const srImmunity = ability === 'Magic Guard' || item === 'Heavy-Duty Boots'; | |
let srWeakness = srImmunity ? 0 : this.dex.getEffectiveness('Rock', species); | |
// Crash damage move users want an odd HP to survive two misses | |
if (['axekick', 'highjumpkick', 'jumpkick', 'supercellslam'].some(m => moves.has(m))) srWeakness = 2; | |
while (evs.hp > 1) { | |
const hp = Math.floor(Math.floor(2 * species.baseStats.hp + ivs.hp + Math.floor(evs.hp / 4) + 100) * level / 100 + 10); | |
if ((moves.has('substitute') && ['Sitrus Berry', 'Salac Berry'].includes(item)) || species.id === 'minior') { | |
// Two Substitutes should activate Sitrus Berry. Two switch-ins to Stealth Rock should activate Shields Down on Minior. | |
if (hp % 4 === 0) break; | |
} else if ( | |
(moves.has('bellydrum') || moves.has('filletaway') || moves.has('shedtail')) && | |
(item === 'Sitrus Berry' || ability === 'Gluttony') | |
) { | |
// Belly Drum should activate Sitrus Berry | |
if (hp % 2 === 0) break; | |
} else if (moves.has('substitute') && moves.has('endeavor')) { | |
// Luvdisc should be able to Substitute down to very low HP | |
if (hp % 4 > 0) break; | |
} else { | |
// Maximize number of Stealth Rock switch-ins in singles | |
if (isDoubles) break; | |
if (srWeakness <= 0 || ability === 'Regenerator' || ['Leftovers', 'Life Orb'].includes(item)) break; | |
if (item !== 'Sitrus Berry' && hp % (4 / srWeakness) > 0) break; | |
// Minimise number of Stealth Rock switch-ins to activate Sitrus Berry | |
if (item === 'Sitrus Berry' && hp % (4 / srWeakness) === 0) break; | |
} | |
evs.hp -= 4; | |
} | |
// Minimize confusion damage | |
const noAttackStatMoves = [...moves].every(m => { | |
const move = this.dex.moves.get(m); | |
if (move.damageCallback || move.damage) return true; | |
if (move.id === 'shellsidearm') return false; | |
// Physical Tera Blast | |
if ( | |
move.id === 'terablast' && (species.id === 'porygon2' || ['Contrary', 'Defiant'].includes(ability) || | |
moves.has('shiftgear') || species.baseStats.atk > species.baseStats.spa) | |
) return false; | |
return move.category !== 'Physical' || move.id === 'bodypress' || move.id === 'foulplay'; | |
}); | |
if (noAttackStatMoves && !moves.has('transform') && this.format.mod !== 'partnersincrime') { | |
evs.atk = 0; | |
ivs.atk = 0; | |
} | |
if (moves.has('gyroball') || moves.has('trickroom')) { | |
evs.spe = 0; | |
ivs.spe = 0; | |
} | |
// Enforce Tera Type after all set generation is done to prevent infinite generation | |
if (this.forceTeraType) teraType = this.forceTeraType; | |
// shuffle moves to add more randomness to camomons | |
const shuffledMoves = Array.from(moves); | |
this.prng.shuffle(shuffledMoves); | |
return { | |
name: species.baseSpecies, | |
species: forme, | |
gender: species.baseSpecies === 'Greninja' ? 'M' : species.gender, | |
shiny: this.randomChance(1, 1024), | |
level, | |
moves: shuffledMoves, | |
ability, | |
evs, | |
ivs, | |
item, | |
teraType, | |
role, | |
}; | |
} | |
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]; | |
} | |
randomSets: { [species: string]: RandomTeamsTypes.RandomSpeciesData } = require('./sets.json'); | |
randomDoublesSets: { [species: string]: RandomTeamsTypes.RandomSpeciesData } = require('./doubles-sets.json'); | |
randomTeam() { | |
this.enforceNoDirectCustomBanlistChanges(); | |
const seed = this.prng.getSeed(); | |
const ruleTable = this.dex.formats.getRuleTable(this.format); | |
const pokemon: RandomTeamsTypes.RandomSet[] = []; | |
// For Monotype | |
const isMonotype = !!this.forceMonotype || ruleTable.has('sametypeclause'); | |
const isDoubles = this.format.gameType !== 'singles'; | |
const typePool = this.dex.types.names().filter(name => name !== "Stellar"); | |
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 = isDoubles ? Object.keys(this.randomDoublesSets) : Object.keys(this.randomSets); | |
const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, pokemonList); | |
let leadsRemaining = this.format.gameType === 'doubles' ? 2 : 1; | |
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; | |
// Treat Ogerpon formes and Terapagos like the Tera Blast user role; reject if team has one already | |
if (['ogerpon', 'ogerponhearthflame', 'terapagos'].includes(species.id) && teamDetails.teraBlast) continue; | |
// Illusion shouldn't be on the last slot | |
if (species.baseSpecies === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue; | |
const types = species.types; | |
const typeCombo = types.slice().sort().join(); | |
const weakToFreezeDry = ( | |
this.dex.getEffectiveness('Ice', species) > 0 || | |
(this.dex.getEffectiveness('Ice', species) > -2 && types.includes('Water')) | |
); | |
// Dynamically scale limits for different team sizes. The default and minimum value is 1. | |
const limitFactor = Math.round(this.maxTeamSize / 6) || 1; | |
if (!isMonotype && !this.forceMonotype) { | |
let skip = false; | |
// Limit two of any type | |
for (const typeName of types) { | |
if (typeCount[typeName] >= 2 * limitFactor) { | |
skip = true; | |
break; | |
} | |
} | |
if (skip) continue; | |
// Limit three weak to any type, and one double weak to any type | |
for (const typeName of this.dex.types.names()) { | |
// it's weak to the type | |
if (this.dex.getEffectiveness(typeName, species) > 0) { | |
if (!typeWeaknesses[typeName]) typeWeaknesses[typeName] = 0; | |
if (typeWeaknesses[typeName] >= 3 * limitFactor) { | |
skip = true; | |
break; | |
} | |
} | |
if (this.dex.getEffectiveness(typeName, species) > 1) { | |
if (!typeDoubleWeaknesses[typeName]) typeDoubleWeaknesses[typeName] = 0; | |
if (typeDoubleWeaknesses[typeName] >= limitFactor) { | |
skip = true; | |
break; | |
} | |
} | |
} | |
if (skip) continue; | |
// Count Dry Skin/Fluffy as Fire weaknesses | |
if ( | |
this.dex.getEffectiveness('Fire', species) === 0 && | |
Object.values(species.abilities).filter(a => ['Dry Skin', 'Fluffy'].includes(a)).length | |
) { | |
if (!typeWeaknesses['Fire']) typeWeaknesses['Fire'] = 0; | |
if (typeWeaknesses['Fire'] >= 3 * limitFactor) continue; | |
} | |
// Limit four weak to Freeze-Dry | |
if (weakToFreezeDry) { | |
if (!typeWeaknesses['Freeze-Dry']) typeWeaknesses['Freeze-Dry'] = 0; | |
if (typeWeaknesses['Freeze-Dry'] >= 4 * limitFactor) continue; | |
} | |
// Limit one level 100 Pokemon | |
if (!this.adjustLevel && (this.getLevel(species, isDoubles) === 100) && numMaxLevelPokemon >= limitFactor) { | |
continue; | |
} | |
} | |
// Limit three of any type combination in Monotype | |
if (!this.forceMonotype && isMonotype && (typeComboCount[typeCombo] >= 3 * limitFactor)) continue; | |
// The Pokemon of the Day | |
if (potd?.exists && (pokemon.length === 1 || this.maxTeamSize === 1)) species = potd; | |
let set: RandomTeamsTypes.RandomSet; | |
if (leadsRemaining) { | |
if ( | |
isDoubles && DOUBLES_NO_LEAD_POKEMON.includes(species.baseSpecies) || | |
!isDoubles && NO_LEAD_POKEMON.includes(species.baseSpecies) | |
) { | |
if (pokemon.length + leadsRemaining === this.maxTeamSize) continue; | |
set = this.randomSet(species, teamDetails, false, isDoubles); | |
pokemon.push(set); | |
} else { | |
set = this.randomSet(species, teamDetails, true, isDoubles); | |
pokemon.unshift(set); | |
leadsRemaining--; | |
} | |
} else { | |
set = this.randomSet(species, teamDetails, false, isDoubles); | |
pokemon.push(set); | |
} | |
// Don't bother tracking details for the last Pokemon | |
if (pokemon.length === this.maxTeamSize) break; | |
// Now that our Pokemon has passed all checks, we can increment our counters | |
baseFormes[species.baseSpecies] = 1; | |
// Increment type counters | |
for (const typeName of types) { | |
if (typeName in typeCount) { | |
typeCount[typeName]++; | |
} else { | |
typeCount[typeName] = 1; | |
} | |
} | |
if (typeCombo in typeComboCount) { | |
typeComboCount[typeCombo]++; | |
} else { | |
typeComboCount[typeCombo] = 1; | |
} | |
// Increment weakness counter | |
for (const typeName of this.dex.types.names()) { | |
// it's weak to the type | |
if (this.dex.getEffectiveness(typeName, species) > 0) { | |
typeWeaknesses[typeName]++; | |
} | |
if (this.dex.getEffectiveness(typeName, species) > 1) { | |
typeDoubleWeaknesses[typeName]++; | |
} | |
} | |
// Count Dry Skin/Fluffy as Fire weaknesses | |
if (['Dry Skin', 'Fluffy'].includes(set.ability) && this.dex.getEffectiveness('Fire', species) === 0) { | |
typeWeaknesses['Fire']++; | |
} | |
if (weakToFreezeDry) typeWeaknesses['Freeze-Dry']++; | |
// Increment level 100 counter | |
if (set.level === 100) numMaxLevelPokemon++; | |
// Track what the team has | |
if (set.ability === 'Drizzle' || set.moves.includes('raindance')) teamDetails.rain = 1; | |
if (set.ability === 'Drought' || set.ability === 'Orichalcum Pulse' || set.moves.includes('sunnyday')) { | |
teamDetails.sun = 1; | |
} | |
if (set.ability === 'Sand Stream') teamDetails.sand = 1; | |
if (set.ability === 'Snow Warning' || set.moves.includes('snowscape') || set.moves.includes('chillyreception')) { | |
teamDetails.snow = 1; | |
} | |
if (set.moves.includes('healbell')) teamDetails.statusCure = 1; | |
if (set.moves.includes('spikes') || set.moves.includes('ceaselessedge')) { | |
teamDetails.spikes = (teamDetails.spikes || 0) + 1; | |
} | |
if (set.moves.includes('toxicspikes') || set.ability === 'Toxic Debris') teamDetails.toxicSpikes = 1; | |
if (set.moves.includes('stealthrock') || set.moves.includes('stoneaxe')) teamDetails.stealthRock = 1; | |
if (set.moves.includes('stickyweb')) teamDetails.stickyWeb = 1; | |
if (set.moves.includes('defog')) teamDetails.defog = 1; | |
if (set.moves.includes('rapidspin') || set.moves.includes('mortalspin')) teamDetails.rapidSpin = 1; | |
if (set.moves.includes('auroraveil') || (set.moves.includes('reflect') && set.moves.includes('lightscreen'))) { | |
teamDetails.screens = 1; | |
} | |
if (set.role === 'Tera Blast user' || ['ogerpon', 'ogerponhearthflame', 'terapagos'].includes(species.id)) { | |
teamDetails.teraBlast = 1; | |
} | |
} | |
if (pokemon.length < this.maxTeamSize && pokemon.length < 12) { // large teams sometimes cannot be built | |
throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`); | |
} | |
return pokemon; | |
} | |
randomCCTeam(): RandomTeamsTypes.RandomSet[] { | |
this.enforceNoDirectCustomBanlistChanges(); | |
const dex = this.dex; | |
const team = []; | |
const natures = this.dex.natures.all(); | |
const items = this.dex.items.all(); | |
const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined, undefined, true); | |
for (let forme of randomN) { | |
let species = dex.species.get(forme); | |
if (species.isNonstandard) species = dex.species.get(species.baseSpecies); | |
// Random legal item | |
let item = ''; | |
let isIllegalItem; | |
let isBadItem; | |
if (this.gen >= 2) { | |
do { | |
item = this.sample(items).name; | |
isIllegalItem = this.dex.items.get(item).gen > this.gen || this.dex.items.get(item).isNonstandard; | |
isBadItem = item.startsWith("TR") || this.dex.items.get(item).isPokeball; | |
} while (isIllegalItem || (isBadItem && this.randomChance(19, 20))); | |
} | |
// Make sure forme is legal | |
if (species.battleOnly) { | |
if (typeof species.battleOnly === 'string') { | |
species = dex.species.get(species.battleOnly); | |
} else { | |
species = dex.species.get(this.sample(species.battleOnly)); | |
} | |
forme = species.name; | |
} else if (species.requiredItems && !species.requiredItems.some(req => toID(req) === item)) { | |
if (!species.changesFrom) throw new Error(`${species.name} needs a changesFrom value`); | |
species = dex.species.get(species.changesFrom); | |
forme = species.name; | |
} | |
// Make sure that a base forme does not hold any forme-modifier items. | |
let itemData = this.dex.items.get(item); | |
if (itemData.forcedForme && forme === this.dex.species.get(itemData.forcedForme).baseSpecies) { | |
do { | |
itemData = this.sample(items); | |
item = itemData.name; | |
} while ( | |
itemData.gen > this.gen || | |
itemData.isNonstandard || | |
(itemData.forcedForme && forme === this.dex.species.get(itemData.forcedForme).baseSpecies) | |
); | |
} | |
// Random legal ability | |
const abilities = Object.values(species.abilities).filter(a => this.dex.abilities.get(a).gen <= this.gen); | |
const ability: string = this.gen <= 2 ? 'No Ability' : this.sample(abilities); | |
// Four random unique moves from the movepool | |
let pool = ['struggle']; | |
if (forme === 'Smeargle') { | |
pool = this.dex.moves.all() | |
.filter(move => !(move.isNonstandard || move.isZ || move.isMax || move.realMove)) | |
.map(m => m.id); | |
} else { | |
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; | |
// If Terapagos, use Terastal-forme's stats | |
if (species.baseSpecies === 'Terapagos') stats = Dex.species.get('terapagosterastal').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 | |
if (this.forceTeraType) { | |
set.teraType = this.forceTeraType; | |
} else { | |
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)); | |
} else { | |
const hasAllMovesBan = ruleTable.check('pokemontag:allmoves'); | |
for (const move of this.dex.moves.all()) { | |
let banReason = ruleTable.check('move:' + move.id); | |
if (banReason) continue; | |
if (banReason !== '') { | |
if (hasAllMovesBan) continue; | |
if (move.isNonstandard) { | |
banReason = ruleTable.check('pokemontag:' + toID(move.isNonstandard)); | |
if (banReason) continue; | |
if (banReason !== '' && move.isNonstandard !== 'Unobtainable') { | |
if (hasNonexistentBan) continue; | |
if (!hasNonexistentWhitelist) continue; | |
} | |
} | |
} | |
movePool.push(move); | |
} | |
this.enforceCustomPoolSizeNoComplexBans('move', movePool, this.maxTeamSize * setMoveCount, 'Max Team Size * Max Move Count'); | |
} | |
// Nature Pool | |
const doNaturesExist = this.gen > 2; | |
let naturePool: Nature[] = []; | |
if (doNaturesExist) { | |
if (!hasCustomBans) { | |
naturePool = [...this.dex.natures.all()]; | |
} else { | |
const hasAllNaturesBan = ruleTable.check('pokemontag:allnatures'); | |
for (const nature of this.dex.natures.all()) { | |
let banReason = ruleTable.check('nature:' + nature.id); | |
if (banReason) continue; | |
if (banReason !== '' && nature.id) { | |
if (hasAllNaturesBan) continue; | |
if (nature.isNonstandard) { | |
banReason = ruleTable.check('pokemontag:' + toID(nature.isNonstandard)); | |
if (banReason) continue; | |
if (banReason !== '' && nature.isNonstandard !== 'Unobtainable') { | |
if (hasNonexistentBan) continue; | |
if (!hasNonexistentWhitelist) continue; | |
} | |
} | |
} | |
naturePool.push(nature); | |
} | |
// There is no 'nature:nonature' rule so do not constrain pool size | |
} | |
} | |
const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined, | |
hasCustomBans ? ruleTable : undefined); | |
const team = []; | |
for (const forme of randomN) { | |
// Choose forme | |
const species = this.dex.species.get(forme); | |
// Random unique item | |
let item = ''; | |
let itemData; | |
let isBadItem; | |
if (doItemsExist) { | |
// We discard TRs and Balls with 95% probability because of their otherwise overwhelming presence | |
do { | |
itemData = this.sampleNoReplace(itemPool); | |
item = itemData?.name; | |
isBadItem = item.startsWith("TR") || itemData.isPokeball; | |
} while (isBadItem && this.randomChance(19, 20) && itemPool.length > this.maxTeamSize); | |
} | |
// Random unique ability | |
let ability = 'No Ability'; | |
let abilityData; | |
if (doAbilitiesExist) { | |
abilityData = this.sampleNoReplace(abilityPool); | |
ability = abilityData?.name; | |
} | |
// Random unique moves | |
const m = []; | |
do { | |
const move = this.sampleNoReplace(movePool); | |
m.push(move.id); | |
} while (m.length < setMoveCount); | |
// Random EVs | |
const evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; | |
if (this.gen === 6) { | |
let evpool = 510; | |
do { | |
const x = this.sample(Dex.stats.ids()); | |
const y = this.random(Math.min(256 - evs[x], evpool + 1)); | |
evs[x] += y; | |
evpool -= y; | |
} while (evpool > 0); | |
} else { | |
for (const x of Dex.stats.ids()) { | |
evs[x] = this.random(256); | |
} | |
} | |
// Random IVs | |
const ivs: StatsTable = { | |
hp: this.random(32), | |
atk: this.random(32), | |
def: this.random(32), | |
spa: this.random(32), | |
spd: this.random(32), | |
spe: this.random(32), | |
}; | |
// Random nature | |
let nature = ''; | |
if (doNaturesExist && (naturePool.length > 0)) { | |
nature = this.sample(naturePool).name; | |
} | |
// Level balance | |
const mbstmin = 1307; | |
const stats = species.baseStats; | |
let mbst = (stats['hp'] * 2 + 31 + 21 + 100) + 10; | |
mbst += (stats['atk'] * 2 + 31 + 21 + 100) + 5; | |
mbst += (stats['def'] * 2 + 31 + 21 + 100) + 5; | |
mbst += (stats['spa'] * 2 + 31 + 21 + 100) + 5; | |
mbst += (stats['spd'] * 2 + 31 + 21 + 100) + 5; | |
mbst += (stats['spe'] * 2 + 31 + 21 + 100) + 5; | |
let level; | |
if (this.adjustLevel) { | |
level = this.adjustLevel; | |
} else { | |
level = Math.floor(100 * mbstmin / mbst); | |
while (level < 100) { | |
mbst = Math.floor((stats['hp'] * 2 + 31 + 21 + 100) * level / 100 + 10); | |
mbst += Math.floor(((stats['atk'] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); | |
mbst += Math.floor((stats['def'] * 2 + 31 + 21 + 100) * level / 100 + 5); | |
mbst += Math.floor(((stats['spa'] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100); | |
mbst += Math.floor((stats['spd'] * 2 + 31 + 21 + 100) * level / 100 + 5); | |
mbst += Math.floor((stats['spe'] * 2 + 31 + 21 + 100) * level / 100 + 5); | |
if (mbst >= mbstmin) break; | |
level++; | |
} | |
} | |
// Random happiness | |
const happiness = this.random(256); | |
// Random shininess | |
const shiny = this.randomChance(1, 1024); | |
const set: PokemonSet = { | |
name: species.baseSpecies, | |
species: species.name, | |
gender: species.gender, | |
item, | |
ability, | |
moves: m, | |
evs, | |
ivs, | |
nature, | |
level, | |
happiness, | |
shiny, | |
}; | |
if (this.gen === 9) { | |
// Random Tera type | |
if (this.forceTeraType) { | |
set.teraType = this.forceTeraType; | |
} else { | |
set.teraType = this.sample(this.dex.types.names()); | |
} | |
} | |
team.push(set); | |
} | |
return team; | |
} | |
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 itemsLimited = ['choicespecs', 'choiceband', 'choicescarf']; | |
const movesLimited: { [k: string]: string } = { | |
stealthrock: 'stealthRock', | |
stoneaxe: 'stealthRock', | |
spikes: 'spikes', | |
ceaselessedge: 'spikes', | |
toxicspikes: 'toxicSpikes', | |
rapidspin: 'hazardClear', | |
defog: 'hazardClear', | |
}; | |
const abilitiesLimited: { [k: string]: string } = { | |
toxicdebris: 'toxicSpikes', | |
}; | |
// Build a pool of eligible sets, given the team partners | |
// Also keep track of moves and items limited to one per team | |
const effectivePool: { | |
set: BattleFactorySet, moves?: string[], item?: string, | |
}[] = []; | |
for (const set of setList) { | |
let reject = false; | |
// limit to 1 dedicated tera user per team | |
if (set.wantsTera && teamData.wantsTeraCount) { | |
continue; | |
} | |
// reject disallowed items, specifically a second of any given choice item | |
const allowedItems: string[] = []; | |
for (const itemString of set.item) { | |
const itemId = toID(itemString); | |
if (itemsLimited.includes(itemId) && teamData.has[itemId]) continue; | |
allowedItems.push(itemString); | |
} | |
if (!allowedItems.length) continue; | |
const item = this.sample(allowedItems); | |
const abilityId = toID(this.sample(set.ability)); | |
if (abilitiesLimited[abilityId] && teamData.has[abilitiesLimited[abilityId]]) continue; | |
const moves: string[] = []; | |
for (const move of set.moves) { | |
const allowedMoves: string[] = []; | |
for (const m of move) { | |
const moveId = toID(m); | |
if (movesLimited[moveId] && teamData.has[movesLimited[moveId]]) continue; | |
allowedMoves.push(m); | |
} | |
if (!allowedMoves.length) { | |
reject = true; | |
break; | |
} | |
moves.push(this.sample(allowedMoves)); | |
} | |
if (reject) continue; | |
effectivePool.push({ set, moves, item }); | |
} | |
if (!effectivePool.length) { | |
if (!teamData.forceResult) return null; | |
for (const set of setList) { | |
effectivePool.push({ set }); | |
} | |
} | |
// Sets have individual weight, choose one with weighted random selection | |
let setData = this.sample(effectivePool); // Init with unweighted random set as fallback | |
const total = effectivePool.reduce((a, b) => a + b.set.weight, 0); | |
const setRand = this.random(total); | |
let cur = 0; | |
for (const set of effectivePool) { | |
cur += set.set.weight; | |
if (cur > setRand) { | |
setData = set; // Bingo! | |
break; | |
} | |
} | |
const moves = []; | |
for (const [i, moveSlot] of setData.set.moves.entries()) { | |
moves.push(setData.moves ? setData.moves[i] : this.sample(moveSlot)); | |
} | |
const item = setData.item || this.sample(setData.set.item); | |
return { | |
name: species.baseSpecies, | |
species: (typeof species.battleOnly === 'string') ? species.battleOnly : species.name, | |
teraType: this.sample(setData.set.teraType), | |
gender: setData.set.gender || species.gender || (tier === 'OU' ? 'F' : ''), // F for Cute Charm Enamorus | |
item, | |
ability: this.sample(setData.set.ability), | |
shiny: setData.set.shiny || this.randomChance(1, 1024), | |
level: this.adjustLevel || (tier === "LC" ? 5 : 100), | |
happiness: 255, | |
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: this.sample(setData.set.nature) || "Serious", | |
moves, | |
wantsTera: setData.set.wantsTera, | |
}; | |
} | |
randomFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] { | |
this.enforceNoDirectCustomBanlistChanges(); | |
const forceResult = depth >= 12; | |
if (!this.factoryTier) { | |
// this.factoryTier = this.sample(['Uber', 'OU', 'UU', 'RU', 'NU', 'PU', 'LC']); | |
this.factoryTier = this.sample(['Uber', 'OU', 'UU', 'RU', 'NU', 'PU']); | |
} | |
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 teamData: TeamData = { | |
typeCount: {}, | |
typeComboCount: {}, | |
baseFormes: {}, | |
has: {}, | |
wantsTeraCount: 0, | |
forceResult, | |
weaknesses: {}, | |
resistances: {}, | |
}; | |
const resistanceAbilities: { [k: string]: string[] } = { | |
dryskin: ['Water'], waterabsorb: ['Water'], stormdrain: ['Water'], | |
flashfire: ['Fire'], heatproof: ['Fire'], waterbubble: ['Fire'], wellbakedbody: ['Fire'], | |
lightningrod: ['Electric'], motordrive: ['Electric'], voltabsorb: ['Electric'], | |
sapsipper: ['Grass'], | |
thickfat: ['Ice', 'Fire'], | |
eartheater: ['Ground'], levitate: ['Ground'], | |
}; | |
const movesLimited: { [k: string]: string } = { | |
stealthrock: 'stealthRock', | |
stoneaxe: 'stealthRock', | |
spikes: 'spikes', | |
ceaselessedge: 'spikes', | |
toxicspikes: 'toxicSpikes', | |
rapidspin: 'hazardClear', | |
defog: 'hazardClear', | |
}; | |
const abilitiesLimited: { [k: string]: string } = { | |
toxicdebris: 'toxicSpikes', | |
}; | |
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.randomFactorySets[this.factoryTier][speciesName].weight), | |
}; | |
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 species = this.dex.species.get(shuffledSpecies.pop()!.speciesName); | |
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; | |
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; | |
if (!teamData.forceResult && !this.forceMonotype) { | |
// Limit 3 of any weakness | |
for (const typeName of this.dex.types.names()) { | |
// it's weak to the type | |
if (this.dex.getEffectiveness(typeName, species) > 0 && this.dex.getImmunity(typeName, types)) { | |
if (teamData.weaknesses[typeName] >= 3 * limitFactor) { | |
skip = true; | |
break; | |
} | |
} | |
} | |
} | |
if (skip) continue; | |
const set = this.randomFactorySet(species, teamData, this.factoryTier); | |
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; | |
// 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[toID(set.item)] = 1; | |
if (set.wantsTera) { | |
if (!teamData.wantsTeraCount) teamData.wantsTeraCount = 0; | |
teamData.wantsTeraCount++; | |
} | |
for (const move of set.moves) { | |
const moveId = toID(move); | |
if (movesLimited[moveId]) { | |
teamData.has[movesLimited[moveId]] = 1; | |
} | |
} | |
const ability = this.dex.abilities.get(set.ability); | |
if (abilitiesLimited[ability.id]) { | |
teamData.has[abilitiesLimited[ability.id]] = 1; | |
} | |
for (const typeName of this.dex.types.names()) { | |
const typeMod = this.dex.getEffectiveness(typeName, types); | |
// Track resistances because we will require it for triple weaknesses | |
if ( | |
typeMod < 0 || | |
resistanceAbilities[ability.id]?.includes(typeName) || | |
!this.dex.getImmunity(typeName, types) | |
) { | |
// We don't care about the number of resistances, so just set to 1 | |
teamData.resistances[typeName] = 1; | |
// Track weaknesses | |
} else if (typeMod > 0) { | |
teamData.weaknesses[typeName] = (teamData.weaknesses[typeName] || 0) + 1; | |
} | |
} | |
} | |
if (!teamData.forceResult && pokemon.length < this.maxTeamSize) return this.randomFactoryTeam(side, ++depth); | |
// Quality control we cannot afford for monotype | |
if (!teamData.forceResult && !this.forceMonotype) { | |
for (const type in teamData.weaknesses) { | |
// We reject if our team is triple weak to any type without having a resist | |
if (teamData.resistances[type]) continue; | |
if (teamData.weaknesses[type] >= 3 * limitFactor) return this.randomFactoryTeam(side, ++depth); | |
} | |
// Try to force Stealth Rock on non-Uber teams | |
if (!teamData.has['stealthRock'] && this.factoryTier !== 'Uber') 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 weatherAbilities = ['drizzle', 'drought', 'snowwarning', 'sandstream']; | |
const terrainAbilities: { [k: string]: string } = { | |
electricsurge: "electric", | |
psychicsurge: "psychic", | |
grassysurge: "grassy", | |
seedsower: "grassy", | |
mistysurge: "misty", | |
}; | |
const terrainItemsRequire: { [k: string]: string } = { | |
electricseed: "electric", | |
psychicseed: "psychic", | |
grassyseed: "grassy", | |
mistyseed: "misty", | |
}; | |
const maxWantsTera = 2; | |
// Build a pool of eligible sets, given the team partners | |
// Also keep track of sets with moves the team requires | |
const effectivePool: { | |
set: BSSFactorySet, moveVariants?: number[], itemVariants?: number, abilityVariants?: number, | |
}[] = []; | |
for (const curSet of setList) { | |
let reject = false; | |
// limit to 2 dedicated tera users per team | |
if (curSet.wantsTera && teamData.wantsTeraCount && teamData.wantsTeraCount >= maxWantsTera) { | |
continue; | |
} | |
// reject 2+ weather setters | |
if (teamData.weather && weatherAbilities.includes(curSet.ability)) { | |
continue; | |
} | |
if (terrainAbilities[curSet.ability]) { | |
if (!teamData.terrain) teamData.terrain = []; | |
teamData.terrain.push(terrainAbilities[curSet.ability]); | |
} | |
for (const item of curSet.item) { | |
if (terrainItemsRequire[item] && !teamData.terrain?.includes(terrainItemsRequire[item])) { | |
reject = true; // reject any sets with a seed item possible and no terrain setter to activate it | |
break; | |
} | |
} | |
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; | |
} | |
curSetMoveVariants.push(variantIndex); | |
} | |
if (reject) continue; | |
const set = { set: curSet, moveVariants: curSetMoveVariants }; | |
effectivePool.push(set); | |
} | |
if (!effectivePool.length) { | |
if (!teamData.forceResult) return null; | |
for (const curSet of setList) { | |
effectivePool.push({ set: curSet }); | |
} | |
} | |
// Sets have individual weight, choose one with weighted random selection | |
let setData = this.sample(effectivePool); // Init with unweighted random set as fallback | |
const total = effectivePool.reduce((a, b) => a + b.set.weight, 0); | |
const setRand = this.random(total); | |
let cur = 0; | |
for (const set of effectivePool) { | |
cur += set.set.weight; | |
if (cur > setRand) { | |
setData = set; // Bingo! | |
break; | |
} | |
} | |
const moves = []; | |
for (const [i, moveSlot] of setData.set.moves.entries()) { | |
moves.push(setData.moveVariants ? moveSlot[setData.moveVariants[i]] : this.sample(moveSlot)); | |
} | |
return { | |
name: setData.set.species || species.baseSpecies, | |
species: setData.set.species, | |
teraType: (this.sampleIfArray(setData.set.teraType)), | |
gender: setData.set.gender || species.gender || (this.randomChance(1, 2) ? "M" : "F"), | |
item: this.sampleIfArray(setData.set.item) || "", | |
ability: this.sampleIfArray(setData.set.ability), | |
shiny: this.randomChance(1, 1024), | |
level: 50, | |
happiness: 255, | |
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, | |
wantsTera: setData.set.wantsTera, | |
}; | |
} | |
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: {}, | |
wantsTeraCount: 0, | |
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].weight), | |
}; | |
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 species = this.dex.species.get(shuffledSpecies.pop()!.speciesName); | |
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; | |
if (set.wantsTera) { | |
if (!teamData.wantsTeraCount) teamData.wantsTeraCount = 0; | |
teamData.wantsTeraCount++; | |
} | |
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; | |
} | |
randomDraftFactoryMatchups: AnyObject = require("./draft-factory-matchups.json").matchups; | |
rdfMatchupIndex = -1; | |
rdfMatchupSide = -1; | |
randomDraftFactoryTeam(side: PlayerOptions): RandomTeamsTypes.RandomDraftFactorySet[] { | |
this.enforceNoDirectCustomBanlistChanges(); | |
if (this.rdfMatchupIndex === -1) this.rdfMatchupIndex = this.random(0, this.randomDraftFactoryMatchups.length); | |
if (this.rdfMatchupSide === -1) this.rdfMatchupSide = this.random(0, 2); | |
const matchup = this.randomDraftFactoryMatchups[this.rdfMatchupIndex]; | |
const team = Teams.unpack(matchup[this.rdfMatchupSide]); | |
if (!team) throw new Error(`Invalid team for draft factory matchup ${this.rdfMatchupIndex}`); | |
this.rdfMatchupSide = 1 - this.rdfMatchupSide; | |
return team.map(set => { | |
let species = this.dex.species.get(set.species); | |
if (species.battleOnly) { | |
if (typeof species.battleOnly !== 'string') { | |
throw new Error(`Invalid species ${species.name} for draft factory matchup ${this.rdfMatchupIndex} team ${this.rdfMatchupSide}`); | |
} | |
species = this.dex.species.get(species.battleOnly); | |
} | |
return { | |
name: species.baseSpecies, | |
species: species.name, | |
gender: set.gender, | |
moves: set.moves, | |
ability: set.ability, | |
evs: set.evs, | |
ivs: set.ivs, | |
item: set.item, | |
level: this.adjustLevel || set.level, | |
shiny: !!set.shiny, | |
nature: set.nature, | |
teraType: set.teraType, | |
teraCaptain: set.name === 'Tera Captain', | |
}; | |
}); | |
} | |
} | |
export default RandomTeams; | |