pokemon-showdown/data/random-battles/gen5/teams.ts
Guangcong Luo 78439b4a02
Update to ESLint 9 (#10926)
ESLint has a whole new config format, so I figure it's a good time to
make the config system saner.

- First, we no longer have separate eslint-no-types configs. Lint
  performance shouldn't be enough of a problem to justify the
  relevant maintenance complexity.

- Second, our base config should work out-of-the-box now. `npx eslint`
  will work as expected, without any CLI flags. You should still use
  `npm run lint` which adds the `--cached` flag for performance.

- Third, whatever updates I did fixed style linting, which apparently
  has been bugged for quite some time, considering all the obvious
  mixed-tabs-and-spaces issues I found in the upgrade.

Also here are some changes to our style rules. In particular:

- Curly brackets (for objects etc) now have spaces inside them. Sorry
  for the huge change. ESLint doesn't support our old style, and most
  projects use Prettier style, so we might as well match them in this way.
  See https://github.com/eslint-stylistic/eslint-stylistic/issues/415

- String + number concatenation is no longer allowed. We now
  consistently use template strings for this.
2025-02-25 20:03:46 -08:00

984 lines
37 KiB
TypeScript

import RandomGen6Teams from '../gen6/teams';
import type { PRNG } from '../../../sim';
import type { MoveCounter } from '../gen8/teams';
import { toID } from '../../../sim/dex';
// Moves that restore HP:
const RECOVERY_MOVES = [
'healorder', 'milkdrink', 'moonlight', 'morningsun', 'recover', 'roost', 'slackoff', 'softboiled', 'synthesis',
];
// Moves that boost Attack:
const PHYSICAL_SETUP = [
'bellydrum', 'bulkup', 'coil', 'curse', 'dragondance', 'honeclaws', 'howl', 'meditate', 'screech', 'swordsdance',
];
// Some moves that only boost Speed:
const SPEED_SETUP = [
'agility', 'autotomize', 'flamecharge', 'rockpolish',
];
// Conglomerate for ease of access
const SETUP = [
'acidarmor', 'agility', 'autotomize', 'bellydrum', 'bulkup', 'calmmind', 'coil', 'curse', 'dragondance', 'flamecharge',
'growth', 'honeclaws', 'howl', 'irondefense', 'meditate', 'nastyplot', 'quiverdance', 'raindance', 'rockpolish',
'shellsmash', 'shiftgear', 'sunnyday', 'swordsdance', 'tailglow', 'workup',
];
// Moves that shouldn't be the only STAB moves:
const NO_STAB = [
'aquajet', 'bulletpunch', 'chatter', 'clearsmog', 'dragontail', 'eruption', 'explosion', 'fakeout', 'flamecharge',
'futuresight', 'iceshard', 'icywind', 'incinerate', 'knockoff', 'machpunch', 'pluck', 'pursuit', 'quickattack',
'rapidspin', 'reversal', 'selfdestruct', 'shadowsneak', 'skyattack', 'skydrop', 'snarl', 'suckerpunch',
'uturn', 'vacuumwave', 'voltswitch', 'waterspout',
];
// Hazard-setting moves
const HAZARDS = [
'spikes', 'stealthrock', 'toxicspikes',
];
// Moves that switch the user out
const PIVOT_MOVES = [
'uturn', 'voltswitch',
];
// Moves that should be paired together when possible
const MOVE_PAIRS = [
['lightscreen', 'reflect'],
['sleeptalk', 'rest'],
['protect', 'wish'],
['leechseed', 'substitute'],
];
/** Pokemon who always want priority STAB, and are fine with it as its only STAB move of that type */
const PRIORITY_POKEMON = [
'bisharp', 'breloom', 'cacturne', 'dusknoir', 'honchkrow', 'scizor', 'shedinja', 'shiftry',
];
export class RandomGen5Teams extends RandomGen6Teams {
randomSets: { [species: string]: RandomTeamsTypes.RandomSpeciesData } = require('./sets.json');
constructor(format: string | Format, prng: PRNG | PRNGSeed | null) {
super(format, prng);
this.noStab = NO_STAB;
this.priorityPokemon = PRIORITY_POKEMON;
this.moveEnforcementCheckers = {
Bug: (movePool, moves, abilities, types, counter) => (
!counter.get('Bug') && (movePool.includes('megahorn') || abilities.includes('Tinted Lens'))
),
Dark: (movePool, moves, abilities, types, counter) => !counter.get('Dark'),
Dragon: (movePool, moves, abilities, types, counter) => !counter.get('Dragon'),
Electric: (movePool, moves, abilities, types, counter) => !counter.get('Electric'),
Fighting: (movePool, moves, abilities, types, counter) => !counter.get('Fighting'),
Fire: (movePool, moves, abilities, types, counter) => !counter.get('Fire'),
Flying: (movePool, moves, abilities, types, counter, species) => (
!counter.get('Flying') && !['aerodactyl', 'mantine', 'murkrow'].includes(species.id) &&
!movePool.includes('hiddenpowerflying')
),
Ghost: (movePool, moves, abilities, types, counter) => !counter.get('Ghost'),
Grass: (movePool, moves, abilities, types, counter, species) => (
!counter.get('Grass') && (species.baseStats.atk >= 100 || movePool.includes('leafstorm'))
),
Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'),
Ice: (movePool, moves, abilities, types, counter) => !counter.get('Ice'),
Poison: (movePool, moves, abilities, types, counter) => (
!counter.get('Poison') && (types.has('Grass') || types.has('Ground'))
),
Psychic: (movePool, moves, abilities, types, counter) => (
!counter.get('Psychic') && (types.has('Fighting') || movePool.includes('calmmind'))
),
Rock: (movePool, moves, abilities, types, counter, species) => (!counter.get('Rock') && species.baseStats.atk >= 80),
Steel: (movePool, moves, abilities, types, counter, species) => (
!counter.get('Steel') && ['aggron', 'metagross'].includes(species.id)
),
Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'),
};
// Nature Power is Earthquake this gen
this.cachedStatusMoves = this.dex.moves.all()
.filter(move => move.category === 'Status' && move.id !== 'naturepower')
.map(move => move.id);
}
cullMovePool(
types: string[],
moves: Set<string>,
abilities: string[],
counter: MoveCounter,
movePool: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
preferredType: string,
role: RandomTeamsTypes.Role,
): void {
// Pokemon cannot have multiple Hidden Powers in any circumstance
let hasHiddenPower = false;
for (const move of moves) {
if (move.startsWith('hiddenpower')) hasHiddenPower = true;
}
if (hasHiddenPower) {
let movePoolHasHiddenPower = true;
while (movePoolHasHiddenPower) {
movePoolHasHiddenPower = false;
for (const moveid of movePool) {
if (moveid.startsWith('hiddenpower')) {
this.fastPop(movePool, movePool.indexOf(moveid));
movePoolHasHiddenPower = true;
break;
}
}
}
}
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]));
}
}
}
// Team-based move culls
if (teamDetails.screens && 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 (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.rapidSpin) {
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('aromatherapy')) this.fastPop(movePool, movePool.indexOf('aromatherapy'));
if (movePool.includes('healbell')) this.fastPop(movePool, movePool.indexOf('healbell'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
// Develop additional move lists
const badWithSetup = ['healbell', 'pursuit', 'toxic'];
const statusMoves = this.cachedStatusMoves;
// 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, badWithSetup],
[PHYSICAL_SETUP, PHYSICAL_SETUP],
[['fakeout', 'uturn'], ['switcheroo', 'trick']],
['substitute', PIVOT_MOVES],
['rest', 'substitute'],
// These attacks are redundant with each other
['psychic', 'psyshock'],
[['scald', 'surf'], 'hydropump'],
[['bodyslam', 'return'], ['bodyslam', 'doubleedge']],
[['gigadrain', 'leafstorm'], ['leafstorm', 'petaldance', 'powerwhip']],
[['drainpunch', 'focusblast'], ['closecombat', 'highjumpkick', 'superpower']],
['payback', 'pursuit'],
// Assorted hardcodes go here:
// Zebstrika
['wildcharge', 'thunderbolt'],
// Manectric
['flamethrower', 'overheat'],
// Meganium
['leechseed', 'dragontail'],
// Volcarona and Heatran
[['fierydance', 'lavaplume'], 'fireblast'],
// Walrein
['encore', 'roar'],
// Lunatone
['moonlight', 'rockpolish'],
// Smeargle
['memento', 'whirlwind'],
// Seviper
['switcheroo', 'suckerpunch'],
// Jirachi
['bodyslam', 'healingwish'],
];
for (const pair of incompatiblePairs) this.incompatibleMoves(moves, movePool, pair[0], pair[1]);
if (species.id === 'dugtrio') this.incompatibleMoves(moves, movePool, statusMoves, 'memento');
const statusInflictingMoves = ['stunspore', 'thunderwave', 'toxic', 'willowisp', 'yawn'];
if (!abilities.includes('Prankster') && role !== 'Staller') {
this.incompatibleMoves(moves, movePool, statusInflictingMoves, statusInflictingMoves);
}
if (abilities.includes('Guts')) this.incompatibleMoves(moves, movePool, 'protect', 'swordsdance');
// Cull filler moves for otherwise fixed set Stealth Rock users
if (!teamDetails.stealthRock) {
if (species.id === 'registeel' && role === 'Staller') {
if (movePool.includes('thunderwave')) this.fastPop(movePool, movePool.indexOf('thunderwave'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (species.baseSpecies === 'Wormadam' && role === 'Staller') {
if (movePool.includes('suckerpunch')) this.fastPop(movePool, movePool.indexOf('suckerpunch'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
}
}
// Generate random moveset for a given species, role, preferred type.
randomMoveset(
types: string[],
abilities: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
movePool: string[],
preferredType: string,
role: RandomTeamsTypes.Role,
): Set<string> {
const moves = new Set<string>();
let counter = this.newQueryMoves(moves, species, preferredType, abilities);
this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead,
preferredType, role);
// If there are only four moves, add all moves and return early
if (movePool.length <= this.maxMoveCount) {
// Still need to ensure that multiple Hidden Powers are not added (if maxMoveCount is increased)
while (movePool.length) {
const moveid = this.sample(movePool);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
return moves;
}
const runEnforcementChecker = (checkerName: string) => {
if (!this.moveEnforcementCheckers[checkerName]) return false;
return this.moveEnforcementCheckers[checkerName](
movePool, moves, abilities, new Set(types), counter, species, teamDetails
);
};
// 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,
movePool, preferredType, 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,
movePool, preferredType, role);
}
// Enforce Seismic Toss and Spore
for (const moveid of ['seismictoss', 'spore']) {
if (movePool.includes(moveid)) {
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
}
// Enforce Thunder Wave on Prankster users
if (movePool.includes('thunderwave') && abilities.includes('Prankster')) {
counter = this.addMove('thunderwave', moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
// Enforce hazard removal on Bulky Support and Spinner if the team doesn't already have it
if (['Bulky Support', 'Spinner'].includes(role) && !teamDetails.rapidSpin) {
if (movePool.includes('rapidspin')) {
counter = this.addMove('rapidspin', moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
}
// Enforce STAB priority
if (['Bulky Attacker', 'Bulky Setup'].includes(role) || this.priorityPokemon.includes(species.id)) {
const priorityMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, preferredType);
if (types.includes(moveType) && move.priority > 0 && (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,
movePool, preferredType, 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, preferredType);
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,
movePool, preferredType, role);
}
}
// Enforce Preferred Type
if (!counter.get('preferred')) {
const stabMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, preferredType);
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && preferredType === moveType) {
stabMoves.push(moveid);
}
}
if (stabMoves.length) {
const moveid = this.sample(stabMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, 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, preferredType);
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,
movePool, preferredType, role);
} else {
// If they have no regular STAB move, enforce U-turn on Bug types.
if (movePool.includes('uturn') && types.includes('Bug')) {
counter = this.addMove('uturn', moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
}
}
// Enforce recovery
if (['Bulky Support', 'Bulky Attacker', 'Bulky Setup', 'Spinner', 'Staller'].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,
movePool, preferredType, role);
}
}
// Enforce Staller moves
if (role === 'Staller') {
const enforcedMoves = ['protect', 'toxic'];
for (const move of enforcedMoves) {
if (movePool.includes(move)) {
counter = this.addMove(move, moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
}
}
// Enforce setup
if (role.includes('Setup')) {
// 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,
movePool, preferredType, 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,
movePool, preferredType, role);
}
}
}
// Enforce a move not on the noSTAB list
if (!counter.damagingMoves.size && !(moves.has('uturn') && types.includes('Bug'))) {
// 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,
movePool, preferredType, role);
}
}
// Enforce coverage move
if (['Fast Attacker', 'Setup Sweeper', 'Bulky Attacker', 'Wallbreaker'].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, preferredType);
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,
movePool, preferredType, role);
}
}
}
// Choose remaining moves randomly from movepool and add them to moves list:
while (moves.size < this.maxMoveCount && movePool.length) {
const moveid = this.sample(movePool);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, 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,
movePool, preferredType, role);
}
if (moveid === pair[1] && movePool.includes(pair[0])) {
counter = this.addMove(pair[0], moves, types, abilities, teamDetails, species, isLead,
movePool, preferredType, role);
}
}
}
return moves;
}
shouldCullAbility(
ability: string,
types: Set<string>,
moves: Set<string>,
abilities: string[],
counter: MoveCounter,
movePool: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
preferredType: string,
role: RandomTeamsTypes.Role
): boolean {
switch (ability) {
case 'Chlorophyll': case 'Solar Power':
return !teamDetails.sun;
case 'Hydration': case 'Swift Swim':
return !teamDetails.rain;
case 'Iron Fist': case 'Sheer Force':
return !counter.get(toID(ability));
case 'Overgrow':
return !counter.get('Grass');
case 'Rock Head':
return !counter.get('recoil');
case 'Sand Force': case 'Sand Rush':
return !teamDetails.sand;
}
return false;
}
getAbility(
types: Set<string>,
moves: Set<string>,
abilities: string[],
counter: MoveCounter,
movePool: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
preferredType: string,
role: RandomTeamsTypes.Role,
): string {
if (abilities.length <= 1) return abilities[0];
// Hard-code abilities here
if (species.id === 'marowak' && counter.get('recoil')) return 'Rock Head';
if (species.id === 'kingler' && counter.get('sheerforce')) return 'Sheer Force';
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, movePool, teamDetails, species, preferredType, 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', '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,
preferredType: string,
role: RandomTeamsTypes.Role,
): string | undefined {
if (species.requiredItems) return this.sample(species.requiredItems);
if (species.id === 'farfetchd') return 'Stick';
if (species.id === 'latias' || species.id === 'latios') return 'Soul Dew';
if (species.id === 'marowak') return 'Thick Club';
if (species.id === 'pikachu') return 'Light Ball';
if (species.id === 'shedinja' || species.id === 'smeargle') return 'Focus Sash';
if (species.id === 'unown') return 'Choice Specs';
if (species.id === 'wobbuffet') return 'Custap Berry';
if (ability === 'Harvest') return 'Sitrus Berry';
if (species.id === 'ditto') return 'Choice Scarf';
if (species.id === 'exploud' && role === 'Bulky Attacker') return 'Choice Band';
if (ability === 'Poison Heal' || moves.has('facade')) return 'Toxic Orb';
if (ability === 'Speed Boost' && species.id !== 'ninjask') return 'Life Orb';
if (species.nfe) return 'Eviolite';
if (['healingwish', 'memento', 'switcheroo', 'trick'].some(m => moves.has(m))) {
if (
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 && role !== 'Wallbreaker' && !counter.get('priority')
) {
return 'Choice Scarf';
} else {
return (counter.get('Physical') > counter.get('Special')) ? 'Choice Band' : 'Choice Specs';
}
}
if (moves.has('bellydrum')) return 'Sitrus Berry';
if (moves.has('waterspout')) return 'Choice Scarf';
if (moves.has('shellsmash')) return 'White Herb';
if (moves.has('psychoshift')) return 'Flame Orb';
if (ability === 'Magic Guard') return moves.has('counter') ? 'Focus Sash' : 'Life Orb';
if (species.id === 'rampardos' && role === 'Fast Attacker') return 'Choice Scarf';
if (ability === 'Sheer Force' && counter.get('sheerforce')) return 'Life Orb';
if (moves.has('acrobatics')) return 'Flying Gem';
if (species.id === 'hitmonlee' && ability === 'Unburden') return moves.has('fakeout') ? 'Normal Gem' : 'Fighting Gem';
if (moves.has('lightscreen') && moves.has('reflect')) return 'Light Clay';
if (moves.has('rest') && !moves.has('sleeptalk') && !['Natural Cure', 'Shed Skin'].includes(ability)) {
return (moves.has('raindance') && ability === 'Hydration') ? 'Damp Rock' : 'Chesto Berry';
}
if (role === 'Staller') return 'Leftovers';
}
getItem(
ability: string,
types: string[],
moves: Set<string>,
counter: MoveCounter,
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
preferredType: string,
role: RandomTeamsTypes.Role,
): string {
const defensiveStatTotal = species.baseStats.hp + species.baseStats.def + species.baseStats.spd;
const scarfReqs = (
role !== 'Wallbreaker' &&
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 &&
!counter.get('priority') && !moves.has('pursuit')
);
if (
moves.has('pursuit') && moves.has('suckerpunch') && counter.get('Dark') &&
(!this.priorityPokemon.includes(species.id) || counter.get('Dark') >= 2)
) return 'Black Glasses';
if (counter.get('Special') === 4) {
return (
scarfReqs && species.baseStats.spa >= 90 && this.randomChance(1, 2)
) ? 'Choice Scarf' : 'Choice Specs';
}
if (counter.get('Special') === 3 && moves.has('uturn')) return 'Choice Specs';
if (counter.get('Physical') === 4 && species.id !== 'jirachi' && species.id !== 'spinda' &&
['dragontail', 'fakeout', 'rapidspin'].every(m => !moves.has(m))
) {
return (
scarfReqs && (species.baseStats.atk >= 100 || ability === 'Pure Power' || ability === 'Huge Power') &&
this.randomChance(1, 2)
) ? 'Choice Scarf' : 'Choice Band';
}
if (ability === 'Sturdy' && moves.has('explosion')) return 'Custap Berry';
if (types.includes('Normal') && moves.has('fakeout') && !!counter.get('Normal')) return 'Silk Scarf';
if (species.id === 'palkia') return 'Lustrous Orb';
if (moves.has('outrage') && counter.get('setup')) return 'Lum Berry';
if (
(ability === 'Rough Skin') || (species.id !== 'hooh' && role !== 'Wallbreaker' &&
ability === 'Regenerator' && species.baseStats.hp + species.baseStats.def >= 180 && this.randomChance(1, 2))
) return 'Rocky Helmet';
if (['protect', 'substitute'].some(m => moves.has(m))) return 'Leftovers';
if (
this.dex.getEffectiveness('Ground', species) >= 2 &&
ability !== 'Levitate'
) {
return 'Air Balloon';
}
if (
role === 'Fast Support' && isLead && defensiveStatTotal < 255 && !counter.get('recovery') &&
(counter.get('hazards') || counter.get('setup')) && (!counter.get('recoil') || ability === 'Rock Head')
) return 'Focus Sash';
// Default Items
if (role === 'Fast Support') {
return (
counter.get('Physical') + counter.get('Special') >= 3 &&
['rapidspin', 'uturn', 'voltswitch'].every(m => !moves.has(m)) &&
this.dex.getEffectiveness('Rock', species) < 2
) ? 'Life Orb' : 'Leftovers';
}
// noStab moves that should reject Expert Belt
const noExpertBeltMoves = (
this.noStab.filter(moveid => ['Dragon', 'Normal', 'Poison'].includes(this.dex.moves.get(moveid).type))
);
const expertBeltReqs = (
!counter.get('Dragon') && !counter.get('Normal') && !counter.get('Poison') &&
noExpertBeltMoves.every(m => !moves.has(m))
);
if (
!counter.get('Status') && expertBeltReqs &&
(moves.has('uturn') || moves.has('voltswitch') || role === 'Fast Attacker')
) return 'Expert Belt';
if (
['Fast Attacker', 'Setup Sweeper', 'Wallbreaker'].some(m => role === m) &&
this.dex.getEffectiveness('Rock', species) < 2 && ability !== 'Sturdy'
) return 'Life Orb';
return 'Leftovers';
}
randomSet(
species: string | Species,
teamDetails: RandomTeamsTypes.TeamDetails = {},
isLead = false
): RandomTeamsTypes.RandomSet {
species = this.dex.species.get(species);
const forme = this.getForme(species);
const sets = this.randomSets[species.id]["sets"];
const possibleSets = [];
// Check if the Pokemon has a Spinner set
let canSpinner = false;
for (const set of sets) {
if (!teamDetails.rapidSpin && set.role === 'Spinner') canSpinner = true;
}
for (const set of sets) {
// Prevent Spinner if the team already has removal
if (teamDetails.rapidSpin && set.role === 'Spinner') continue;
// Enforce Spinner if the team does not have removal
if (canSpinner && set.role !== 'Spinner') continue;
possibleSets.push(set);
}
const set = this.sampleIfArray(possibleSets);
const role = set.role;
const movePool: string[] = Array.from(set.movepool);
const preferredTypes = set.preferredTypes;
const preferredType = this.sampleIfArray(preferredTypes) || '';
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, movePool,
preferredType, role);
const counter = this.newQueryMoves(moves, species, preferredType, abilities);
// Get ability
ability = this.getAbility(new Set(types), moves, abilities, counter, movePool, teamDetails, species,
preferredType, role);
// Get items
item = this.getPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, preferredType, role);
if (item === undefined) {
item = this.getItem(ability, types, moves, counter, teamDetails, species, isLead, preferredType, role);
}
// For Trick / Switcheroo
if (item === 'Leftovers' && types.includes('Poison')) {
item = 'Black Sludge';
}
const level = this.getLevel(species);
// We use a special variable to track Hidden Power
// so that we can check for all Hidden Powers at once
let hasHiddenPower = false;
for (const move of moves) {
if (move.startsWith('hiddenpower')) hasHiddenPower = true;
}
if (hasHiddenPower) {
let hpType;
for (const move of moves) {
if (move.startsWith('hiddenpower')) hpType = move.substr(11);
}
if (!hpType) throw new Error(`hasHiddenPower is true, but no Hidden Power move was found.`);
const HPivs = this.dex.types.get(hpType).HPivs;
let iv: StatID;
for (iv in HPivs) {
ivs[iv] = HPivs[iv]!;
}
}
// Prepare optimal HP
const srImmunity = ability === 'Magic Guard';
const srWeakness = srImmunity ? 0 : this.dex.getEffectiveness('Rock', species);
while (evs.hp > 1) {
const hp = Math.floor(Math.floor(2 * species.baseStats.hp + ivs.hp + Math.floor(evs.hp / 4) + 100) * level / 100 + 10);
if (moves.has('substitute') && !['Black Sludge', 'Leftovers'].includes(item)) {
if (item === 'Sitrus Berry') {
// Two Substitutes should activate Sitrus Berry
if (hp % 4 === 0) break;
} else {
// Should be able to use Substitute four times from full HP without fainting
if (hp % 4 > 0) break;
}
} else if (moves.has('bellydrum') && item === 'Sitrus Berry') {
// Belly Drum should activate Sitrus Berry
if (hp % 2 === 0) break;
} else if (['highjumpkick', 'jumpkick'].some(m => moves.has(m))) {
// Crash damage move users want an odd HP to survive two misses
if (hp % 2 > 0) break;
} else {
// Maximize number of Stealth Rock switch-ins
if (srWeakness <= 0 || ability === 'Regenerator') break;
if (srWeakness === 1 && ['Black Sludge', '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, including if Foul Play is its only physical attack
if (
(!counter.get('Physical') || (counter.get('Physical') <= 1 && (moves.has('foulplay') || moves.has('rapidspin')))) &&
!moves.has('transform')
) {
evs.atk = 0;
ivs.atk = hasHiddenPower ? (ivs.atk || 31) - 28 : 0;
}
if (['gyroball', 'metalburst', 'trickroom'].some(m => moves.has(m))) {
evs.spe = 0;
ivs.spe = hasHiddenPower ? (ivs.spe || 31) - 28 : 0;
}
// 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.gender,
shiny: this.randomChance(1, 1024),
level,
moves: shuffledMoves,
ability,
evs,
ivs,
item,
role,
};
}
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 typePool = this.dex.types.names();
const type = this.forceMonotype || this.sample(typePool);
const baseFormes: { [k: string]: number } = {};
const typeCount: { [k: string]: number } = {};
const typeWeaknesses: { [k: string]: number } = {};
const typeDoubleWeaknesses: { [k: string]: number } = {};
const teamDetails: RandomTeamsTypes.TeamDetails = {};
let numMaxLevelPokemon = 0;
const pokemonList = Object.keys(this.randomSets);
const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, pokemonList);
while (baseSpeciesPool.length && pokemon.length < this.maxTeamSize) {
const baseSpecies = this.sampleNoReplace(baseSpeciesPool);
const species = this.dex.species.get(this.sample(pokemonPool[baseSpecies]));
if (!species.exists) continue;
// Limit to one of each species (Species Clause)
if (baseFormes[species.baseSpecies]) continue;
// Illusion shouldn't be in the last slot
if (species.name === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue;
// Prevent Shedinja from generating after Sandstorm/Hail setters
if (species.name === 'Shedinja' && (teamDetails.sand || teamDetails.hail)) continue;
// Dynamically scale limits for different team sizes. The default and minimum value is 1.
const limitFactor = Math.round(this.maxTeamSize / 6) || 1;
const types = species.types;
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 as a Fire weakness
if (this.dex.getEffectiveness('Fire', species) === 0 && Object.values(species.abilities).includes('Dry Skin')) {
if (!typeWeaknesses['Fire']) typeWeaknesses['Fire'] = 0;
if (typeWeaknesses['Fire'] >= 3 * limitFactor) continue;
}
// Limit one level 100 Pokemon
if (!this.adjustLevel && (this.getLevel(species) === 100) && numMaxLevelPokemon >= limitFactor) {
continue;
}
}
const set = this.randomSet(species, teamDetails, pokemon.length === 0);
// Okay, the set passes, add it to our team
pokemon.push(set);
// Don't bother tracking details for the last Pokemon
if (pokemon.length === this.maxTeamSize) break;
// Now that our Pokemon has passed all checks, we can increment our counters
baseFormes[species.baseSpecies] = 1;
// Increment type counters
for (const typeName of types) {
if (typeName in typeCount) {
typeCount[typeName]++;
} else {
typeCount[typeName] = 1;
}
}
// 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 as a Fire weakness
if (set.ability === 'Dry Skin' && this.dex.getEffectiveness('Fire', species) === 0) typeWeaknesses['Fire']++;
// Increment level 100 counter
if (set.level === 100) numMaxLevelPokemon++;
// Team details
if (set.ability === 'Snow Warning' || set.moves.includes('hail')) teamDetails.hail = 1;
if (set.ability === 'Drizzle' || set.moves.includes('raindance')) teamDetails.rain = 1;
if (set.ability === 'Sand Stream') teamDetails.sand = 1;
if (set.ability === 'Drought' || set.moves.includes('sunnyday')) teamDetails.sun = 1;
if (set.moves.includes('aromatherapy') || set.moves.includes('healbell')) teamDetails.statusCure = 1;
if (set.moves.includes('spikes')) teamDetails.spikes = (teamDetails.spikes || 0) + 1;
if (set.moves.includes('stealthrock')) teamDetails.stealthRock = 1;
if (set.moves.includes('toxicspikes')) teamDetails.toxicSpikes = 1;
if (set.moves.includes('rapidspin')) teamDetails.rapidSpin = 1;
if (set.moves.includes('reflect') && set.moves.includes('lightscreen')) teamDetails.screens = 1;
}
if (pokemon.length < this.maxTeamSize && pokemon.length < 12) {
throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`);
}
return pokemon;
}
}
export default RandomGen5Teams;