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