pokemon-showdown/data/mods/gen2/random-teams.ts
Annika 4d73b897b6 Random Battles: Support more value rules
This adds support for `Max Move Count` and `Adjust Level`
2022-03-30 21:56:43 -07:00

316 lines
11 KiB
TypeScript

import RandomGen3Teams from '../gen3/random-teams';
import {PRNG, PRNGSeed} from '../../../sim/prng';
import type {MoveCounter} from '../../random-teams';
export class RandomGen2Teams extends RandomGen3Teams {
constructor(format: string | Format, prng: PRNG | PRNGSeed | null) {
super(format, prng);
this.moveEnforcementCheckers = {
Electric: (movePool, moves, abilities, types, counter) => !counter.get('Electric'),
Fire: (movePool, moves, abilities, types, counter) => !counter.get('Fire'),
Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'),
Ice: (movePool, moves, abilities, types, counter) => !counter.get('Ice'),
Normal: (movePool, moves, abilities, types, counter) => !counter.get('Normal') && counter.setupType === 'Physical',
Psychic: (movePool, moves, abilities, types, counter) => !counter.get('Psychic') && types.has('Grass'),
Rock: (movePool, moves, abilities, types, counter, species) => !counter.get('Rock') && species.baseStats.atk > 60,
Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'),
};
}
shouldCullMove(
move: Move,
types: Set<string>,
moves: Set<string>,
abilities = {},
counter: MoveCounter,
movePool: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
): {cull: boolean, isSetup?: boolean} {
const restTalk = moves.has('rest') && moves.has('sleeptalk');
switch (move.id) {
// Set up once and only if we have the moves for it
case 'bellydrum': case 'curse': case 'meditate': case 'screech': case 'swordsdance':
return {
cull: (
(counter.setupType !== 'Physical' || counter.get('physicalsetup') > 1) ||
(!counter.get('Physical') || counter.damagingMoves.size < 2 && !moves.has('batonpass') && !moves.has('sleeptalk'))
),
isSetup: true,
};
// Not very useful without their supporting moves
case 'batonpass':
return {cull: !counter.setupType && !counter.get('speedsetup') && !moves.has('meanlook')};
case 'meanlook':
return {cull: movePool.includes('perishsong')};
case 'nightmare':
return {cull: !moves.has('lovelykiss') && !moves.has('sleeppowder')};
case 'swagger':
return {cull: !moves.has('substitute')};
// Bad after setup
case 'charm': case 'counter':
return {cull: !!counter.setupType};
case 'haze':
return {cull: !!counter.setupType || restTalk};
case 'reflect': case 'lightscreen':
return {cull: !!counter.setupType || moves.has('rest')};
// Ineffective to have both
case 'doubleedge':
return {cull: moves.has('bodyslam') || moves.has('return')};
case 'explosion':
return {cull: moves.has('softboiled')};
case 'extremespeed':
return {cull: moves.has('bodyslam') || restTalk};
case 'hyperbeam':
return {cull: moves.has('rockslide')};
case 'quickattack': case 'selfdestruct':
return {cull: moves.has('rest')};
case 'rapidspin':
return {cull: !!teamDetails.rapidSpin || moves.has('sleeptalk')};
case 'return':
return {cull: moves.has('bodyslam')};
case 'surf':
return {cull: moves.has('hydropump')};
case 'thunder':
return {cull: moves.has('thunderbolt')};
case 'gigadrain':
return {cull: moves.has('razorleaf') || moves.has('swordsdance') && movePool.includes('sludgebomb')};
case 'icebeam':
return {cull: moves.has('dragonbreath')};
case 'seismictoss':
return {cull: moves.has('rest') || moves.has('sleeptalk')};
case 'destinybond':
return {cull: moves.has('explosion')};
case 'pursuit':
return {cull: moves.has('crunch') && moves.has('solarbeam')};
case 'thief':
return {cull: moves.has('rest') || moves.has('substitute')};
case 'irontail':
return {cull: types.has('Ground') && movePool.includes('earthquake')};
// Status and illegal move rejections
case 'confuseray': case 'roar': case 'whirlwind':
return {cull: restTalk};
case 'encore':
return {cull: moves.has('bodyslam') || moves.has('surf') || restTalk};
case 'lovelykiss':
return {cull: ['healbell', 'moonlight', 'morningsun'].some(m => moves.has(m)) || restTalk};
case 'sleeptalk':
return {cull: moves.has('curse') && counter.get('stab') >= 2};
case 'softboiled':
return {cull: movePool.includes('swordsdance')};
case 'spikes':
return {cull: !!teamDetails.spikes || types.has('Ice') && moves.has('rapidspin')};
case 'substitute':
return {cull: moves.has('agility') || moves.has('rest')};
case 'synthesis':
return {cull: moves.has('explosion')};
case 'thunderwave':
return {cull: moves.has('thunder') || moves.has('toxic')};
}
return {cull: false};
}
getItem(
ability: string,
types: Set<string>,
moves: Set<string>,
counter: MoveCounter,
species: Species,
) {
// First, the high-priority items
if (species.name === 'Ditto') return this.sample(['Metal Powder', 'Quick Claw']);
if (species.name === 'Farfetch\u2019d') return 'Stick';
if (species.name === 'Marowak') return 'Thick Club';
if (species.name === 'Pikachu') return 'Light Ball';
if (species.name === 'Unown') return 'Twisted Spoon';
if (moves.has('thief')) return '';
// Medium priority
if (moves.has('rest') && !moves.has('sleeptalk')) return 'Mint Berry';
if (
(moves.has('bellydrum') || moves.has('swordsdance')) &&
species.baseStats.spe >= 60 && !types.has('Ground') &&
!moves.has('sleeptalk') && !moves.has('substitute') &&
this.randomChance(1, 2)
) {
return 'Miracle Berry';
}
// Default to Leftovers
return 'Leftovers';
}
randomSet(species: string | Species, teamDetails: RandomTeamsTypes.TeamDetails = {}): RandomTeamsTypes.RandomSet {
species = this.dex.species.get(species);
const movePool = (species.randomBattleMoves || Object.keys(this.dex.species.getLearnset(species.id)!)).slice();
const rejectedPool: string[] = [];
const moves = new Set<string>();
let ivs = {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30};
let availableHP = 0;
for (const setMoveid of movePool) {
if (setMoveid.startsWith('hiddenpower')) availableHP++;
}
const types = new Set(species.types);
let counter;
// We use a special variable to track Hidden Power
// so that we can check for all Hidden Powers at once
let hasHiddenPower = false;
do {
// Choose next 4 moves from learnset/viable moves and add them to moves list:
while (moves.size < this.maxMoveCount && movePool.length) {
const moveid = this.sampleNoReplace(movePool);
if (moveid.startsWith('hiddenpower')) {
availableHP--;
if (hasHiddenPower) continue;
hasHiddenPower = true;
}
moves.add(moveid);
}
while (moves.size < this.maxMoveCount && rejectedPool.length) {
const moveid = this.sampleNoReplace(rejectedPool);
if (moveid.startsWith('hiddenpower')) {
if (hasHiddenPower) continue;
hasHiddenPower = true;
}
moves.add(moveid);
}
counter = this.queryMoves(moves, species.types, new Set(), movePool);
// Iterate through the moves again, this time to cull them:
for (const moveid of moves) {
const move = this.dex.moves.get(moveid);
let {cull, isSetup} = this.shouldCullMove(move, types, moves, {}, counter, movePool, teamDetails);
// This move doesn't satisfy our setup requirements:
if (counter.setupType === 'Physical' && move.category === 'Special' && !counter.get('Physical')) {
cull = true;
}
// Reject Status, non-STAB, or low basepower moves
const moveIsRejectable = (
(move.category !== 'Status' || !move.flags.heal) &&
// These moves cannot be rejected in favor of a forced move
!['batonpass', 'sleeptalk', 'spikes', 'sunnyday'].includes(move.id) &&
(move.category === 'Status' || !types.has(move.type) || (move.basePower && move.basePower < 40))
);
if (!cull && !isSetup && moveIsRejectable && (counter.setupType || !move.stallingMove)) {
// There may be more important moves that this Pokemon needs
if (
// Pokemon should usually have at least one STAB move
(
!counter.get('stab') &&
!counter.get('damage') &&
!types.has('Ghost') &&
counter.get('physicalpool') + counter.get('specialpool') > 0
) || (movePool.includes('megahorn') || (movePool.includes('softboiled') && moves.has('present'))) ||
// Rest + Sleep Talk should be selected together
((moves.has('rest') && movePool.includes('sleeptalk')) || (moves.has('sleeptalk') && movePool.includes('rest'))) ||
// Sunny Day + Solar Beam should be selected together
(moves.has('sunnyday') && movePool.includes('solarbeam') ||
(moves.has('solarbeam') && movePool.includes('sunnyday'))) ||
['milkdrink', 'recover', 'spore'].some(m => movePool.includes(m))
) {
cull = true;
} else {
// Pokemon should have moves that benefit their typing
for (const type of types) {
if (this.moveEnforcementCheckers[type]?.(movePool, moves, new Set(), types, counter, species, teamDetails)) cull = true;
}
}
}
// Remove rejected moves from the move list
if (
cull &&
(movePool.length - availableHP || availableHP && (move.id === 'hiddenpower' || !hasHiddenPower))
) {
if (move.category !== 'Status' && !move.damage && (move.id !== 'hiddenpower' || !availableHP)) {
rejectedPool.push(moveid);
}
moves.delete(moveid);
if (moveid.startsWith('hiddenpower')) hasHiddenPower = false;
break;
}
if (cull && rejectedPool.length) {
moves.delete(moveid);
if (moveid.startsWith('hiddenpower')) hasHiddenPower = false;
break;
}
}
} while (moves.size < this.maxMoveCount && (movePool.length || rejectedPool.length));
// Adjust IVs for Hidden Power
for (const setMoveid of moves) {
if (!setMoveid.startsWith('hiddenpower')) continue;
const hpType = setMoveid.substr(11, setMoveid.length);
const hpIVs: {[k: string]: Partial<typeof ivs>} = {
dragon: {def: 28},
ice: {def: 26},
psychic: {def: 24},
electric: {atk: 28},
grass: {atk: 28, def: 28},
water: {atk: 28, def: 26},
fire: {atk: 28, def: 24},
steel: {atk: 26},
ghost: {atk: 26, def: 28},
bug: {atk: 26, def: 26},
rock: {atk: 26, def: 24},
ground: {atk: 24},
poison: {atk: 24, def: 28},
flying: {atk: 24, def: 26},
fighting: {atk: 24, def: 24},
};
if (hpIVs[hpType]) {
ivs = {...ivs, ...hpIVs[hpType]};
}
if (ivs.atk === 28 || ivs.atk === 24) ivs.hp = 14;
if (ivs.def === 28 || ivs.def === 24) ivs.hp -= 8;
}
const levelScale: {[k: string]: number} = {
NU: 73,
NUBL: 71,
UU: 69,
UUBL: 67,
OU: 65,
Uber: 61,
};
const customScale: {[k: string]: number} = {
Ditto: 83, Unown: 87, Wobbuffet: 83,
};
const level = this.adjustLevel || customScale[species.name] || levelScale[species.tier] || 80;
return {
name: species.name,
species: species.name,
moves: Array.from(moves),
ability: 'No Ability',
evs: {hp: 255, atk: 255, def: 255, spa: 255, spd: 255, spe: 255},
ivs,
item: this.getItem('None', types, moves, counter, species),
level,
// No shiny chance because Gen 2 shinies have bad IVs
shiny: false,
gender: species.gender ? species.gender : 'M',
};
}
}
export default RandomGen2Teams;