/** * Team Validator * Pokemon Showdown - http://pokemonshowdown.com/ * * Handles team validation, and specifically learnset checking. * * @license MIT */ import { Dex, toID } from './dex'; import type { MoveSource } from './dex-species'; import { Utils } from '../lib/utils'; import { Tags } from '../data/tags'; import { Teams } from './teams'; import { PRNG } from './prng'; import { type RuleTable } from './dex-formats'; /** * Describes a possible way to get a pokemon. Is not exhaustive! * sourcesBefore covers all sources that do not have exclusive * moves (like catching wild pokemon). * * First character is a generation number, 1-8. * Second character is a source ID, one of: * * - E = egg, 3rd char+ is the father in gen 2-5, empty in gen 6-7 * because egg moves aren't restricted to fathers anymore * - S = event, 3rd char+ is the index in .eventData * - D = Dream World, only 5D is valid * - V = Virtual Console or Let's Go transfer, only 7V/8V is valid * * Designed to match MoveSource where possible. */ export type PokemonSource = string; /** * Represents a set of possible ways to get a Pokémon with a given * set. * * `new PokemonSources()` creates an empty set; * `new PokemonSources(dex.gen)` allows all Pokemon. * * The set mainly stored as an Array `sources`, but for sets that * could be sourced from anywhere (for instance, TM moves), we * instead just set `sourcesBefore` to a number meaning "any * source at or before this gen is possible." * * In other words, this variable represents the set of all * sources in `sources`, union all sources at or before * gen `sourcesBefore`. */ export class PokemonSources { /** * A set of specific possible PokemonSources; implemented as * an Array rather than a Set for perf reasons. */ sources: PokemonSource[]; /** * if nonzero: the set also contains all possible sources from * this gen and earlier. */ sourcesBefore: number; /** * the set requires sources from this gen or later * this should be unchanged from the format's minimum past gen * (3 in modern games, 6 if pentagon is required, etc) */ sourcesAfter: number; isHidden: boolean | null; /** * `limitedEggMoves` is a list of moves that can only be obtained from an * egg with another father in gen 2-5. If there are multiple such moves, * potential fathers need to be checked to see if they can actually * learn the move combination in question. * * `null` = the current move is definitely not a limited egg move * * `undefined` = the current move may or may not be a limited egg move */ limitedEggMoves?: ID[] | null; /** * Moves that should be in limitedEggMoves that would otherwise be skipped * because they can be learned universally in a past generation */ possiblyLimitedEggMoves?: ID[] | null; /** * Moves that should be in limitedEggMoves that would otherwise be skipped * because they can be learned via Gen 1-2 tradeback */ tradebackLimitedEggMoves?: ID[] | null; /** * Tracks level up egg moves for female-only Pokemon */ levelUpEggMoves?: ID[] | null; /** * Moves that can be learned via Pomeg glitch and does not require a * particular parent to learn */ pomegEggMoves?: ID[] | null; /** * Event egg source that may be used with the Pomeg glitch * * `null` = definitely not an event egg that can be used with the Pomeg glitch */ pomegEventEgg?: string | null; /** * For event-only Pokemon that do not have a minimum source gen identified by its moves */ eventOnlyMinSourceGen?: number; /** * A list of movepools, identified by gen and species, which moves can be pulled from. * Used to deal with compatibility issues for prevo/evo-exclusive moves */ learnsetDomain?: string[] | null; /** * Some Pokemon evolve by having a move in their learnset (like Piloswine * with Ancient Power). These can only carry three other moves from their * prevo, because the fourth move must be the evo move. This restriction * doesn't apply to gen 6+ eggs, which can get around the restriction with * the relearner. */ moveEvoCarryCount: number; babyOnly?: string; sketchMove?: string; dreamWorldMoveCount: number; hm?: string; isFromPokemonGo?: boolean; pokemonGoSource?: string; restrictiveMoves?: string[]; /** Obscure learn methods */ restrictedMove?: ID; constructor(sourcesBefore = 0, sourcesAfter = 0) { this.sources = []; this.sourcesBefore = sourcesBefore; this.sourcesAfter = sourcesAfter; this.isHidden = null; this.limitedEggMoves = undefined; this.moveEvoCarryCount = 0; this.dreamWorldMoveCount = 0; } size() { if (this.sourcesBefore) return Infinity; return this.sources.length; } add(source: PokemonSource, limitedEggMove?: ID | null) { if (this.sources[this.sources.length - 1] !== source) this.sources.push(source); if (limitedEggMove) { if (source.substr(0, 3) === '1ET') { this.tradebackLimitedEggMoves = [limitedEggMove]; } } if (limitedEggMove && this.limitedEggMoves !== null) { this.limitedEggMoves = [limitedEggMove]; } else if (limitedEggMove === null) { this.limitedEggMoves = null; } } addGen(sourceGen: number) { this.sourcesBefore = Math.max(this.sourcesBefore, sourceGen); this.limitedEggMoves = null; } minSourceGen() { if (this.eventOnlyMinSourceGen) return this.eventOnlyMinSourceGen; if (this.sourcesBefore) return this.sourcesAfter || 1; let min = 10; for (const source of this.sources) { const sourceGen = parseInt(source.charAt(0)); if (sourceGen < min) min = sourceGen; } if (min === 10) return 0; return min; } maxSourceGen() { let max = this.sourcesBefore; for (const source of this.sources) { const sourceGen = parseInt(source.charAt(0)); if (sourceGen > max) max = sourceGen; } return max; } intersectWith(other: PokemonSources) { if (this.pomegEventEgg && other.pomegEggMoves) { const newSources = []; for (const source of other.sources) { newSources.push(source.substr(0, 2) === '3E' ? this.pomegEventEgg : source); } other.sources = newSources; } else if (other.pomegEventEgg && this.pomegEventEgg !== null) { const newSources = []; for (const source of this.sources) { newSources.push(source.substr(0, 2) === '3E' ? other.pomegEventEgg : source); } this.sources = newSources; this.pomegEventEgg = other.pomegEventEgg; } else if (!other.pomegEggMoves && !other.sourcesBefore) { this.pomegEventEgg = null; } if (other.sourcesBefore || this.sourcesBefore) { // having sourcesBefore is the equivalent of having everything before that gen // in sources, so we fill the other array in preparation for intersection if (other.sourcesBefore > this.sourcesBefore) { for (const source of this.sources) { const sourceGen = parseInt(source.charAt(0)); if (sourceGen <= other.sourcesBefore) { other.sources.push(source); } } } else if (this.sourcesBefore > other.sourcesBefore) { for (const source of other.sources) { const sourceGen = parseInt(source.charAt(0)); if (sourceGen <= this.sourcesBefore) { this.sources.push(source); } } } this.sourcesBefore = Math.min(other.sourcesBefore, this.sourcesBefore); } if (this.sources.length) { if (other.sources.length) { const sourcesSet = new Set(other.sources); const intersectSources = this.sources.filter(source => sourcesSet.has(source)); this.sources = intersectSources; } else { this.sources = []; } } if (other.restrictedMove && other.restrictedMove !== this.restrictedMove) { if (this.restrictedMove) { // incompatible this.sources = []; this.sourcesBefore = 0; } else { this.restrictedMove = other.restrictedMove; } } if (other.limitedEggMoves) { if (!this.limitedEggMoves) { this.limitedEggMoves = other.limitedEggMoves; } else { this.limitedEggMoves.push(...other.limitedEggMoves); } } if (other.possiblyLimitedEggMoves) { if (!this.possiblyLimitedEggMoves) { this.possiblyLimitedEggMoves = other.possiblyLimitedEggMoves; } else { this.possiblyLimitedEggMoves.push(...other.possiblyLimitedEggMoves); } } if (other.tradebackLimitedEggMoves) { if (!this.tradebackLimitedEggMoves) { this.tradebackLimitedEggMoves = other.tradebackLimitedEggMoves; } else { this.tradebackLimitedEggMoves.push(...other.tradebackLimitedEggMoves); } } if (other.levelUpEggMoves) { if (!this.levelUpEggMoves) { this.levelUpEggMoves = other.levelUpEggMoves; } else { this.levelUpEggMoves.push(...other.levelUpEggMoves); } } if (other.pomegEggMoves) { if (!this.pomegEggMoves) { this.pomegEggMoves = other.pomegEggMoves; } else { this.pomegEggMoves.push(...other.pomegEggMoves); } } if (other.learnsetDomain) { if (!this.learnsetDomain) { this.learnsetDomain = other.learnsetDomain; } else { this.learnsetDomain.filter(source => other.learnsetDomain?.includes(source)); } } if (this.possiblyLimitedEggMoves && !this.sourcesBefore) { const eggSources = this.sources.filter(source => source.charAt(1) === 'E'); let minEggGen = parseInt(eggSources[0]); for (const source of eggSources) { minEggGen = Math.min(minEggGen, parseInt(source.charAt(0))); } if (minEggGen) { for (const eggMoveAndGen of this.possiblyLimitedEggMoves) { if (!this.limitedEggMoves) this.limitedEggMoves = []; if (parseInt(eggMoveAndGen.charAt(0)) < minEggGen) { const eggMove = toID(eggMoveAndGen.substr(1)); if (!this.limitedEggMoves.includes(eggMove)) this.limitedEggMoves.push(eggMove); } } } } let eggTradebackLegal = false; for (const source of this.sources) { if (source.substr(0, 3) === '1ET') { eggTradebackLegal = true; break; } } if (!eggTradebackLegal && this.tradebackLimitedEggMoves) { for (const eggMove of this.tradebackLimitedEggMoves) { if (!this.limitedEggMoves) this.limitedEggMoves = []; if (!this.limitedEggMoves.includes(eggMove)) this.limitedEggMoves.push(eggMove); } } this.moveEvoCarryCount += other.moveEvoCarryCount; this.dreamWorldMoveCount += other.dreamWorldMoveCount; if (other.sourcesAfter > this.sourcesAfter) this.sourcesAfter = other.sourcesAfter; if (other.isHidden) this.isHidden = true; } } export class TeamValidator { readonly format: Format; readonly dex: ModdedDex; readonly gen: number; readonly ruleTable: RuleTable; readonly minSourceGen: number; readonly toID: (str: any) => ID; constructor(format: string | Format, dex = Dex) { this.format = dex.formats.get(format); if (this.format.effectType !== 'Format') { throw new Error(`format should be a 'Format', but was a '${this.format.effectType}'`); } this.dex = dex.forFormat(this.format); this.gen = this.dex.gen; this.ruleTable = this.dex.formats.getRuleTable(this.format); this.minSourceGen = this.ruleTable.minSourceGen; this.toID = toID; } validateTeam( team: PokemonSet[] | null, options: { removeNicknames?: boolean, skipSets?: { [name: string]: { [key: string]: boolean } }, } = {} ): string[] | null { if (team && this.format.validateTeam) { return this.format.validateTeam.call(this, team, options) || null; } return this.baseValidateTeam(team, options); } baseValidateTeam( team: PokemonSet[] | null, options: { removeNicknames?: boolean, skipSets?: { [name: string]: { [key: string]: boolean } }, } = {} ): string[] | null { const format = this.format; const dex = this.dex; let problems: string[] = []; const ruleTable = this.ruleTable; if (format.team) { if (team) { return [ `This format doesn't let you use your own team.`, `If you're not using a custom client, please report this as a bug. If you are, remember to use \`/utm null\` before starting a game in this format.`, ]; } const testTeamSeed = PRNG.generateSeed(); try { const testTeamGenerator = Teams.getGenerator(format, testTeamSeed); testTeamGenerator.getTeam(options); // Throws error if generation fails } catch (e) { return [ `${format.name}'s team generator (${format.team}) failed using these rules and seed (${testTeamSeed}):-`, `${e}`, ]; } return null; } if (!team) { return [ `This format requires you to use your own team.`, `If you're not using a custom client, please report this as a bug.`, ]; } if (!Array.isArray(team)) { throw new Error(`Invalid team data`); } if (team.length < ruleTable.minTeamSize) { problems.push(`You must bring at least ${ruleTable.minTeamSize} Pok\u00E9mon (your team has ${team.length}).`); } if (team.length > ruleTable.maxTeamSize) { return [`You may only bring up to ${ruleTable.maxTeamSize} Pok\u00E9mon (your team has ${team.length}).`]; } // A limit is imposed here to prevent too much engine strain or // too much layout deformation - to be exact, this is the limit // allowed in Custom Game. if (team.length > 24) { problems.push(`Your team has more than than 24 Pok\u00E9mon, which the simulator can't handle.`); return problems; } const teamHas: { [k: string]: number } = {}; let lgpeStarterCount = 0; let deoxysType; for (const set of team) { if (!set) return [`You sent invalid team data. If you're not using a custom client, please report this as a bug.`]; let setProblems: string[] | null = null; if (options.skipSets?.[set.name]) { for (const i in options.skipSets[set.name]) { teamHas[i] = (teamHas[i] || 0) + 1; } } else { setProblems = (format.validateSet || this.validateSet).call(this, set, teamHas); } if (set.species === 'Pikachu-Starter' || set.species === 'Eevee-Starter') { lgpeStarterCount++; if (lgpeStarterCount === 2 && ruleTable.isBanned('nonexistent')) { problems.push(`You can only have one of Pikachu-Starter or Eevee-Starter on a team.`); } } if (dex.gen === 3 && set.species.startsWith('Deoxys')) { if (!deoxysType) { deoxysType = set.species; } else if (deoxysType !== set.species && ruleTable.isBanned('nonexistent')) { return [ `You cannot have more than one type of Deoxys forme.`, `(Each game in Gen 3 supports only one forme of Deoxys.)`, ]; } } if (setProblems) { problems = problems.concat(setProblems); } if (options.removeNicknames) { const useCrossSpeciesNicknames = format.name.includes('Cross Evolution') || ruleTable.has('franticfusionsmod'); const species = dex.species.get(set.species); let crossSpecies: Species; if (useCrossSpeciesNicknames && (crossSpecies = dex.species.get(set.name)).exists) { set.name = crossSpecies.name; } else { set.name = species.baseSpecies; if (species.baseSpecies === 'Unown') set.species = 'Unown'; } } } for (const [rule, source, limit, bans] of ruleTable.complexTeamBans) { let count = 0; for (const ban of bans) { if (teamHas[ban] > 0) { count += limit ? teamHas[ban] : 1; } } if (limit && count > limit) { const clause = source ? ` by ${source}` : ``; problems.push(`You are limited to ${limit} of ${rule}${clause}.`); } else if (!limit && count >= bans.length) { const clause = source ? ` by ${source}` : ``; problems.push(`Your team has the combination of ${rule}, which is banned${clause}.`); } } for (const rule of ruleTable.keys()) { if ('!+-*'.includes(rule.charAt(0))) continue; const subformat = dex.formats.get(rule); if (subformat.onValidateTeam && ruleTable.has(subformat.id)) { problems = problems.concat(subformat.onValidateTeam.call(this, team, format, teamHas) || []); } } if (format.onValidateTeam) { problems = problems.concat(format.onValidateTeam.call(this, team, format, teamHas) || []); } if (!problems.length) return null; return problems; } getEventOnlyData(species: Species, noRecurse?: boolean): { species: Species, eventData: EventInfo[] } | null { const dex = this.dex; const learnset = dex.species.getLearnsetData(species.id); if (!learnset?.eventOnly) { if (noRecurse) return null; return this.getEventOnlyData(dex.species.get(species.prevo), true); } if (!learnset.eventData && species.forme) { return this.getEventOnlyData(dex.species.get(species.baseSpecies), true); } if (!learnset.eventData) { throw new Error(`Event-only species ${species.name} has no eventData table`); } return { species, eventData: learnset.eventData }; } getValidationSpecies(set: PokemonSet): { outOfBattleSpecies: Species, tierSpecies: Species } { const dex = this.dex; const ruleTable = this.ruleTable; const species = dex.species.get(set.species); const item = dex.items.get(set.item); const ability = dex.abilities.get(set.ability); let outOfBattleSpecies = species; let tierSpecies = species; if (ability.id === 'battlebond' && toID(species.baseSpecies) === 'greninja') { outOfBattleSpecies = dex.species.get('greninjabond'); if (ruleTable.has('obtainableformes')) { tierSpecies = outOfBattleSpecies; } } if (ability.id === 'owntempo' && toID(species.baseSpecies) === 'rockruff') { outOfBattleSpecies = dex.species.get('rockruffdusk'); if (ruleTable.has('obtainableformes')) { tierSpecies = outOfBattleSpecies; } } if (ruleTable.has('obtainableformes')) { const canMegaEvo = dex.gen <= 7 || ruleTable.has('+pokemontag:past'); if (item.megaEvolves === species.name) { if (!item.megaStone) throw new Error(`Item ${item.name} has no base form for mega evolution`); tierSpecies = dex.species.get(item.megaStone); } else if (item.id === 'redorb' && species.id === 'groudon') { tierSpecies = dex.species.get('Groudon-Primal'); } else if (item.id === 'blueorb' && species.id === 'kyogre') { tierSpecies = dex.species.get('Kyogre-Primal'); } else if ( canMegaEvo && species.id === 'rayquaza' && set.moves.map(toID).includes('dragonascent' as ID) && !ruleTable.has('megarayquazaclause') ) { tierSpecies = dex.species.get('Rayquaza-Mega'); } else if (item.id === 'rustedsword' && species.id === 'zacian') { tierSpecies = dex.species.get('Zacian-Crowned'); } else if (item.id === 'rustedshield' && species.id === 'zamazenta') { tierSpecies = dex.species.get('Zamazenta-Crowned'); } } return { outOfBattleSpecies, tierSpecies }; } validateSet(set: PokemonSet, teamHas: AnyObject): string[] | null { const format = this.format; const dex = this.dex; const ruleTable = this.ruleTable; let problems: string[] = []; if (!set) { return [`This is not a Pokemon.`]; } let species = dex.species.get(set.species); set.species = species.name; // Backwards compatability with old Gmax format if (set.species.toLowerCase().endsWith('-gmax') && this.format.id !== 'gen8megamax') { set.species = set.species.slice(0, -5); species = dex.species.get(set.species); if (set.name?.endsWith('-Gmax')) set.name = species.baseSpecies; set.gigantamax = true; } if (set.name && set.name.length > 18) { if (set.name === set.species) { set.name = species.baseSpecies; } else { problems.push(`Nickname "${set.name}" too long (should be 18 characters or fewer)`); } } set.name = dex.getName(set.name); let item = dex.items.get(Utils.getString(set.item)); set.item = item.name; let ability = dex.abilities.get(Utils.getString(set.ability)); set.ability = ability.name; let nature = dex.natures.get(Utils.getString(set.nature)); set.nature = nature.name; if (!Array.isArray(set.moves)) set.moves = []; set.name = set.name || species.baseSpecies; let name = set.species; if (set.species !== set.name && species.baseSpecies !== set.name) { name = `${set.name} (${set.species})`; } if (!set.teraType && this.gen === 9) { set.teraType = species.types[0]; } if (!set.level) set.level = ruleTable.defaultLevel; let adjustLevel = ruleTable.adjustLevel; if (ruleTable.adjustLevelDown && set.level >= ruleTable.adjustLevelDown) { adjustLevel = ruleTable.adjustLevelDown; } if (set.level === adjustLevel || (set.level === 100 && ruleTable.maxLevel < 100)) { // Note that we're temporarily setting level 50 pokemon in VGC to level 100 // This allows e.g. level 50 Hydreigon even though it doesn't evolve until level 64. // Leveling up can't make an obtainable pokemon unobtainable, so this is safe. // Just remember to set the level back to adjustLevel at the end of validation. set.level = ruleTable.maxLevel; } if (set.level < ruleTable.minLevel) { problems.push(`${name} (level ${set.level}) is below the minimum level of ${ruleTable.minLevel}${ruleTable.blame('minlevel')}`); } if (set.level > ruleTable.maxLevel) { problems.push(`${name} (level ${set.level}) is above the maximum level of ${ruleTable.maxLevel}${ruleTable.blame('maxlevel')}`); } const setHas: { [k: string]: true } = {}; if (!set.evs) set.evs = TeamValidator.fillStats(null, ruleTable.evLimit === null ? 252 : 0); if (!set.ivs) set.ivs = TeamValidator.fillStats(null, 31); if (ruleTable.has('obtainableformes')) { problems.push(...this.validateForme(set)); species = dex.species.get(set.species); } const setSources = this.allSources(species); for (const [rule] of ruleTable) { if ('!+-*'.includes(rule.charAt(0))) continue; const subformat = dex.formats.get(rule); if (subformat.onChangeSet && ruleTable.has(subformat.id)) { problems = problems.concat(subformat.onChangeSet.call(this, set, format, setHas, teamHas) || []); } } if (format.onChangeSet) { problems = problems.concat(format.onChangeSet.call(this, set, format, setHas, teamHas) || []); } // onChangeSet can modify set.species, set.item, set.ability species = dex.species.get(set.species); item = dex.items.get(set.item); ability = dex.abilities.get(set.ability); const { outOfBattleSpecies, tierSpecies } = this.getValidationSpecies(set); if (ability.id === 'battlebond' && toID(species.baseSpecies) === 'greninja') { if (ruleTable.has('obtainablemisc')) { if (set.gender && set.gender !== 'M') { problems.push(`Battle Bond Greninja must be male.`); } set.gender = 'M'; } } if (species.id === 'melmetal' && set.gigantamax && this.dex.species.getLearnsetData(species.id).eventData) { setSources.sourcesBefore = 0; setSources.sources = ['8S0 melmetal']; } if (!species.exists) { return [`The Pokemon "${set.species}" does not exist.`]; } if (item.id && !item.exists) { return [`"${set.item}" is an invalid item.`]; } if (ability.id && !ability.exists) { if (dex.gen < 3) { // gen 1-2 don't have abilities, just silently remove ability = dex.abilities.get(''); set.ability = ''; } else { return [`"${set.ability}" is an invalid ability.`]; } } if (nature.id && !nature.exists) { if (dex.gen < 3) { // gen 1-2 don't have natures, just remove them nature = dex.natures.get(''); set.nature = ''; } else { problems.push(`"${set.nature}" is an invalid nature.`); } } if (set.happiness !== undefined && isNaN(set.happiness)) { problems.push(`${name} has an invalid happiness value.`); } if (set.hpType) { const type = dex.types.get(set.hpType); if (!type.exists || ['normal', 'fairy', 'stellar'].includes(type.id)) { problems.push(`${name}'s Hidden Power type (${set.hpType}) is invalid.`); } else { set.hpType = type.name; } } if (species.forceTeraType) { set.teraType = species.forceTeraType; } if (set.teraType) { const type = dex.types.get(set.teraType); if (!type.exists || type.isNonstandard) { problems.push(`${name}'s Terastal type (${set.teraType}) is invalid.`); } else { set.teraType = type.name; } if (dex.gen !== 9 || (ruleTable.has('terastalclause') && !ruleTable.has('bonustypemod'))) { delete set.teraType; } } let problem = this.checkSpecies(set, species, tierSpecies, setHas); if (problem) problems.push(problem); problem = this.checkItem(set, item, setHas); if (problem) problems.push(problem); if (ruleTable.has('obtainablemisc')) { if (dex.gen === 4 && item.id === 'griseousorb' && species.num !== 487) { problems.push(`${set.name} cannot hold the Griseous Orb.`, `(In Gen 4, only Giratina could hold the Griseous Orb).`); } if (dex.gen <= 1 || dex.currentMod === 'gen7letsgo') { if (item.id) { // no items allowed set.item = ''; } } } if (!set.ability) set.ability = 'No Ability'; if (ruleTable.has('obtainableabilities')) { if (dex.gen <= 2 || dex.currentMod === 'gen7letsgo') { set.ability = 'No Ability'; } else { if (!ability.name || ability.name === 'No Ability') { problems.push(`${name} needs to have an ability.`); } else if (!Object.values(species.abilities).includes(ability.name)) { if (tierSpecies.abilities[0] === ability.name) { set.ability = species.abilities[0]; } else { problems.push(`${name} can't have ${set.ability}.`); } } if (ability.name === species.abilities['H']) { setSources.isHidden = true; let unreleasedHidden = species.unreleasedHidden; if (unreleasedHidden === 'Past' && this.minSourceGen < dex.gen) unreleasedHidden = false; if (unreleasedHidden && ruleTable.has('-unreleased')) { problems.push(`${name}'s Hidden Ability is unreleased.`); } else if (dex.gen === 7 && ['entei', 'suicune', 'raikou'].includes(species.id) && this.minSourceGen > 1) { problems.push(`${name}'s Hidden Ability is only available from Virtual Console, which is not allowed in this format.`); } else if (dex.gen === 6 && ability.name === 'Symbiosis' && (set.species.endsWith('Orange') || set.species.endsWith('White'))) { problems.push(`${name}'s Hidden Ability is unreleased for the Orange and White forms.`); } else if (dex.gen === 5 && set.level < 10 && (species.maleOnlyHidden || species.gender === 'N')) { problems.push(`${name} must be at least level 10 to have a Hidden Ability.`); } if (species.maleOnlyHidden) { if (set.gender && set.gender !== 'M') { problems.push(`${name} must be male to have a Hidden Ability.`); } set.gender = 'M'; setSources.sources = ['5D']; } } else { setSources.isHidden = false; } } } ability = dex.abilities.get(set.ability); problem = this.checkAbility(set, ability, setHas); if (problem) problems.push(problem); if (!set.nature || dex.gen <= 2) { set.nature = ''; } nature = dex.natures.get(set.nature); problem = this.checkNature(set, nature, setHas); if (problem) problems.push(problem); if (set.shiny && dex.gen === 1) { set.shiny = false; } if (set.moves && Array.isArray(set.moves)) { set.moves = set.moves.filter(val => val); } if (!set.moves?.length) { problems.push(`${name} has no moves (it must have at least one to be usable).`); set.moves = []; } if (set.moves.length > ruleTable.maxMoveCount) { problems.push(`${name} has ${set.moves.length} moves, which is more than the limit of ${ruleTable.maxMoveCount}.`); return problems; } const pokemonGoProblems = this.validatePokemonGo(outOfBattleSpecies, set, setSources); if (ruleTable.isBanned('nonexistent')) { problems.push(...this.validateStats(set, species, setSources, pokemonGoProblems)); } const moveLegalityWhitelist: { [k: string]: true | undefined } = {}; for (const moveName of set.moves) { if (!moveName) continue; const move = dex.moves.get(Utils.getString(moveName)); if (!move.exists) return [`"${move.name}" is an invalid move.`]; problem = this.checkMove(set, move, setHas); if (problem) { let allowedByOM; if (problem.includes('hacking or glitches') && ruleTable.has('omunobtainablemoves')) { problem = `${name}'s ${problem}`; allowedByOM = !this.omCheckCanLearn(move, outOfBattleSpecies, setSources, set, problem); } if (!allowedByOM) { problems.push(problem); } else { moveLegalityWhitelist[move.id] = true; } } } const learnsetSpecies = dex.species.getLearnsetData(outOfBattleSpecies.id); let isFromRBYEncounter = false; if (this.gen === 1 && ruleTable.has('obtainablemisc') && !this.ruleTable.has('allowtradeback')) { let lowestEncounterLevel; for (const encounter of learnsetSpecies.encounters || []) { if (encounter.generation !== 1) continue; if (!encounter.level) continue; if (lowestEncounterLevel && encounter.level > lowestEncounterLevel) continue; lowestEncounterLevel = encounter.level; } if (lowestEncounterLevel) { if (set.level < lowestEncounterLevel) { problems.push(`${name} is not obtainable at levels below ${lowestEncounterLevel} in Gen 1.`); } isFromRBYEncounter = true; } } let isUnderleveled; let requiredLevel; if (!isFromRBYEncounter && ruleTable.has('obtainablemisc')) { // FIXME: Event pokemon given at a level under what it normally can be attained at gives a false positive let evoSpecies = species; while (evoSpecies.prevo) { if (set.level < (evoSpecies.evoLevel || 0)) { isUnderleveled = evoSpecies.name; requiredLevel = evoSpecies.evoLevel; break; } evoSpecies = dex.species.get(evoSpecies.prevo); } } let moveProblems; if (ruleTable.has('obtainablemoves')) { moveProblems = this.validateMoves(outOfBattleSpecies, set.moves, setSources, set, name, moveLegalityWhitelist); problems.push(...moveProblems); } let eventOnlyData; if ((!setSources.sourcesBefore && setSources.sources.length) || isUnderleveled) { let checkGoLegality = false; let skippedEggSource = true; const legalSources = []; if (isUnderleveled && !setSources.sources.length) { let evoSpecies = species; while (evoSpecies.prevo) { const eventData = dex.species.getLearnsetData(evoSpecies.id).eventData; if (eventData) { for (let eventIndex = 0; eventIndex < eventData.length; eventIndex++) { const eventLevel = eventData[eventIndex].level; if (eventLevel && set.level >= eventLevel) { setSources.sources.push(`${eventData[eventIndex].generation}S${eventIndex} ${evoSpecies.id}`); } } } if (evoSpecies.name === isUnderleveled) break; evoSpecies = dex.species.get(evoSpecies.prevo); } } else { checkGoLegality = true; } for (const source of setSources.sources) { if (isUnderleveled) { if (source.charAt(1) === 'S') { const eventSpecies = dex.species.get(source.substr(3)).baseSpecies; const underleveledSpecies = dex.species.get(isUnderleveled).baseSpecies; // Can only be an FE species or in a three-stage line, the prevo if (eventSpecies !== species.baseSpecies && eventSpecies !== underleveledSpecies) continue; } else if (source !== '8V') { continue; } } if (['2E', '3E'].includes(source.substr(0, 2)) && set.level < 5) continue; skippedEggSource = false; if (this.validateSource(set, source, setSources, outOfBattleSpecies)) continue; legalSources.push(source); } if (checkGoLegality && !legalSources.includes('8V')) setSources.isFromPokemonGo = false; if (setSources.isFromPokemonGo !== false && pokemonGoProblems && !pokemonGoProblems.length) { if (!legalSources.length) setSources.isFromPokemonGo = true; if (!legalSources.includes('8V')) legalSources.push('8V'); } if (legalSources.length) { setSources.sources = legalSources; } else if (isUnderleveled) { problems.push(`${name} must be at least level ${requiredLevel} to be evolved.`); const firstEventSource = setSources.sources.find(source => source.charAt(1) === 'S'); if (firstEventSource) { const eventProblems = this.validateSource( set, firstEventSource, setSources, outOfBattleSpecies, ` to be underleveled` ); if (eventProblems) problems.push(...eventProblems); } if (pokemonGoProblems?.length) { problems.push(`It failed to validate as a Pokemon from Pokemon GO because:`); for (const pokemonGoProblem of pokemonGoProblems) { problems.push(pokemonGoProblem); } } } else { let nonEggSource = null; for (const source of setSources.sources) { if (source.charAt(1) !== 'E') { nonEggSource = source; break; } } if (!nonEggSource) { // all egg moves if (skippedEggSource) { problems.push(`${name} is from a Gen 2 or 3 egg, which cannot be obtained at levels below 5.`); } else { problems.push(`${name} can't get its egg move combination (${setSources.limitedEggMoves!.join(', ')}) from any possible father.`); problems.push(`(Is this incorrect? If so, post the chainbreeding instructions in Bug Reports)`); } } else { if (species.id === 'mew' && pokemonGoProblems && !pokemonGoProblems.length) { // Whitelist Pokemon GO Mew, which cannot be sent to Let's Go setSources.isFromPokemonGo = true; } else { if (setSources.sources.length > 1) { problems.push(`${name} has an event-exclusive move that it doesn't qualify for (only one of several ways to get the move will be listed):`); } const eventProblems = this.validateSource( set, nonEggSource, setSources, outOfBattleSpecies, ` because it has a move only available` ); if (eventProblems) problems.push(...eventProblems); if (species.id === 'mew' && pokemonGoProblems?.length) { problems.push(`Additionally, it failed to validate as a Pokemon from Pokemon GO because:`); for (const pokemonGoProblem of pokemonGoProblems) { problems.push(pokemonGoProblem); } } } } } } else if (ruleTable.has('obtainablemisc') && (eventOnlyData = this.getEventOnlyData(outOfBattleSpecies))) { const { species: eventSpecies, eventData } = eventOnlyData; let legal = false; for (const event of eventData) { if (this.validateEvent(set, setSources, event, eventSpecies)) continue; setSources.eventOnlyMinSourceGen = event.generation; legal = true; break; } if (!legal && species.gen <= 2 && dex.gen >= 7 && !this.validateSource(set, '7V', setSources, species)) { legal = true; } if (!legal) { if (!pokemonGoProblems || (pokemonGoProblems?.length)) { if (eventData.length === 1) { problems.push(`${species.name} is only obtainable from an event - it needs to match its event:`); } else { problems.push(`${species.name} is only obtainable from events - it needs to match one of its events:`); } for (const [i, event] of eventData.entries()) { if (event.generation <= dex.gen && (event.generation >= this.minSourceGen || dex.gen > 8)) { const eventInfo = event; const eventNum = i + 1; const eventName = eventData.length > 1 ? ` #${eventNum}` : ``; const eventProblems = this.validateEvent( set, setSources, eventInfo, eventSpecies, ` to be`, `from its event${eventName}` ); if (eventProblems) problems.push(...eventProblems); } } if (pokemonGoProblems?.length) { problems.push(`Additionally, it failed to validate as a Pokemon from Pokemon GO because:`); for (const pokemonGoProblem of pokemonGoProblems) { problems.push(pokemonGoProblem); } } } else { setSources.isFromPokemonGo = true; } } } // Hardcoded forced validation for Pokemon GO const pokemonGoOnlySpecies = ['meltan', 'melmetal', 'gimmighoulroaming']; if (ruleTable.has('obtainablemisc') && (pokemonGoOnlySpecies.includes(species.id))) { setSources.isFromPokemonGo = true; if (pokemonGoProblems?.length) { problems.push(`${name} is only obtainable from Pokemon GO, and failed to validate because:`); for (const pokemonGoProblem of pokemonGoProblems) { problems.push(pokemonGoProblem); } } } // Attempt move validation again after verifying Pokemon GO origin if (ruleTable.has('obtainablemoves') && setSources.isFromPokemonGo) { setSources.restrictiveMoves = []; setSources.sources = ['8V']; setSources.sourcesBefore = 0; if (moveProblems && !moveProblems.length) { problems.push(...this.validateMoves(outOfBattleSpecies, set.moves, setSources, set, name, moveLegalityWhitelist)); } } if (ruleTable.has('obtainablemoves')) { if (species.id === 'keldeo' && set.moves.includes('secretsword') && this.minSourceGen > 5 && dex.gen <= 7) { problems.push(`${name} has Secret Sword, which is only compatible with Keldeo-Ordinary obtained from Gen 5.`); } const requiresGen3Source = setSources.maxSourceGen() <= 3; if (requiresGen3Source && dex.abilities.get(set.ability).gen === 4 && !species.prevo && dex.gen <= 5) { // Ability Capsule allows this in Gen 6+ problems.push(`${name} has a Gen 4 ability and isn't evolved - it can't use moves from Gen 3.`); } const canUseAbilityPatch = dex.gen >= 8 && format.mod !== 'gen8dlc1'; if (setSources.isHidden && !canUseAbilityPatch && setSources.maxSourceGen() < 5) { problems.push(`${name} has a Hidden Ability - it can't use moves from before Gen 5.`); } if ( species.maleOnlyHidden && setSources.isHidden && setSources.sourcesBefore < 5 && setSources.sources.every(source => source.charAt(1) === 'E') ) { problems.push(`${name} has an unbreedable Hidden Ability - it can't use egg moves.`); } } if (teamHas) { for (const i in setHas) { if (i in teamHas) { teamHas[i]++; } else { teamHas[i] = 1; } } } for (const [rule, source, limit, bans] of ruleTable.complexBans) { let count = 0; for (const ban of bans) { if (setHas[ban]) count++; } if (limit && count > limit) { const clause = source ? ` by ${source}` : ``; problems.push(`${name} is limited to ${limit} of ${rule}${clause}.`); } else if (!limit && count >= bans.length) { const clause = source ? ` by ${source}` : ``; if (source === 'Obtainable Moves') { problems.push(`${name} has the combination of ${rule}, which is impossible to obtain legitimately.`); } else { problems.push(`${name} has the combination of ${rule}, which is banned${clause}.`); } } } for (const [rule] of ruleTable) { if ('!+-*'.includes(rule.charAt(0))) continue; const subformat = dex.formats.get(rule); if (subformat.onValidateSet && ruleTable.has(subformat.id)) { problems = problems.concat(subformat.onValidateSet.call(this, set, format, setHas, teamHas) || []); } } if (format.onValidateSet) { problems = problems.concat(format.onValidateSet.call(this, set, format, setHas, teamHas) || []); } const nameSpecies = dex.species.get(set.name); if (nameSpecies.exists && nameSpecies.name.toLowerCase() === set.name.toLowerCase()) { // nickname is the name of a species if (nameSpecies.baseSpecies === species.baseSpecies) { set.name = species.baseSpecies; } else if (nameSpecies.name !== species.name && nameSpecies.name !== species.baseSpecies && ruleTable.has('nicknameclause')) { // nickname species doesn't match actual species // Nickname Clause problems.push(`${name} must not be nicknamed a different Pokémon species than what it actually is.`); } } if (!problems.length) { if (set.gender === '' && !species.gender) { set.gender = ['M', 'F'][Math.floor(Math.random() * 2)]; } if (adjustLevel) set.level = adjustLevel; return null; } return problems; } validateStats(set: PokemonSet, species: Species, setSources: PokemonSources, pokemonGoProblems: string[] | null) { const ruleTable = this.ruleTable; const dex = this.dex; const allowAVs = ruleTable.has('allowavs'); const evLimit = ruleTable.evLimit; const canBottleCap = dex.gen >= 7 && (set.level >= (dex.gen < 9 ? 100 : 50) || !ruleTable.has('obtainablemisc')); if (!set.evs) set.evs = TeamValidator.fillStats(null, evLimit === null ? 252 : 0); if (!set.ivs) set.ivs = TeamValidator.fillStats(null, 31); const problems = []; const name = set.name || set.species; const maxedIVs = Object.values(set.ivs).every(stat => stat === 31); for (const moveName of set.moves) { const move = dex.moves.get(moveName); if (move.id === 'hiddenpower' && move.type !== 'Normal') { if (!set.hpType) { set.hpType = move.type; } else if (set.hpType !== move.type && ruleTable.has('obtainablemisc')) { problems.push(`${name}'s Hidden Power type ${set.hpType} is incompatible with Hidden Power ${move.type}`); } } } if (set.hpType && maxedIVs && ruleTable.has('obtainablemisc')) { if (dex.gen <= 2) { const HPdvs = dex.types.get(set.hpType).HPdvs; set.ivs = { hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30 }; let statName: StatID; for (statName in HPdvs) { set.ivs[statName] = HPdvs[statName]! * 2; } set.ivs.hp = -1; } else if (!canBottleCap) { set.ivs = TeamValidator.fillStats(dex.types.get(set.hpType).HPivs, 31); } } if (!set.hpType && set.moves.some(m => dex.moves.get(m).id === 'hiddenpower')) { set.hpType = dex.getHiddenPower(set.ivs).type; } const cantBreedNorEvolve = (species.eggGroups[0] === 'Undiscovered' && !species.prevo && !species.nfe); const isLegendary = (cantBreedNorEvolve && !species.tags.includes('Paradox') && ![ 'Pikachu', 'Unown', 'Dracozolt', 'Arctozolt', 'Dracovish', 'Arctovish', 'Gouging Fire', 'Raging Bolt', 'Iron Boulder', 'Iron Crown', 'Terapagos', ].includes(species.baseSpecies)) || [ 'Manaphy', 'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', ].includes(species.baseSpecies); const diancieException = species.name === 'Diancie' && !set.shiny; const has3PerfectIVs = setSources.minSourceGen() >= 6 && isLegendary && !diancieException; if (set.hpType === 'Fighting' && ruleTable.has('obtainablemisc')) { if (has3PerfectIVs) { // Legendary Pokemon must have at least 3 perfect IVs in gen 6+ problems.push(`${name} must not have Hidden Power Fighting because it starts with 3 perfect IVs because it's a Gen 6+ legendary.`); } } if (has3PerfectIVs && ruleTable.has('obtainablemisc')) { let perfectIVs = 0; for (const stat in set.ivs) { if (set.ivs[stat as 'hp'] >= 31) perfectIVs++; } if (perfectIVs < 3) { if (!pokemonGoProblems || pokemonGoProblems?.length) { const reason = (this.minSourceGen === 6 ? ` and this format requires Gen ${dex.gen} Pokémon` : ` in Gen 6 or later`); problems.push(`${name} must have at least three perfect IVs because it's a legendary${reason}.`); if (pokemonGoProblems?.length) { problems.push(`Additionally, it failed to validate as a Pokemon from Pokemon GO because:`); for (const pokemonGoProblem of pokemonGoProblems) { problems.push(pokemonGoProblem); } } } else { setSources.isFromPokemonGo = true; } } } if (set.hpType && !canBottleCap) { const ivHpType = dex.getHiddenPower(set.ivs).type; if (set.hpType !== ivHpType) { problems.push(`${name} has Hidden Power ${set.hpType}, but its IVs are for Hidden Power ${ivHpType}.`); } } else if (set.hpType) { if (!this.possibleBottleCapHpType(set.hpType, set.ivs)) { problems.push(`${name} has Hidden Power ${set.hpType}, but its IVs don't allow this even with (Bottle Cap) Hyper Training.`); } } if (dex.gen <= 2) { // validate DVs const ivs = set.ivs; const atkDV = Math.floor(ivs.atk / 2); const defDV = Math.floor(ivs.def / 2); const speDV = Math.floor(ivs.spe / 2); const spcDV = Math.floor(ivs.spa / 2); const expectedHpDV = (atkDV % 2) * 8 + (defDV % 2) * 4 + (speDV % 2) * 2 + (spcDV % 2); if (ivs.hp === -1) ivs.hp = expectedHpDV * 2; const hpDV = Math.floor(ivs.hp / 2); if (expectedHpDV !== hpDV) { problems.push(`${name} has an HP DV of ${hpDV}, but its Atk, Def, Spe, and Spc DVs give it an HP DV of ${expectedHpDV}.`); } if (ivs.spa !== ivs.spd) { if (dex.gen === 2) { problems.push(`${name} has different SpA and SpD DVs, which is not possible in Gen 2.`); } else { ivs.spd = ivs.spa; } } if (dex.gen > 1 && !species.gender) { // Gen 2 gender is calculated from the Atk DV. // High Atk DV <-> M. The meaning of "high" depends on the gender ratio. const genderThreshold = species.genderRatio.F * 16; const expectedGender = (atkDV >= genderThreshold ? 'M' : 'F'); if (set.gender && set.gender !== expectedGender) { problems.push(`${name} is ${set.gender}, but it has an Atk DV of ${atkDV}, which makes its gender ${expectedGender}.`); } else { set.gender = expectedGender; } } if ( set.species === 'Marowak' && toID(set.item) === 'thickclub' && set.moves.map(toID).includes('swordsdance' as ID) && set.level === 100 ) { // Marowak hack set.ivs.atk = Math.floor(set.ivs.atk / 2) * 2; while (set.evs.atk > 0 && 2 * 80 + set.ivs.atk + Math.floor(set.evs.atk / 4) + 5 > 255) { set.evs.atk -= 4; } } if (dex.gen > 1) { const expectedShiny = !!(defDV === 10 && speDV === 10 && spcDV === 10 && atkDV % 4 >= 2); if (expectedShiny && !set.shiny) { problems.push(`${name} is not shiny, which does not match its DVs.`); } else if (!expectedShiny && set.shiny) { problems.push(`${name} is shiny, which does not match its DVs (its DVs must all be 10, except Atk which must be 2, 3, 6, 7, 10, 11, 14, or 15).`); } } set.nature = 'Serious'; } for (const stat in set.evs) { if (set.evs[stat as 'hp'] < 0) { problems.push(`${name} has less than 0 ${allowAVs ? 'Awakening Values' : 'EVs'} in ${Dex.stats.names[stat as 'hp']}.`); } } if (dex.currentMod === 'gen7letsgo') { // AVs for (const stat in set.evs) { if (set.evs[stat as 'hp'] > 0 && !allowAVs) { problems.push(`${name} has Awakening Values but this format doesn't allow them.`); break; } else if (set.evs[stat as 'hp'] > 200) { problems.push(`${name} has more than 200 Awakening Values in ${Dex.stats.names[stat as 'hp']}.`); } } } else { // EVs for (const stat in set.evs) { if (set.evs[stat as StatID] > 255) { problems.push(`${name} has more than 255 EVs in ${Dex.stats.names[stat as 'hp']}.`); } } if (dex.gen <= 2) { if (set.evs.spa !== set.evs.spd) { if (dex.gen === 2) { problems.push(`${name} has different SpA and SpD EVs, which is not possible in Gen 2.`); } else { set.evs.spd = set.evs.spa; } } } } let totalEV = 0; for (const stat in set.evs) totalEV += set.evs[stat as 'hp']; if (!this.format.debug) { if (set.level > 1 && evLimit !== 0 && totalEV === 0) { problems.push(`${name} has exactly 0 EVs - did you forget to EV it? (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`); } else if (![508, 510].includes(evLimit!) && [508, 510].includes(totalEV)) { problems.push(`${name} has exactly ${totalEV} EVs, but this format does not restrict you to 510 EVs (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`); } // Check for level import errors from user in VGC -> DOU, etc. // Note that in VGC etc (Adjust Level Down = 50), `set.level` will be 100 here for validation purposes if (set.level === 50 && ruleTable.maxLevel !== 50 && !ruleTable.maxTotalLevel && evLimit !== 0 && totalEV % 4 === 0) { problems.push(`${name} is level 50, but this format allows level ${ruleTable.maxLevel} Pokémon. (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`); } } if (evLimit !== null && totalEV > evLimit) { if (!evLimit) { problems.push(`${name} has EVs, which is not allowed by this format.`); } else { problems.push(`${name} has ${totalEV} total EVs, which is more than this format's limit of ${evLimit}.`); } } return problems; } /** * Not exhaustive, just checks Atk and Spe, which are the only competitively * relevant IVs outside of extremely obscure situations. */ possibleBottleCapHpType(type: string, ivs: StatsTable) { if (!type) return true; if (['Dark', 'Dragon', 'Grass', 'Ghost', 'Poison'].includes(type)) { // Spe must be odd if (ivs.spe % 2 === 0) return false; } if (['Psychic', 'Fire', 'Rock', 'Fighting'].includes(type)) { // Spe must be even if (ivs.spe !== 31 && ivs.spe % 2 === 1) return false; } if (type === 'Dark') { // Atk must be odd if (ivs.atk % 2 === 0) return false; } if (['Ice', 'Water'].includes(type)) { // Spe or Atk must be odd if (ivs.spe % 2 === 0 && ivs.atk % 2 === 0) return false; } return true; } validateSource( set: PokemonSet, source: PokemonSource, setSources: PokemonSources, species: Species, because: string ): string[] | undefined; validateSource( set: PokemonSet, source: PokemonSource, setSources: PokemonSources, species: Species ): true | undefined; /** * Returns array of error messages if invalid, undefined if valid * * If `because` is not passed, instead returns true if invalid. */ validateSource( set: PokemonSet, source: PokemonSource, setSources: PokemonSources, species: Species, because?: string ) { let eventData: EventInfo | undefined; let eventSpecies = species; if (source.charAt(1) === 'S') { const splitSource = source.substr(source.charAt(2) === 'T' ? 3 : 2).split(' '); const dex = (this.dex.gen === 1 ? this.dex.mod('gen2') : this.dex); eventSpecies = dex.species.get(splitSource[1]); const eventLsetData = this.dex.species.getLearnsetData(eventSpecies.id); eventData = eventLsetData.eventData?.[parseInt(splitSource[0])]; if (!eventData) { throw new Error(`${eventSpecies.name} from ${species.name} doesn't have data for event ${source}`); } } else if (source === '7V') { const isMew = species.id === 'mew'; const isCelebi = species.id === 'celebi'; const g7speciesName = (species.gen > 2 && species.prevo) ? species.prevo : species.id; const isHidden = !!this.dex.mod('gen7').species.get(g7speciesName).abilities['H']; eventData = { generation: 2, level: isMew ? 5 : isCelebi ? 30 : 3, // Level 1/2 Pokémon can't be obtained by transfer from RBY/GSC perfectIVs: isMew || isCelebi ? 5 : 3, isHidden, shiny: isMew ? undefined : 1, pokeball: 'pokeball', from: 'Gen 1-2 Virtual Console transfer', }; } else if (source === '8V') { const isMew = species.id === 'mew'; eventData = { generation: 8, perfectIVs: isMew ? 3 : undefined, shiny: isMew ? undefined : 1, from: 'Gen 7 Let\'s Go! HOME transfer', }; } else if (source.charAt(1) === 'D') { eventData = { generation: 5, level: 10, from: 'Gen 5 Dream World', isHidden: !!this.dex.mod('gen5').species.get(species.id).abilities['H'], }; } else if (source.charAt(1) === 'E') { if (this.findEggMoveFathers(source, species, setSources)) { return undefined; } if (because) throw new Error(`Wrong place to get an egg incompatibility message`); return true; } else { throw new Error(`Unidentified source ${source} passed to validateSource`); } // complicated fancy return signature return this.validateEvent(set, setSources, eventData, eventSpecies, because as any) as any; } findEggMoveFathers(source: PokemonSource, species: Species, setSources: PokemonSources, getAll?: false, pokemonBlacklist?: ID[], noRecurse?: true): boolean; findEggMoveFathers(source: PokemonSource, species: Species, setSources: PokemonSources, getAll?: true): ID[] | null; findEggMoveFathers(source: PokemonSource, species: Species, setSources: PokemonSources, getAll?: boolean, pokemonBlacklist?: ID[], noRecurse?: boolean) { if (!pokemonBlacklist) pokemonBlacklist = []; if (!pokemonBlacklist.includes(species.id)) pokemonBlacklist.push(species.id); // tradebacks have an eggGen of 2 even though the source is 1ET const eggGen = Math.max(parseInt(source.charAt(0)), 2); const fathers: ID[] = []; // Gen 6+ don't have egg move incompatibilities // (except for certain cases with baby Pokemon not handled here) if (!getAll && eggGen >= 6 && !setSources.levelUpEggMoves && !species.mother) return true; let eggMoves = setSources.limitedEggMoves; if (eggGen === 3) eggMoves = eggMoves?.filter(eggMove => !setSources.pomegEggMoves?.includes(eggMove)); // must have 2 or more egg moves to have egg move incompatibilities if (!eggMoves) { // happens often in gen 1-6 LC if your only egg moves are level-up moves, // which aren't limited and so aren't in `limitedEggMoves` return getAll ? ['*'] : true; } if (!getAll && eggMoves.length <= 1 && !setSources.levelUpEggMoves) return true; if (setSources.levelUpEggMoves && eggGen >= 6) eggMoves = setSources.levelUpEggMoves; // gen 1 eggs come from gen 2 breeding const dex = this.dex.gen === 1 ? this.dex.mod('gen2') : this.dex; // In Gen 5 and earlier, egg moves can only be inherited from the father // we'll test each possible father separately let eggGroups = species.eggGroups; if (species.id === 'nidoqueen' || species.id === 'nidorina') { eggGroups = dex.species.get('nidoranf').eggGroups; } else if (species.id === 'shedinja') { // Shedinja and Nincada are different Egg groups; Shedinja itself is genderless eggGroups = dex.species.get('nincada').eggGroups; } else if (dex !== this.dex) { // Gen 1 tradeback; grab the egg groups from Gen 2 eggGroups = dex.species.get(species.id).eggGroups; } if (eggGroups[0] === 'Undiscovered') eggGroups = dex.species.get(species.evos[0]).eggGroups; if (eggGroups[0] === 'Undiscovered' || !eggGroups.length) { throw new Error(`${species.name} has no egg groups for source ${source}`); } // no chainbreeding necessary if the father can be Smeargle if (!getAll && eggGroups.includes('Field')) return true; // try to find a father to inherit the egg move combination from for (const father of dex.species.all()) { // can't inherit from CAP pokemon if (father.isNonstandard) continue; // can't breed mons from future gens if (father.gen > eggGen) continue; // father must be male if (father.gender === 'N' || father.gender === 'F') continue; // can't inherit from dex entries with no learnsets if (!dex.species.getLearnsetData(father.id).learnset) continue; // something is clearly wrong if its only possible father is itself // (exceptions: ExtremeSpeed Dragonite, Self-destruct Snorlax) if (pokemonBlacklist.includes(father.id) && !['dragonite', 'snorlax'].includes(father.id)) continue; // don't check NFE Pokémon - their evolutions will know all their moves and more // exception: Combee/Salandit, because their evos can't be fathers if (father.evos.length) { const evolvedFather = dex.species.get(father.evos[0]); if (evolvedFather.gen <= eggGen && evolvedFather.gender !== 'F') continue; } // must be able to breed with father if (!father.eggGroups.some(eggGroup => eggGroups.includes(eggGroup))) continue; // father must be able to learn the move if (!this.fatherCanLearn(species, father, eggMoves, eggGen, pokemonBlacklist, noRecurse)) continue; // father found! if (!getAll) return true; fathers.push(father.id); } if (!getAll) return false; return (!fathers.length && eggGen < 6) ? null : fathers; } /** * We could, if we wanted, do a complete move validation of the father's * moveset to see if it's valid. This would recurse and be NP-Hard so * instead we won't. We'll instead use a simplified algorithm: The father * is allowed to have multiple egg moves and a maximum of one move from * any other restrictive source; recursion is done only if there are less * egg moves to validate or if the father has an egg group it doesn't * share with the egg Pokemon. Recursion is also limited to two iterations * of calling findEggMoveFathers. */ fatherCanLearn(baseSpecies: Species, species: Species, moves: ID[], eggGen: number, pokemonBlacklist: ID[], noRecurse: boolean | undefined) { if (!this.dex.species.getLearnsetData(species.id).learnset) return false; if (species.id === 'smeargle') return true; const canBreedWithSmeargle = species.eggGroups.includes('Field'); const allEggSources = new PokemonSources(); allEggSources.sourcesBefore = eggGen; for (const move of moves) { const eggSources = new PokemonSources(); for (const { learnset, species: curSpecies } of this.dex.species.getFullLearnset(species.id)) { const eggPokemon = curSpecies.prevo ? curSpecies.id : ''; if (learnset[move]) { for (const moveSource of learnset[move]) { if (eggGen > 8 && parseInt(moveSource.charAt(0)) <= 8) continue; if (parseInt(moveSource.charAt(0)) > eggGen) continue; const canLearnFromSmeargle = moveSource.charAt(1) === 'E' && canBreedWithSmeargle; if (!'ESDV'.includes(moveSource.charAt(1)) || canLearnFromSmeargle) { eggSources.addGen(parseInt(moveSource.charAt(0))); break; } else { if (moveSource.charAt(1) === 'E') { eggSources.add(moveSource + eggPokemon, move); if (eggGen === 2 && this.dex.moves.getByID(move).gen === 1) eggSources.add('1ET' + eggPokemon, move); } else { eggSources.add(moveSource + eggPokemon); } } } } if (eggSources.sourcesBefore === eggGen) break; } if (eggSources.sourcesBefore === eggGen) continue; if (!eggSources.sourcesBefore && !eggSources.sources.length) return false; const onlyEggSources = eggSources.sources.filter(source => source.charAt(1) === 'E'); if (eggGen >= 3 && onlyEggSources.length && eggSources.limitedEggMoves === null && eggSources.sourcesBefore) { eggSources.possiblyLimitedEggMoves = [toID(`${eggSources.sourcesBefore}${move}`)]; } allEggSources.intersectWith(eggSources); if (!allEggSources.size()) return false; } pokemonBlacklist.push(species.id); if (allEggSources.limitedEggMoves && allEggSources.limitedEggMoves.length > 1) { if (noRecurse) return false; let canChainbreed = false; for (const fatherEggGroup of species.eggGroups) { if (!baseSpecies.eggGroups.includes(fatherEggGroup)) { canChainbreed = true; break; } } if (!canChainbreed && allEggSources.limitedEggMoves.length === moves.length) return false; const setSources = new PokemonSources(); setSources.limitedEggMoves = allEggSources.limitedEggMoves; return this.findEggMoveFathers(allEggSources.sources[0], species, setSources, false, pokemonBlacklist, true); } return true; } motherCanLearn(species: ID, move: ID) { if (!species) return false; const fullLearnset = this.dex.species.getFullLearnset(species); for (const { learnset } of fullLearnset) { if (learnset[move]) return true; } return false; } validateForme(set: PokemonSet) { const dex = this.dex; const name = set.name || set.species; const problems = []; const item = dex.items.get(set.item); const species = dex.species.get(set.species); if (species.name === 'Necrozma-Ultra') { const whichMoves = (set.moves.map(toID).includes('sunsteelstrike' as ID) ? 1 : 0) + (set.moves.map(toID).includes('moongeistbeam' as ID) ? 2 : 0); if (item.name !== 'Ultranecrozium Z') { // Necrozma-Ultra transforms from one of two formes, and neither one is the base forme problems.push(`Necrozma-Ultra must start the battle holding Ultranecrozium Z.`); } else if (whichMoves === 1) { set.species = 'Necrozma-Dusk-Mane'; set.ability = 'Prism Armor'; } else if (whichMoves === 2) { set.species = 'Necrozma-Dawn-Wings'; set.ability = 'Prism Armor'; } else { problems.push(`Necrozma-Ultra must start the battle as Necrozma-Dusk-Mane or Necrozma-Dawn-Wings holding Ultranecrozium Z. Please specify which Necrozma it should start as.`); } } else if (species.name === 'Zygarde-Complete') { problems.push(`Zygarde-Complete must start the battle as Zygarde or Zygarde-10% with Power Construct. Please specify which Zygarde it should start as.`); } else if (species.baseSpecies === 'Terapagos') { set.species = 'Terapagos'; set.ability = 'Tera Shift'; } else if (species.battleOnly) { if (species.requiredAbility && set.ability !== species.requiredAbility) { // Darmanitan-Zen problems.push(`${species.name} transforms in-battle with ${species.requiredAbility}, please fix its ability.`); } if (species.requiredItems) { if (!species.requiredItems.includes(item.name)) { // Mega or Primal problems.push(`${species.name} transforms in-battle with ${species.requiredItem}, please fix its item.`); } } if (species.requiredMove && !set.moves.map(toID).includes(toID(species.requiredMove))) { // Meloetta-Pirouette, Rayquaza-Mega problems.push(`${species.name} transforms in-battle with ${species.requiredMove}, please fix its moves.`); } if (typeof species.battleOnly !== 'string') { // Ultra Necrozma and Complete Zygarde are already checked above throw new Error(`${species.name} should have a string battleOnly`); } // Set to out-of-battle forme set.species = species.battleOnly; } else { if (species.requiredAbility) { // Impossible! throw new Error(`Species ${species.name} has a required ability despite not being a battle-only forme; it should just be in its abilities table.`); } if (species.requiredItems && !species.requiredItems.includes(item.name)) { if (dex.gen >= 8 && (species.baseSpecies === 'Arceus' || species.baseSpecies === 'Silvally')) { // Arceus/Silvally formes in gen 8 only require the item with Multitype/RKS System if (set.ability === species.abilities[0]) { problems.push( `${name} needs to hold ${species.requiredItems.join(' or ')}.`, `(It will revert to its Normal forme if you remove the item or give it a different item.)` ); } } else { // Memory/Drive/Griseous Orb/Plate/Z-Crystal - Forme mismatch const baseSpecies = this.dex.species.get(species.changesFrom); problems.push( `${name} needs to hold ${species.requiredItems.join(' or ')} to be in its ${species.forme} forme.`, `(It will revert to its ${baseSpecies.baseForme || 'base'} forme if you remove the item or give it a different item.)` ); } } if (species.requiredMove && !set.moves.map(toID).includes(toID(species.requiredMove))) { const baseSpecies = this.dex.species.get(species.changesFrom); problems.push( `${name} needs to know the move ${species.requiredMove} to be in its ${species.forme} forme.`, `(It will revert to its ${baseSpecies.baseForme} forme if it forgets the move.)` ); } // Mismatches between the set forme (if not base) and the item signature forme will have been rejected already. // It only remains to assign the right forme to a set with the base species (Arceus/Genesect/Giratina/Silvally). if (item.forcedForme && species.name === dex.species.get(item.forcedForme).baseSpecies) { set.species = item.forcedForme; } } if (species.name === 'Pikachu-Cosplay') { const cosplay: { [k: string]: string } = { meteormash: 'Pikachu-Rock-Star', iciclecrash: 'Pikachu-Belle', drainingkiss: 'Pikachu-Pop-Star', electricterrain: 'Pikachu-PhD', flyingpress: 'Pikachu-Libre', }; for (const moveid of set.moves) { if (moveid in cosplay) { set.species = cosplay[moveid]; break; } } } if (species.name === 'Keldeo' && set.moves.map(toID).includes('secretsword' as ID) && dex.gen >= 8) { set.species = 'Keldeo-Resolute'; } const crowned: { [k: string]: string } = { 'Zacian-Crowned': 'behemothblade', 'Zamazenta-Crowned': 'behemothbash', }; if (species.name in crowned) { const behemothMove = set.moves.map(toID).indexOf(crowned[species.name] as ID); if (behemothMove >= 0) { set.moves[behemothMove] = 'ironhead'; } } if (species.baseSpecies === "Hoopa" && dex.gen >= 9) { const moves = set.moves.map(toID); const hyperspaceHole = moves.indexOf('hyperspacehole' as ID); const hyperspaceFury = moves.indexOf('hyperspacefury' as ID); if (species.name === "Hoopa" && hyperspaceFury >= 0) { problems.push(`In Generation 9, Hoopa cannot run Hyperspace Fury because it gets replaced with Hyperspace Hole upon changing forme.`); } else if (species.name === "Hoopa-Unbound" && hyperspaceHole >= 0) { problems.push(`In Generation 9, Hoopa-Unbound cannot run Hyperspace Hole because it gets replaced with Hyperspace Fury upon changing forme.`); } } if (species.baseSpecies === "Greninja" && toID(set.ability) === 'battlebond') { set.species = "Greninja-Bond"; } if (species.baseSpecies === "Rockruff" && toID(set.ability) === 'owntempo') { set.species = "Rockruff-Dusk"; } if (species.baseSpecies === "Unown" && dex.gen === 2) { let resultBinary = ''; for (const iv of ['atk', 'def', 'spe', 'spa'] as const) { resultBinary += set.ivs[iv].toString(2).padStart(5, '0').slice(1, 3); } const resultDecimal = Math.floor(parseInt(resultBinary, 2) / 10); const expectedLetter = String.fromCharCode(resultDecimal + 65); const unownLetter = species.forme || "A"; if (unownLetter !== expectedLetter) { problems.push(`Unown has forme ${unownLetter}, but its DVs give it the forme ${expectedLetter}.`); } } return problems; } checkSpecies(set: PokemonSet, species: Species, tierSpecies: Species, setHas: { [k: string]: true }) { const dex = this.dex; const ruleTable = this.ruleTable; // https://www.smogon.com/forums/posts/8659168 if ( (tierSpecies.id === 'zamazentacrowned' && species.id === 'zamazenta') || (tierSpecies.id === 'zaciancrowned' && species.id === 'zacian') ) { species = tierSpecies; } setHas['pokemon:' + species.id] = true; setHas['basepokemon:' + toID(species.baseSpecies)] = true; let isMega = false; if (tierSpecies !== species) { setHas['pokemon:' + tierSpecies.id] = true; if (tierSpecies.isMega || tierSpecies.isPrimal) { setHas['pokemontag:mega'] = true; isMega = true; } } let isGmax = false; if (tierSpecies.canGigantamax && set.gigantamax) { setHas['pokemon:' + tierSpecies.id + 'gmax'] = true; isGmax = true; } if (tierSpecies.baseSpecies === 'Greninja' && toID(set.ability) === 'battlebond') { setHas['pokemon:greninjabond'] = true; } if (tierSpecies.baseSpecies === 'Rockruff' && toID(set.ability) === 'owntempo') { setHas['pokemon:rockruffdusk'] = true; } const tier = tierSpecies.tier === '(PU)' ? 'ZU' : tierSpecies.tier === '(NU)' ? 'PU' : tierSpecies.tier; const tierTag = 'pokemontag:' + toID(tier); setHas[tierTag] = true; const doublesTier = tierSpecies.doublesTier === '(DUU)' ? 'DNU' : tierSpecies.doublesTier; const doublesTierTag = 'pokemontag:' + toID(doublesTier); setHas[doublesTierTag] = true; const ndTier = tierSpecies.natDexTier === '(PU)' ? 'ZU' : tierSpecies.natDexTier === '(NU)' ? 'PU' : tierSpecies.natDexTier; const ndTierTag = 'pokemontag:nd' + toID(ndTier); setHas[ndTierTag] = true; // Only pokemon that can gigantamax should have the Gmax flag if (!tierSpecies.canGigantamax && set.gigantamax) { return `${tierSpecies.name} cannot Gigantamax but is flagged as being able to.`; } let banReason = ruleTable.check('pokemon:' + species.id); if (banReason) { return `${species.name} is ${banReason}.`; } if (banReason === '') return null; if (tierSpecies !== species) { banReason = ruleTable.check('pokemon:' + tierSpecies.id); if (banReason) { return `${tierSpecies.name} is ${banReason}.`; } if (banReason === '') return null; } if (isMega) { banReason = ruleTable.check('pokemontag:mega', setHas); if (banReason) { return `Mega evolutions are ${banReason}.`; } } if (isGmax) { banReason = ruleTable.check('pokemon:' + tierSpecies.id + 'gmax'); if (banReason) { return `Gigantamaxing ${species.name} is ${banReason}.`; } } banReason = ruleTable.check('basepokemon:' + toID(species.baseSpecies)); if (banReason) { return `${species.name} is ${banReason}.`; } if (banReason === '') { // don't allow nonstandard speciess when whitelisting standard base species // i.e. unbanning Pichu doesn't mean allowing Pichu-Spiky-Eared outside of Gen 4 const baseSpecies = dex.species.get(species.baseSpecies); if (baseSpecies.isNonstandard === species.isNonstandard) { return null; } } // We can't return here because the `-nonexistent` rule is a bit // complicated in terms of what trumps it. We don't want e.g. // +Mythical to unban Shaymin in Gen 1, for instance. let nonexistentCheck = Tags.nonexistent.genericFilter!(tierSpecies) && ruleTable.check('nonexistent'); const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent']; 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)!(tierSpecies)) { const existenceTag = EXISTENCE_TAG.includes(tagid); if (ruleid.startsWith('+')) { // we want rules like +CAP to trump -Nonexistent, but most tags shouldn't if (!existenceTag && nonexistentCheck) continue; return null; } if (existenceTag) { // for a nicer error message nonexistentCheck = 'banned'; break; } return `${species.name} is tagged ${tag.name}, which is ${ruleTable.check(ruleid.slice(1)) || "banned"}.`; } } if (nonexistentCheck) { if (tierSpecies.isNonstandard === 'Past' || tierSpecies.isNonstandard === 'Future') { return `${tierSpecies.name} does not exist in Gen ${dex.gen}.`; } if (tierSpecies.isNonstandard === 'LGPE') { return `${tierSpecies.name} does not exist in this game, only in Let's Go Pikachu/Eevee.`; } if (tierSpecies.isNonstandard === 'CAP') { return `${tierSpecies.name} is a CAP and does not exist in this game.`; } if (tierSpecies.isNonstandard === 'Unobtainable') { return `${tierSpecies.name} is not possible to obtain in this game.`; } if (tierSpecies.isNonstandard === 'Gigantamax') { return `${tierSpecies.name} is a placeholder for a Gigantamax sprite, not a real Pokémon. (This message is likely to be a validator bug.)`; } return `${tierSpecies.name} does not exist in this game.`; } if (nonexistentCheck === '') return null; // Special casing for Pokemon that can Gmax, but their Gmax factor cannot be legally obtained if (tierSpecies.gmaxUnreleased && set.gigantamax) { banReason = ruleTable.check('pokemontag:unobtainable'); if (banReason) { return `${tierSpecies.name} is flagged as gigantamax, but it cannot gigantamax without hacking or glitches.`; } if (banReason === '') return null; } banReason = ruleTable.check('pokemontag:allpokemon'); if (banReason) { return `${species.name} is not in the list of allowed pokemon.`; } return null; } checkItem(set: PokemonSet, item: Item, setHas: { [k: string]: true }) { const dex = this.dex; const ruleTable = this.ruleTable; setHas['item:' + item.id] = true; let banReason = ruleTable.check('item:' + (item.id || 'noitem')); if (banReason) { if (!item.id) { return `${set.name} not holding an item is ${banReason}.`; } return `${set.name}'s item ${item.name} is ${banReason}.`; } if (banReason === '') return null; if (!item.id) return null; banReason = ruleTable.check('pokemontag:allitems'); if (banReason) { return `${set.name}'s item ${item.name} is not in the list of allowed items.`; } // obtainability if (item.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(item.isNonstandard)); if (banReason) { if (item.isNonstandard === 'Unobtainable') { return `${item.name} is not obtainable without hacking or glitches.`; } return `${set.name}'s item ${item.name} is tagged ${item.isNonstandard}, which is ${banReason}.`; } if (banReason === '') return null; } if (item.isNonstandard && item.isNonstandard !== 'Unobtainable') { banReason = ruleTable.check('nonexistent', setHas); if (banReason) { if (['Past', 'Future'].includes(item.isNonstandard)) { return `${set.name}'s item ${item.name} does not exist in Gen ${dex.gen}.`; } return `${set.name}'s item ${item.name} does not exist in this game.`; } if (banReason === '') return null; } return null; } checkMove(set: PokemonSet, move: Move, setHas: { [k: string]: true }) { const dex = this.dex; const ruleTable = this.ruleTable; setHas['move:' + move.id] = true; let banReason = ruleTable.check('move:' + move.id); if (banReason) { return `${set.name}'s move ${move.name} is ${banReason}.`; } if (banReason === '') return null; banReason = ruleTable.check('pokemontag:allmoves'); if (banReason) { return `${set.name}'s move ${move.name} is not in the list of allowed moves.`; } // obtainability if (move.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(move.isNonstandard)); if (banReason) { if (move.isNonstandard === 'Unobtainable') { return `${move.name} is not obtainable without hacking or glitches${dex.gen >= 9 && move.gen < dex.gen ? ` in Gen ${dex.gen}` : ``}.`; } if (move.isNonstandard === 'Gigantamax') { return `${move.name} is not usable without Gigantamaxing its user, ${move.isMax}.`; } return `${set.name}'s move ${move.name} is tagged ${move.isNonstandard}, which is ${banReason}.`; } if (banReason === '') return null; } if (move.isNonstandard && move.isNonstandard !== 'Unobtainable') { banReason = ruleTable.check('nonexistent', setHas); if (banReason) { if (['Past', 'Future'].includes(move.isNonstandard)) { return `${set.name}'s move ${move.name} does not exist in Gen ${dex.gen}.`; } return `${set.name}'s move ${move.name} does not exist in this game.`; } if (banReason === '') return null; } return null; } checkAbility(set: PokemonSet, ability: Ability, setHas: { [k: string]: true }) { const dex = this.dex; const ruleTable = this.ruleTable; setHas['ability:' + ability.id] = true; if (this.format.id.startsWith('gen9pokebilities')) { const species = dex.species.get(set.species); const unSeenAbilities = Object.keys(species.abilities) .filter(key => key !== 'S' && (key !== 'H' || !species.unreleasedHidden)) .map(key => species.abilities[key as "0" | "1" | "H" | "S"]); if (ability.id !== this.toID(species.abilities['S'])) { for (const abilityName of unSeenAbilities) { setHas['ability:' + toID(abilityName)] = true; } } } let banReason = ruleTable.check('ability:' + ability.id); if (banReason) { return `${set.name}'s ability ${ability.name} is ${banReason}.`; } if (banReason === '') return null; banReason = ruleTable.check('pokemontag:allabilities'); if (banReason) { return `${set.name}'s ability ${ability.name} is not in the list of allowed abilities.`; } // obtainability if (ability.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(ability.isNonstandard)); if (banReason) { return `${set.name}'s ability ${ability.name} is tagged ${ability.isNonstandard}, which is ${banReason}.`; } if (banReason === '') return null; banReason = ruleTable.check('nonexistent', setHas); if (banReason) { if (['Past', 'Future'].includes(ability.isNonstandard)) { return `${set.name}'s ability ${ability.name} does not exist in Gen ${dex.gen}.`; } return `${set.name}'s ability ${ability.name} does not exist in this game.`; } if (banReason === '') return null; } return null; } checkNature(set: PokemonSet, nature: Nature, setHas: { [k: string]: true }) { const dex = this.dex; const ruleTable = this.ruleTable; setHas['nature:' + nature.id] = true; let banReason = ruleTable.check('nature:' + nature.id); if (banReason) { return `${set.name}'s nature ${nature.name} is ${banReason}.`; } if (banReason === '') return null; banReason = ruleTable.check('allnatures'); if (banReason) { return `${set.name}'s nature ${nature.name} is not in the list of allowed natures.`; } // obtainability if (nature.isNonstandard) { banReason = ruleTable.check('pokemontag:' + toID(nature.isNonstandard)); if (banReason) { return `${set.name}'s nature ${nature.name} is tagged ${nature.isNonstandard}, which is ${banReason}.`; } if (banReason === '') return null; banReason = ruleTable.check('nonexistent', setHas); if (banReason) { if (['Past', 'Future'].includes(nature.isNonstandard)) { return `${set.name}'s nature ${nature.name} does not exist in Gen ${dex.gen}.`; } return `${set.name}'s nature ${nature.name} does not exist in this game.`; } if (banReason === '') return null; } return null; } validateEvent( set: PokemonSet, setSources: PokemonSources, eventData: EventInfo, eventSpecies: Species ): true | undefined; validateEvent( set: PokemonSet, setSources: PokemonSources, eventData: EventInfo, eventSpecies: Species, because: string, from?: string ): string[] | undefined; /** * Returns array of error messages if invalid, undefined if valid * * If `because` is not passed, instead returns true if invalid. */ validateEvent( set: PokemonSet, setSources: PokemonSources, eventData: EventInfo, eventSpecies: Species, because = ``, from = `from an event` ) { const dex = this.dex; let name = set.species; const species = dex.species.get(set.species); const maxSourceGen = this.ruleTable.has('allowtradeback') ? Utils.clampIntRange(dex.gen + 1, 1, 8) : dex.gen; if (!eventSpecies) eventSpecies = species; if (set.name && set.species !== set.name && species.baseSpecies !== set.name) name = `${set.name} (${set.species})`; const fastReturn = !because; if (eventData.from) from = `from ${eventData.from}`; const etc = `${because} ${from}`; const problems = []; if (dex.gen < 8 && this.minSourceGen > eventData.generation) { if (fastReturn) return true; problems.push(`This format requires Pokemon from gen ${this.minSourceGen} or later and ${name} is from gen ${eventData.generation}${etc}.`); } if (maxSourceGen < eventData.generation) { if (fastReturn) return true; problems.push(`This format is in gen ${dex.gen} and ${name} is from gen ${eventData.generation}${etc}.`); } if (eventData.japan && dex.currentMod !== 'gen1jpn') { if (fastReturn) return true; problems.push(`${name} has moves from Japan-only events, but this format simulates International Yellow/Crystal which can't trade with Japanese games.`); } if (eventData.level && (set.level || 0) < eventData.level) { if (fastReturn) return true; problems.push(`${name} must be at least level ${eventData.level}${etc}.`); } if ((eventData.shiny === true && !set.shiny) || (!eventData.shiny && set.shiny)) { if (fastReturn) return true; const shinyReq = eventData.shiny ? ` be shiny` : ` not be shiny`; problems.push(`${name} must${shinyReq}${etc}.`); } if (eventData.gender) { if (set.gender && eventData.gender !== set.gender) { if (fastReturn) return true; problems.push(`${name}'s gender must be ${eventData.gender}${etc}.`); } } const canMint = dex.gen > 7; if (eventData.nature && eventData.nature !== set.nature && !canMint) { if (fastReturn) return true; problems.push(`${name} must have a ${eventData.nature} nature${etc} - Mints are only available starting gen 8.`); } let requiredIVs = 0; if (eventData.ivs) { /** In Gen 7+, IVs can be changed to 31 */ const canBottleCap = dex.gen >= 7 && set.level >= (dex.gen < 9 ? 100 : 50); if (!set.ivs) set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; let statName: StatID; for (statName in eventData.ivs) { if (canBottleCap && set.ivs[statName] === 31) continue; if (set.ivs[statName] !== eventData.ivs[statName]) { if (fastReturn) return true; problems.push(`${name} must have ${eventData.ivs[statName]} ${Dex.stats.names[statName]} IVs${etc}.`); } } if (canBottleCap) { // IVs can be overridden but Hidden Power type can't if (Object.keys(eventData.ivs).length >= 6) { const requiredHpType = dex.getHiddenPower(eventData.ivs as StatsTable).type; if (set.hpType && set.hpType !== requiredHpType) { if (fastReturn) return true; problems.push(`${name} can only have Hidden Power ${requiredHpType}${etc}.`); } set.hpType = requiredHpType; } } } else { requiredIVs = eventData.perfectIVs || 0; } if (requiredIVs && set.ivs) { // Legendary Pokemon must have at least 3 perfect IVs in gen 6 // Events can also have a certain amount of guaranteed perfect IVs let perfectIVs = 0; let statName: StatID; for (statName in set.ivs) { if (set.ivs[statName] >= 31) perfectIVs++; } if (perfectIVs < requiredIVs) { if (fastReturn) return true; if (eventData.perfectIVs) { problems.push(`${name} must have at least ${requiredIVs} perfect IVs${etc}.`); } } // The perfect IV count affects Hidden Power availability if (dex.gen >= 3 && requiredIVs >= 3 && set.hpType === 'Fighting') { if (fastReturn) return true; problems.push(`${name} can't use Hidden Power Fighting because it must have at least three perfect IVs${etc}.`); } else if ( dex.gen >= 3 && requiredIVs >= 5 && set.hpType && !['Dark', 'Dragon', 'Electric', 'Steel', 'Ice'].includes(set.hpType) ) { if (fastReturn) return true; problems.push(`${name} can only use Hidden Power Dark/Dragon/Electric/Steel/Ice because it must have at least 5 perfect IVs${etc}.`); } } const ruleTable = this.ruleTable; if (ruleTable.has('obtainablemoves')) { const ssMaxSourceGen = setSources.maxSourceGen(); const tradebackEligible = dex.gen === 2 && (species.gen === 1 || eventSpecies.gen === 1); if (ssMaxSourceGen && eventData.generation > ssMaxSourceGen && !tradebackEligible) { if (fastReturn) return true; problems.push(`${name} must not have moves only learnable in gen ${ssMaxSourceGen}${etc}.`); } if (eventData.from === "Gen 5 Dream World" && setSources.dreamWorldMoveCount > 1) { problems.push(`${name} can only have one Dream World move.`); } } if (ruleTable.has('obtainableabilities')) { if (dex.gen <= 5 && eventData.abilities && eventData.abilities.length === 1 && !eventData.isHidden) { if (species.name === eventSpecies.name) { // has not evolved, abilities must match const requiredAbility = dex.abilities.get(eventData.abilities[0]).name; if (set.ability !== requiredAbility) { if (fastReturn) return true; problems.push(`${name} must have ${requiredAbility}${etc}.`); } } else { // has evolved const ability1 = dex.abilities.get(eventSpecies.abilities['1']); if (ability1.gen && eventData.generation >= ability1.gen) { // pokemon had 2 available abilities in the gen the event happened // ability is restricted to a single ability slot const requiredAbilitySlot = (toID(eventData.abilities[0]) === ability1.id ? 1 : 0); const requiredAbility = dex.abilities.get(species.abilities[requiredAbilitySlot] || species.abilities['0']).name; if (set.ability !== requiredAbility) { const originalAbility = dex.abilities.get(eventData.abilities[0]).name; if (fastReturn) return true; problems.push(`${name} must have ${requiredAbility}${because} from a ${originalAbility} ${eventSpecies.name} event.`); } } } } if (species.abilities['H']) { const isHidden = (set.ability === species.abilities['H']); if (!isHidden && eventData.isHidden && dex.gen <= 8) { if (fastReturn) return true; problems.push(`${name} must have its Hidden Ability${etc}.`); } const canUseAbilityPatch = dex.gen >= 8 && this.format.mod !== 'gen8dlc1'; if (isHidden && !eventData.isHidden && !canUseAbilityPatch) { if (fastReturn) return true; problems.push(`${name} must not have its Hidden Ability${etc}.`); } } } if (problems.length) return problems; if (eventData.gender) set.gender = eventData.gender; } allSources(species?: Species) { let minSourceGen = this.minSourceGen; if (this.dex.gen >= 3 && minSourceGen < 3) minSourceGen = 3; if (species) minSourceGen = Math.max(minSourceGen, species.gen); const maxSourceGen = this.ruleTable.has('allowtradeback') ? Utils.clampIntRange(this.dex.gen + 1, 1, 8) : this.dex.gen; return new PokemonSources(maxSourceGen, minSourceGen); } validateMoves( species: Species, moves: string[], setSources: PokemonSources, set?: Partial, name: string = species.name, moveLegalityWhitelist: { [k: string]: true | undefined } = {} ) { const dex = this.dex; const ruleTable = this.ruleTable; const problems = []; const checkCanLearn = (ruleTable.checkCanLearn?.[0] || this.checkCanLearn); for (const moveName of moves) { const move = dex.moves.get(moveName); if (moveLegalityWhitelist[move.id]) continue; const problem = checkCanLearn.call(this, move, species, setSources, set); if (problem) { problems.push(`${name}${problem}`); break; } } if (setSources.size() && setSources.moveEvoCarryCount > 3) { if (setSources.sourcesBefore < 6) setSources.sourcesBefore = 0; setSources.sources = setSources.sources.filter( source => source.charAt(1) === 'E' && parseInt(source.charAt(0)) >= 6 ); if (!setSources.size()) { problems.push(`${name} needs to know ${species.evoMove || 'a Fairy-type move'} to evolve, so it can only know 3 other moves from ${species.prevo}.`); } } if (problems.length) return problems; if (setSources.isHidden) { setSources.sources = setSources.sources.filter( source => parseInt(source.charAt(0)) >= 5 ); if (setSources.sourcesBefore < 5) setSources.sourcesBefore = 0; const canUseAbilityPatch = dex.gen >= 8 && this.format.mod !== 'gen8dlc1'; if (!setSources.size() && !canUseAbilityPatch) { problems.push(`${name} has a hidden ability - it can't have moves only learned before gen 5.`); return problems; } } if (setSources.babyOnly && setSources.sources.length) { const baby = dex.species.get(setSources.babyOnly); const babyEvo = toID(baby.evos[0]); setSources.sources = setSources.sources.filter(source => { if (source.charAt(1) === 'S') { const sourceId = source.split(' ')[1]; if (sourceId !== baby.id) return false; } if (source.charAt(1) === 'E') { if (babyEvo && source.slice(2) === babyEvo) return false; } if (source.charAt(1) === 'D') { if (babyEvo && source.slice(2) === babyEvo) return false; } return true; }); if (!setSources.size()) { problems.push(`${name}'s event/egg moves are from an evolution, and are incompatible with its moves from ${baby.name}.`); } } if (setSources.babyOnly && setSources.size() && this.gen > 2) { // there do theoretically exist evo/tradeback incompatibilities in // gen 2, but those are very complicated to validate and should be // handled separately anyway, so for now we just treat them all as // legal (competitively relevant ones can be manually banned) const baby = dex.species.get(setSources.babyOnly); setSources.sources = setSources.sources.filter(source => { if (baby.gen > parseInt(source.charAt(0)) && !source.startsWith('1ST')) return false; if (baby.gen > 2 && source === '7V') return false; return true; }); if (setSources.sourcesBefore < baby.gen) setSources.sourcesBefore = 0; if (!setSources.size()) { problems.push(`${name} has moves from before Gen ${baby.gen}, which are incompatible with its moves from ${baby.name}.`); } } return problems; } /** * Returns a list of problems regarding a Pokemon's avilability in Pokemon GO (empty list if no problems) * If the Pokemon cannot be obtained from Pokemon GO, returns null */ validatePokemonGo( species: Species, set: PokemonSet, setSources: PokemonSources, name: string = species.name, ): string[] | null { let problems = []; let minLevel = 50; // maximum level a Pokemon can be in Pokemon GO let minIVs = 15; // IVs range from 0 to 15 in Pokemon GO const dex = this.dex; const pokemonGoData = dex.species.getPokemonGoData(species.id); if (dex.gen < 8 || this.format.mod === 'gen8dlc1') return null; if (!pokemonGoData) { // Handles forms and evolutions not obtainable from Pokemon GO const otherSpecies = this.dex.species.learnsetParent(species); // If a Pokemon is somehow not obtainable from Pokemon GO and it must be leveled up to be evolved, // validation for the game should stop because it's more optimal to get the Pokemon outside of the game if (otherSpecies && !species.evoLevel) { const otherProblems = this.validatePokemonGo(otherSpecies, set, setSources, name); if (otherProblems) { problems = otherProblems; } else { return null; } } else { return null; } } else { const pokemonGoSources = pokemonGoData.encounters; // should never happen if (!pokemonGoSources) throw new Error(`Species with no Pokemon GO data: ${species.id}`); if (set.shiny) name = "Shiny " + name; if (set.shiny && pokemonGoSources.includes('noshiny')) { problems.push(`${name} is not obtainable from Pokemon GO.`); } else { if (pokemonGoSources.includes('wild')) { minLevel = 1; minIVs = 0; } if (pokemonGoSources.includes('egg')) { /** * A Pokemon's level when hatched is determined by the trainer's level when it is obtained * It is no longer possible for new accounts to obtain eggs at level 1 because they will have reached * level 2 by the time they can spin a PokeStop. However, it might be possible for a sleeper account * from before XP changes to get a level 1 egg from spinning a PokeStop that sends the account to * level 2, but this needs research */ minLevel = Math.min(minLevel, 2); minIVs = Math.min(minIVs, 10); } if (pokemonGoSources.includes('12kmegg')) { minLevel = Math.min(minLevel, 8); minIVs = Math.min(minIVs, 10); } if (pokemonGoSources.includes('raid')) { minLevel = Math.min(minLevel, 20); minIVs = Math.min(minIVs, 10); } if (species.id === 'mewtwo' && set.level >= 20) { // A bug allowed Mewtwo to be encountered with an IV floor of 0 from GO Battle League minIVs = Math.min(minIVs, 0); } if (pokemonGoSources.includes('research')) { if (species.id === 'cresselia') { // For some reason, Cresselia from research has a possible level 10 source minLevel = Math.min(minLevel, 10); } else { minLevel = Math.min(minLevel, 15); } minIVs = Math.min(minIVs, 10); } if (pokemonGoSources.includes('giovanni') && !set.shiny) { /** * Purified Pokemon can be leveled down to level 8 after trading; they are forced to * special trades, but currently all Giovanni Shadow Pokemon are already forced special trades */ minLevel = Math.min(minLevel, 8); minIVs = Math.min(minIVs, 1); if (set.level < 12) setSources.pokemonGoSource = "purified"; } // Attempt to trade the Pokemon to reduce level and IVs if (!pokemonGoSources.includes('notrade')) { // Special trades require a good friend // Trading with a friend of this level has an IV floor of 1 // Note that (non-shiny) Deoxys could be traded for a short time when it was introduced if (!set.shiny || species.id !== 'deoxys') { const specialTrade = pokemonGoSources.includes('specialtrade') || set.shiny; minLevel = Math.min(minLevel, 12); minIVs = Math.min(minIVs, specialTrade ? 1 : 0); } } if (set.level < minLevel) { problems.push(`${name} must be at least level ${minLevel} to be from Pokemon GO.`); } const ivs = set.ivs || TeamValidator.fillStats(null, 31); const postTransferMinIVs = minIVs * 2 + 1; let IVsTooLow = false; let hasEvenIVs = false; for (const stat in ivs) { if (stat === 'spe') continue; if (ivs[stat as 'hp'] < postTransferMinIVs) IVsTooLow = true; if (ivs[stat as 'hp'] % 2 === 0) hasEvenIVs = true; } if (IVsTooLow) { problems.push(`${name} must have at least ${postTransferMinIVs} ` + (postTransferMinIVs === 1 ? `IV` : `IVs`) + ` in non-Speed stats to be from Pokemon GO.`); } if (hasEvenIVs) { problems.push(`${name} must have odd non-Speed IVs to be from Pokemon GO.`); } const canBottleCap = dex.gen >= 7 && set.level >= (dex.gen < 9 ? 100 : 50); if (ivs.atk !== ivs.spa && !(canBottleCap && (ivs.atk === 31 || ivs.spa === 31))) { problems.push(`${name}'s Atk and Sp. Atk IVs must match to be from Pokemon GO.`); } if (ivs.def !== ivs.spd && !(canBottleCap && (ivs.def === 31 || ivs.spd === 31))) { problems.push(`${name}'s Def and Sp. Def IVs must match to be from Pokemon GO.`); } } } return problems; } omCheckCanLearn( move: Move, s: Species, setSources = this.allSources(s), set: Partial = {}, problem = `${set.name || s.name} can't learn ${move.name}`, ): string | null { if (!this.ruleTable.checkCanLearn?.[0]) return problem; const baseCheckCanLearn = this.checkCanLearn; // tell the custom move legality check that the move is illegal by default this.checkCanLearn = () => problem; const omVerdict = this.ruleTable.checkCanLearn[0].call(this, move, s, setSources, set); this.checkCanLearn = baseCheckCanLearn; return omVerdict; } /** Returns null if you can learn the move, or a string explaining why you can't learn it */ checkCanLearn( move: Move, originalSpecies: Species, setSources = this.allSources(originalSpecies), set: Partial = {} ): string | null { const dex = this.dex; if (!setSources.size()) throw new Error(`Bad sources passed to checkCanLearn`); move = dex.moves.get(move); const moveid = move.id; const baseSpecies = dex.species.get(originalSpecies); const format = this.format; const ruleTable = this.ruleTable; const level = set.level || 100; const canLearnSpecies: ID[] = []; let cantLearnReason = null; let limit1 = true; let sketch = false; let blockedHM = false; let babyOnly = ''; let minLearnGen = dex.gen; // This is a pretty complicated algorithm // Abstractly, what it does is construct the union of sets of all // possible ways this pokemon could be obtained, and then intersect // it with a the pokemon's existing set of all possible ways it could // be obtained. If this intersection is non-empty, the move is legal. // set of possible sources of a pokemon with this move const moveSources = new PokemonSources(); /** * The format doesn't allow Pokemon traded from the future * (This is everything except in Gen 1 Tradeback) */ const noFutureGen = !ruleTable.has('allowtradeback'); /** * The format allows Sketch to copy moves in Gen 8 */ const canSketchPostGen7Moves = ruleTable.has('sketchpostgen7moves') || this.dex.currentMod === 'gen8bdsp'; let tradebackEligible = false; const fullLearnset = dex.species.getFullLearnset(originalSpecies.id); if (!fullLearnset.length) { // It's normal for a nonstandard species not to have learnset data // Formats should replace the `Obtainable Moves` rule if they want to // allow pokemon without learnsets. return ` can't learn any moves at all.`; } for (const { species, learnset } of fullLearnset) { if (dex.gen <= 2 && species.gen === 1) tradebackEligible = true; const checkingPrevo = species.baseSpecies !== originalSpecies.baseSpecies; if (checkingPrevo && !moveSources.size()) { if (!setSources.babyOnly || !species.prevo) { babyOnly = species.id; } } const formeCantInherit = dex.species.eggMovesOnly(species, baseSpecies); if (formeCantInherit && dex.gen < 9) break; let sources = learnset[moveid] || []; if (moveid === 'sketch') { sketch = true; } else if (learnset['sketch']) { if (move.flags['nosketch'] || move.isZ || move.isMax) { cantLearnReason = `can't be Sketched.`; } else if (move.gen > 7 && !canSketchPostGen7Moves && (dex.gen === 8 || (dex.gen === 9 && ['gen9dlc1', 'gen9predlc'].includes(format.mod)))) { cantLearnReason = `can't be Sketched because it's a Gen ${move.gen} move and Sketch isn't available in Gen ${move.gen}.`; } else { if (!sources.length || !moveSources.size()) sketch = true; sources = [...learnset['sketch'], ...sources]; } } for (let learned of sources) { // Every `learned` represents a single way a pokemon might // learn a move. This can be handled one of several ways: // `continue` // means we can't learn it // `return null` // means we can learn it with no restrictions // (there's a way to just teach any pokemon of this species // the move in the current gen, like a TM.) // `moveSources.add(source)` // means we can learn it only if obtained that exact way described // in source // `moveSources.addGen(learnedGen)` // means we can learn it only if obtained at or before learnedGen // (i.e. get the pokemon however you want, transfer to that gen, // teach it, and transfer it to the current gen.) const learnedGen = parseInt(learned.charAt(0)); if (formeCantInherit && (learned.charAt(1) !== 'E' || learnedGen < 9)) continue; if (setSources.learnsetDomain && !setSources.learnsetDomain.includes(`${learnedGen}${toID(species.baseSpecies)}`) && (learned.charAt(1) !== 'E' || learnedGen < 8) ) { if (!cantLearnReason) { cantLearnReason = `is incompatible with ${(setSources.restrictiveMoves || []).join(', ')}.`; } continue; } if (learnedGen < this.minSourceGen) { if (!cantLearnReason) { cantLearnReason = `can't be transferred from Gen ${learnedGen} to ${this.minSourceGen}.`; } continue; } if (noFutureGen && learnedGen > dex.gen) { if (!cantLearnReason) { cantLearnReason = `can't be transferred from Gen ${learnedGen} to ${dex.gen}.`; } continue; } if ( baseSpecies.evoRegion === 'Alola' && checkingPrevo && learnedGen >= 8 && (dex.gen < 9 || learned.charAt(1) !== 'E') ) { cantLearnReason = `is from a ${species.name} that can't be transferred to USUM to evolve into ${baseSpecies.name}.`; continue; } const canUseAbilityPatch = dex.gen >= 8 && format.mod !== 'gen8dlc1'; if ( learnedGen < 7 && setSources.isHidden && !canUseAbilityPatch && !dex.mod(`gen${learnedGen}`).species.get(baseSpecies.name).abilities['H'] ) { cantLearnReason = `can only be learned in gens without Hidden Abilities.`; continue; } const ability = dex.abilities.get(set.ability); if (dex.gen < 6 && ability.gen > learnedGen && !checkingPrevo) { // You can evolve a transfered mon to reroll for its new Ability. cantLearnReason = `is learned in gen ${learnedGen}, but the Ability ${ability.name} did not exist then.`; continue; } if (species.isNonstandard !== 'CAP') { // HMs can't be transferred if (dex.gen >= 4 && learnedGen <= 3 && [ 'cut', 'fly', 'surf', 'strength', 'flash', 'rocksmash', 'waterfall', 'dive', ].includes(moveid)) { cantLearnReason = `can't be transferred from Gen 3 to 4 because it's an HM move.`; continue; } if (dex.gen >= 5 && learnedGen <= 4 && [ 'cut', 'fly', 'surf', 'strength', 'rocksmash', 'waterfall', 'rockclimb', ].includes(moveid)) { cantLearnReason = `can't be transferred from Gen 4 to 5 because it's an HM move.`; continue; } // Defog and Whirlpool can't be transferred together if (dex.gen >= 5 && ['defog', 'whirlpool'].includes(moveid) && learnedGen <= 4) blockedHM = true; } if (learned.charAt(1) === 'L') { // special checking for level-up moves if (level >= parseInt(learned.substr(2)) || learnedGen === 7) { // we're past the required level to learn it // (gen 7 level-up moves can be relearnered at any level) // falls through to LMT check below } else if (level >= 5 && learnedGen === 3 && species.canHatch) { // Pomeg Glitch learned = `${learnedGen}Epomeg` as MoveSource; } else if (species.gender !== 'N' && learnedGen >= 2 && species.canHatch && !setSources.isFromPokemonGo) { // available as egg move if (species.gender === 'M' && !this.motherCanLearn(toID(species.mother), moveid)) { // male-only Pokemon can have level-up egg moves if it can have a mother that learns the move cantLearnReason = `is learned at level ${parseInt(learned.substr(2))}.`; continue; } learned = `${learnedGen}Eany` as MoveSource; // falls through to E check below } else { // this move is unavailable, skip it cantLearnReason = `is learned at level ${parseInt(learned.substr(2))}.`; continue; } } // Gen 8+ egg moves can be taught to any pokemon from any source if (learnedGen >= 8 && learned.charAt(1) === 'E' && learned.slice(1) !== 'Eany' && learned.slice(1) !== 'Epomeg' || 'LMTR'.includes(learned.charAt(1))) { if (learnedGen === dex.gen && learned.charAt(1) !== 'R') { // current-gen level-up, TM or tutor moves: // always available if (!(learnedGen >= 8 && learned.charAt(1) === 'E') && babyOnly && setSources.isFromPokemonGo && species.evoLevel) { cantLearnReason = `is from a prevo, which is incompatible with its Pokemon GO origin.`; continue; } if (!moveSources.moveEvoCarryCount && !babyOnly) return null; } // past-gen level-up, TM, or tutor moves: // available as long as the source gen was or was before this gen if (learned.charAt(1) === 'R') { moveSources.restrictedMove = moveid; } limit1 = false; moveSources.addGen(learnedGen); } else if (learned.charAt(1) === 'E') { // egg moves: // only if hatched from an egg let limitedEggMove: ID | null | undefined = undefined; if (learned.slice(1) === 'Eany') { if (species.gender === 'F') { limitedEggMove = move.id; moveSources.levelUpEggMoves = [move.id]; } else { limitedEggMove = null; } } else if (learned.slice(1) === 'Epomeg') { // Pomeg glitched moves have to be from an egg but since they aren't true egg moves, // there should be no breeding restrictions moveSources.pomegEggMoves = [move.id]; } else if (learnedGen < 6 || (species.mother && !this.motherCanLearn(toID(species.mother), moveid))) { limitedEggMove = move.id; } learned = `${learnedGen}E${species.prevo ? species.id : ''}` as MoveSource; if (tradebackEligible && learnedGen === 2 && move.gen <= 1) { // can tradeback moveSources.add(`1ET${learned.slice(2)}`, limitedEggMove); } moveSources.add(learned, limitedEggMove); } else if (learned.charAt(1) === 'S') { // event moves: // only if that was the source // Event Pokémon: // Available as long as the past gen can get the Pokémon and then trade it back. if (tradebackEligible && learnedGen === 2 && move.gen <= 1) { // can tradeback moveSources.add(`1ST${learned.slice(2)} ${species.id}`); } moveSources.add(`${learned} ${species.id}`); const eventLearnset = dex.species.getLearnsetData(species.id); if (eventLearnset.eventData?.[parseInt(learned.charAt(2))].emeraldEventEgg && learnedGen === 3) { moveSources.pomegEventEgg = `${learned} ${species.id}`; } } else if (learned.charAt(1) === 'D') { // DW moves: // only if that was the source moveSources.add(`${learned}${species.id}`); // If a DW move can be learned through some means other than DW, // it should not be treated as a DW move if (!moveSources.sourcesBefore) moveSources.dreamWorldMoveCount++; } else if (learned.charAt(1) === 'V' && this.minSourceGen < learnedGen) { // Virtual Console or Let's Go transfer moves: // only if that was the source if (learned === '8V' && setSources.isFromPokemonGo && babyOnly && species.evoLevel) { cantLearnReason = `is from a prevo, which is incompatible with its Pokemon GO origin.`; continue; } moveSources.add(learned); } if (learned.charAt(1) === 'E' && learnedGen >= 8 && !canLearnSpecies.includes(toID(baseSpecies.baseSpecies))) { canLearnSpecies.push(toID(baseSpecies.baseSpecies)); } if (!canLearnSpecies.includes(toID(species.baseSpecies))) canLearnSpecies.push(toID(species.baseSpecies)); minLearnGen = Math.min(minLearnGen, learnedGen); } if (ruleTable.has('mimicglitch') && species.gen < 5) { // include the Mimic Glitch when checking this mon's learnset const glitchMoves = ['metronome', 'copycat', 'transform', 'mimic', 'assist']; let getGlitch = false; for (const i of glitchMoves) { if (learnset[i]) { if (!(i === 'mimic' && dex.abilities.get(set.ability).gen === 4 && !species.prevo)) { getGlitch = true; break; } } } if (getGlitch) { moveSources.addGen(4); if (move.gen < 5) { limit1 = false; } } } if (!moveSources.size()) { if ( (species.evoType === 'levelMove' && species.evoMove !== move.name) || (species.id === 'sylveon' && move.type !== 'Fairy') ) { moveSources.moveEvoCarryCount = 1; } } } if (limit1 && sketch) { // limit 1 sketch move if (setSources.sketchMove) { return ` can't Sketch ${move.name} and ${setSources.sketchMove} because it can only Sketch 1 move.`; } setSources.sketchMove = move.name; } if (blockedHM) { // Limit one of Defog/Whirlpool to be transferred if (setSources.hm) return ` can't simultaneously transfer Defog and Whirlpool from Gen 4 to 5.`; setSources.hm = moveid; } if (!setSources.restrictiveMoves) { setSources.restrictiveMoves = []; } if (!setSources.restrictiveMoves.includes(move.name)) { setSources.restrictiveMoves.push(move.name); } const checkedSpecies = babyOnly ? fullLearnset[fullLearnset.length - 1].species : baseSpecies; if (checkedSpecies && setSources.isFromPokemonGo && (setSources.pokemonGoSource === 'purified' || checkedSpecies.id === 'mew')) { // Pokemon that cannot be sent from Pokemon GO to Let's Go can only access Let's Go moves through HOME // It can only obtain a chain of four level up moves and cannot have TM moves const pokemonGoData = dex.species.getPokemonGoData(checkedSpecies.id); if (pokemonGoData.LGPERestrictiveMoves) { let levelUpMoveCount = 0; const restrictiveMovesToID = []; for (const moveName of setSources.restrictiveMoves) { restrictiveMovesToID.push(toID(moveName)); } for (const restrictiveMove in pokemonGoData.LGPERestrictiveMoves) { const moveLevel = pokemonGoData.LGPERestrictiveMoves[restrictiveMove]; if (toID(move) === restrictiveMove) { if (!moveLevel) { return `'s move ${move.name} is incompatible with its Pokemon GO origin.`; } else if (set.level && set.level < moveLevel) { return ` must be at least level ${moveLevel} to learn ${move.name} due to its Pokemon GO origin.`; } } if (levelUpMoveCount) levelUpMoveCount++; if (restrictiveMovesToID.includes(restrictiveMove)) { if (!levelUpMoveCount) { levelUpMoveCount++; } else if (levelUpMoveCount > 4) { return `'s moves ${(setSources.restrictiveMoves || []).join(', ')} are incompatible with its Pokemon GO origin.`; } } } } } let nextSpecies; nextSpecies = baseSpecies; let speciesCount = 0; if (!tradebackEligible) { if (!dex.species.getLearnsetData(nextSpecies.id).learnset) { nextSpecies = dex.species.get(nextSpecies.changesFrom || nextSpecies.baseSpecies); } while (nextSpecies) { for (let gen = nextSpecies.gen; gen <= dex.gen; gen++) { /** * Case 1: The species can learn the move - allow moves of the species from all gens * Case 2: Both prevo and evo can learn the move - same as case 1 * Case 3: Prevo-only move - allow moves of the species from the min gen and later * Case 4: Evo-only move - allow moves of the species from the max gen and before */ const baseSpeciesID = toID(nextSpecies.baseSpecies); if (canLearnSpecies.includes(baseSpeciesID) || (0 < speciesCount && speciesCount < canLearnSpecies.length) || (speciesCount === 0 && gen >= minLearnGen) || (speciesCount === canLearnSpecies.length && gen <= moveSources.sourcesBefore) ) { if (!moveSources.learnsetDomain) moveSources.learnsetDomain = []; moveSources.learnsetDomain.push(`${gen}${baseSpeciesID}`); } } if (canLearnSpecies.includes(nextSpecies.id)) speciesCount++; nextSpecies = dex.species.learnsetParent(nextSpecies); } } // Now that we have our list of possible sources, intersect it with the current list if (!moveSources.size()) { if (cantLearnReason) return `'s move ${move.name} ${cantLearnReason}`; return ` can't learn ${move.name}.`; } const eggSources = moveSources.sources.filter(source => source.charAt(1) === 'E'); if (dex.gen >= 3 && eggSources.length && moveSources.limitedEggMoves === null && moveSources.sourcesBefore) { moveSources.possiblyLimitedEggMoves = [toID(`${moveSources.sourcesBefore}${move.id}`)]; } const backupSources = setSources.sources; const backupSourcesBefore = setSources.sourcesBefore; setSources.intersectWith(moveSources); if (!setSources.size()) { // pretend this pokemon didn't have this move: // prevents a crash if OMs override `checkCanLearn` to keep validating after an error setSources.sources = backupSources; setSources.sourcesBefore = backupSourcesBefore; if (setSources.isFromPokemonGo) return `'s move ${move.name} is incompatible with its Pokemon GO origin.`; return `'s moves ${(setSources.restrictiveMoves || []).join(', ')} are incompatible.`; } if (babyOnly) setSources.babyOnly = babyOnly; return null; } static fillStats(stats: SparseStatsTable | null, fillNum = 0): StatsTable { const filledStats: StatsTable = { hp: fillNum, atk: fillNum, def: fillNum, spa: fillNum, spd: fillNum, spe: fillNum }; if (stats) { let statName: StatID; for (statName in filledStats) { const stat = stats[statName]; if (typeof stat === 'number') filledStats[statName] = stat; } } return filledStats; } static get(format: string | Format) { return new TeamValidator(format); } }