diff --git a/config/formats.js b/config/formats.js index 038d7f4423..aac3b562ea 100644 --- a/config/formats.js +++ b/config/formats.js @@ -21,7 +21,7 @@ let Formats = [ mod: 'gen7', team: 'random', - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Obtainable', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Unrated Random Battle", @@ -30,7 +30,7 @@ let Formats = [ team: 'random', challengeShow: false, rated: false, - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Obtainable', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] OU", @@ -41,7 +41,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['Pokemon', 'Standard', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Team Preview'], banlist: ['Uber', 'Arena Trap', 'Power Construct', 'Shadow Tag', 'Baton Pass'], }, { @@ -64,7 +64,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['Pokemon', 'Standard', 'Team Preview', 'Mega Rayquaza Clause'], + ruleset: ['Obtainable', 'Standard', 'Team Preview', 'Mega Rayquaza Clause'], banlist: ['Baton Pass'], }, { @@ -127,7 +127,7 @@ let Formats = [ mod: 'gen7', maxLevel: 5, - ruleset: ['Pokemon', 'Standard', 'Swagger Clause', 'Team Preview', 'Little Cup'], + ruleset: ['Obtainable', 'Standard', 'Swagger Clause', 'Team Preview', 'Little Cup'], banlist: [ 'Aipom', 'Cutiefly', 'Drifloon', 'Gligar', 'Gothita', 'Meditite', 'Misdreavus', 'Murkrow', 'Porygon', 'Scyther', 'Sneasel', 'Swirlix', 'Tangela', 'Trapinch', 'Vulpix-Base', 'Wingull', 'Yanma', @@ -164,7 +164,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['Pokemon', 'Standard', 'Swagger Clause', 'Same Type Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Swagger Clause', 'Same Type Clause', 'Team Preview'], banlist: [ 'Aegislash', 'Arceus', 'Blaziken', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Dialga', 'Genesect', 'Gengar-Mega', 'Giratina', 'Groudon', 'Ho-Oh', 'Hoopa-Unbound', 'Kangaskhan-Mega', 'Kartana', 'Kyogre', 'Kyurem-White', 'Lucario-Mega', 'Lugia', 'Lunala', 'Magearna', @@ -182,8 +182,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['Pokemon', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased'], + ruleset: ['Obtainable', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] 1v1", @@ -200,9 +199,9 @@ let Formats = [ battle: 1, }, allowMultisearch: true, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], banlist: [ - 'Illegal', 'Unreleased', 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', + 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', 'Ho-Oh', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', 'Mimikyu', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', 'Snorlax', 'Solgaleo', 'Tapu Koko', 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Perish Song', 'Detect + Fightinium Z', @@ -255,7 +254,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['[Gen 7] OU', 'Allow CAP'], + ruleset: ['[Gen 7] OU', '+CAP'], banlist: [ 'Aurumoth + Quiver Dance', 'Crucibelle + Head Smash', 'Crucibelle + Low Kick', 'Tomohawk + Earth Power', 'Tomohawk + Reflect', @@ -268,7 +267,7 @@ let Formats = [ mod: 'gen7', searchShow: false, maxLevel: 5, - ruleset: ['[Gen 7] LC', 'Allow CAP'], + ruleset: ['[Gen 7] LC', '+CAP'], }, { name: "[Gen 7] Battle Spot Singles", @@ -285,7 +284,7 @@ let Formats = [ validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], requirePentagon: true, }, { @@ -298,7 +297,7 @@ let Formats = [ validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], banlist: ['Sonic Boom', 'Dragon Rage', 'Type: Null', 'Poipole'], onValidateSet(set) { let allowedNonLittleCupMons = [ @@ -331,8 +330,7 @@ let Formats = [ validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Nickname Clause', 'Team Preview', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal'], + ruleset: ['Obtainable', 'Nickname Clause', 'Team Preview', 'Cancel Mod'], }, { name: "[Gen 7] Custom Game", @@ -364,7 +362,7 @@ let Formats = [ mod: 'gen7', gameType: 'doubles', team: 'random', - ruleset: ['PotD', 'Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Obtainable', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Doubles OU", @@ -376,7 +374,7 @@ let Formats = [ mod: 'gen7', gameType: 'doubles', - ruleset: ['Pokemon', 'Standard Doubles', 'Swagger Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard Doubles', 'Swagger Clause', 'Team Preview'], banlist: ['DUber', 'Power Construct', 'Eevium Z', 'Dark Void', 'Gravity ++ Grass Whistle', 'Gravity ++ Hypnosis', 'Gravity ++ Lovely Kiss', 'Gravity ++ Sing', 'Gravity ++ Sleep Powder'], }, { @@ -385,7 +383,7 @@ let Formats = [ mod: 'gen7', gameType: 'doubles', - ruleset: ['Pokemon', 'Standard Doubles', 'Team Preview'], + ruleset: ['Obtainable', 'Standard Doubles', 'Team Preview'], banlist: ['Dark Void'], }, { @@ -408,7 +406,7 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Minimal GBU', 'VGC Timer'], + ruleset: ['Obtainable', 'Minimal GBU', 'VGC Timer'], banlist: ['Unown', 'Dragon Ascent', 'Custap Berry', 'Enigma Berry', 'Jaboca Berry', 'Micle Berry', 'Rowap Berry'], requirePlus: true, onValidateTeam(team) { @@ -438,7 +436,7 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Minimal GBU', 'VGC Timer'], + ruleset: ['Obtainable', 'Minimal GBU', 'VGC Timer'], banlist: ['Unown', 'Dragon Ascent'], requirePlus: true, onValidateTeam(team) { @@ -471,7 +469,7 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Minimal GBU', 'VGC Timer'], + ruleset: ['Obtainable', 'Minimal GBU', 'VGC Timer'], banlist: ['Unown'], requirePlus: true, onValidateTeam(team) { @@ -500,7 +498,7 @@ let Formats = [ battle: 4, }, timer: {starting: 5 * 60, addPerTurn: 0, maxPerTurn: 55, maxFirstTurn: 90, grace: 90, timeoutAutoChoose: true, dcTimerBank: false}, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], banlist: ['Unown', 'Custap Berry', 'Enigma Berry', 'Jaboca Berry', 'Micle Berry', 'Rowap Berry'], requirePlus: true, }, @@ -521,9 +519,9 @@ let Formats = [ battle: 4, }, timer: {starting: 15 * 60, addPerTurn: 0, maxPerTurn: 55, maxFirstTurn: 90, grace: 90, timeoutAutoChoose: true, dcTimerBank: false}, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod', 'Alola Pokedex'], + ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod', 'Alola Pokedex'], banlist: [ - 'Illegal', 'Unreleased', 'Solgaleo', 'Lunala', 'Necrozma', 'Magearna', 'Marshadow', 'Zygarde', 'Mega', + 'Solgaleo', 'Lunala', 'Necrozma', 'Magearna', 'Marshadow', 'Zygarde', 'Mega', 'Custap Berry', 'Enigma Berry', 'Jaboca Berry', 'Micle Berry', 'Rowap Berry', ], requirePlus: true, @@ -543,7 +541,7 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], requirePentagon: true, }, { @@ -560,7 +558,7 @@ let Formats = [ validate: [2, 4], battle: 2, }, - ruleset: ['Pokemon', 'Standard Doubles', 'Accuracy Moves Clause', 'Swagger Clause', 'Z-Move Clause', 'Sleep Clause Mod', 'Team Preview'], + ruleset: ['Obtainable', 'Standard Doubles', 'Accuracy Moves Clause', 'Swagger Clause', 'Z-Move Clause', 'Sleep Clause Mod', 'Team Preview'], banlist: [ 'Arceus', 'Dialga', 'Giratina', 'Groudon', 'Ho-Oh', 'Jirachi', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-White', 'Lugia', 'Lunala', 'Magearna', 'Marshadow', 'Mewtwo', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', @@ -646,13 +644,11 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['[Gen 7] OU'], + ruleset: ['[Gen 7] OU', '!Obtainable Formes', '!Obtainable Abilities', '!Obtainable Moves'], banlist: [ 'Blacephalon', 'Chansey', 'Cresselia', 'Hoopa-Unbound', 'Kartana', 'Kyurem-Black', 'Regigigas', 'Shedinja', 'Slaking', 'Gyaradosite', 'Huge Power', 'Imposter', 'Innards Out', 'Pure Power', 'Speed Boost', 'Water Bubble', 'Assist', 'Chatter', 'Shell Smash', ], - noChangeForme: true, - noChangeAbility: true, // @ts-ignore getEvoFamily(species) { let template = Dex.getTemplate(species); @@ -682,29 +678,14 @@ let Formats = [ this.format.abilityMap = abilityMap; } - // @ts-ignore - this.format.noChangeForme = false; /** @type {string[]} */ let problems = []; - let pkmnRule = Dex.getFormat('pokemon'); - if (pkmnRule.exists && pkmnRule.onChangeSet && pkmnRule.onChangeSet.call(Dex, set, this.format)) { - problems = pkmnRule.onChangeSet.call(Dex, set, this.format) || []; - } - // @ts-ignore - this.format.noChangeForme = true; - - if (problems.length) return problems; let template = Dex.getTemplate(set.species); - if (!template.exists) return [`The Pokemon "${set.species}" does not exist.`]; - if (template.isUnreleased) return [`${template.species} is unreleased.`]; let megaTemplate = Dex.getTemplate(Dex.getItem(set.item).megaStone); if (template.tier === 'Uber' || megaTemplate.tier === 'Uber' || this.format.banlist.includes(template.species)) return [`${megaTemplate.tier === 'Uber' ? megaTemplate.species : template.species} is banned.`]; - let name = set.name; - let ability = Dex.getAbility(set.ability); - if (!ability.exists || ability.isNonstandard || ability.isUnreleased) return [`${name} needs to have a valid ability.`]; // @ts-ignore let pokemonWithAbility = this.format.abilityMap[ability.id]; if (!pokemonWithAbility) return [`"${set.ability}" is not available on a legal Pok\u00e9mon.`]; @@ -824,7 +805,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['Pokemon', 'Ability Clause', 'OHKO Clause', 'Evasion Moves Clause', 'CFZ Clause', 'Sleep Clause Mod', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['-Nonexistent', 'Ability Clause', 'OHKO Clause', 'Evasion Moves Clause', 'CFZ Clause', 'Sleep Clause Mod', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], banlist: ['Groudon-Primal', 'Rayquaza-Mega', 'Arena Trap', 'Contrary', 'Huge Power', 'Illusion', 'Innards Out', 'Magnet Pull', 'Moody', 'Parental Bond', 'Protean', 'Psychic Surge', 'Pure Power', 'Shadow Tag', 'Stakeout', 'Water Bubble', 'Wonder Guard', 'Gengarite', 'Chatter', 'Comatose + Sleep Talk'], }, { @@ -836,7 +817,7 @@ let Formats = [ ], mod: 'mixandmega', - ruleset: ['Pokemon', 'Standard', 'Mega Rayquaza Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Mega Rayquaza Clause', 'Team Preview'], banlist: ['Shadow Tag', 'Gengarite', 'Baton Pass', 'Electrify'], restrictedStones: ['Beedrillite', 'Blazikenite', 'Kangaskhanite', 'Mawilite', 'Medichamite', 'Pidgeotite', 'Ultranecrozium Z'], cannotMega: [ @@ -898,7 +879,7 @@ let Formats = [ ], mod: 'gen7', - ruleset: ['[Gen 7] OU', 'Ability Clause', 'Ignore Illegal Abilities'], + ruleset: ['[Gen 7] OU', 'Ability Clause', '!Obtainable Abilities'], banlist: ['Archeops', 'Dragonite', 'Hoopa-Unbound', 'Kartana', 'Keldeo', 'Kyurem-Black', 'Regigigas', 'Shedinja', 'Slaking', 'Terrakion', 'Victini', 'Weavile'], unbanlist: ['Aegislash', 'Genesect', 'Landorus', 'Metagross-Mega', 'Naganadel'], restrictedAbilities: [ @@ -1079,7 +1060,7 @@ let Formats = [ mod: 'gennext', searchShow: false, challengeShow: false, - ruleset: ['Pokemon', 'Standard NEXT', 'Team Preview'], + ruleset: ['Obtainable', 'Standard NEXT', 'Team Preview'], banlist: ['Uber'], }, @@ -1095,7 +1076,7 @@ let Formats = [ mod: 'letsgo', team: 'random', - ruleset: ['Pokemon', 'Allow AVs', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'Allow AVs', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7 Let's Go] OU", @@ -1105,8 +1086,8 @@ let Formats = [ mod: 'letsgo', forcedLevel: 50, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Evasion Moves Clause', 'OHKO Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], - banlist: ['Illegal', 'Unreleased', 'Uber'], + ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Evasion Moves Clause', 'OHKO Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + banlist: ['Uber'], }, { name: "[Gen 7 Let's Go] Singles No Restrictions", @@ -1116,8 +1097,7 @@ let Formats = [ mod: 'letsgo', searchShow: false, - ruleset: ['Pokemon', 'Allow AVs', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased'], + ruleset: ['Obtainable', 'Allow AVs', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7 Let's Go] Custom Game", @@ -1140,8 +1120,8 @@ let Formats = [ mod: 'letsgo', gameType: 'doubles', forcedLevel: 50, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Evasion Moves Clause', 'OHKO Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], - banlist: ['Illegal', 'Unreleased', 'Mewtwo'], + ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Evasion Moves Clause', 'OHKO Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + banlist: ['Mewtwo'], }, { name: "[Gen 7 Let's Go] Doubles No Restrictions", @@ -1152,8 +1132,7 @@ let Formats = [ mod: 'letsgo', gameType: 'doubles', searchShow: false, - ruleset: ['Pokemon', 'Allow AVs', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased'], + ruleset: ['Obtainable', 'Allow AVs', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], }, // Randomized Metas @@ -1169,7 +1148,7 @@ let Formats = [ mod: 'gen7', team: 'randomFactory', - ruleset: ['Pokemon', 'Sleep Clause Mod', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Mega Rayquaza Clause'], + ruleset: ['Obtainable', 'Sleep Clause Mod', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Mega Rayquaza Clause'], }, { name: "[Gen 7] BSS Factory", @@ -1184,14 +1163,14 @@ let Formats = [ validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], }, { name: "[Gen 7] Monotype Random Battle", mod: 'gen7', team: 'random', - ruleset: ['Pokemon', 'Same Type Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'Same Type Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Super Staff Bros Brawl", @@ -1235,7 +1214,7 @@ let Formats = [ teamLength: { battle: 1, }, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['Obtainable', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], }, { name: "[Gen 7] Challenge Cup 2v2", @@ -1247,7 +1226,7 @@ let Formats = [ battle: 2, }, searchShow: false, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['Obtainable', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], }, { name: "[Gen 7] Hackmons Cup", @@ -1255,7 +1234,7 @@ let Formats = [ mod: 'gen7', team: 'randomHC', - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Doubles Hackmons Cup", @@ -1264,14 +1243,14 @@ let Formats = [ gameType: 'doubles', team: 'randomHC', searchShow: false, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 6] Random Battle", mod: 'gen6', team: 'random', - ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 6] Battle Factory", @@ -1280,42 +1259,42 @@ let Formats = [ mod: 'gen6', team: 'randomFactory', searchShow: false, - ruleset: ['Pokemon', 'Sleep Clause Mod', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Mega Rayquaza Clause'], + ruleset: ['Obtainable', 'Sleep Clause Mod', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Mega Rayquaza Clause'], }, { name: "[Gen 5] Random Battle", mod: 'gen5', team: 'random', - ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 4] Random Battle", mod: 'gen4', team: 'random', - ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 3] Random Battle", mod: 'gen3', team: 'random', - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], }, { name: "[Gen 2] Random Battle", mod: 'gen2', team: 'random', - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], }, { name: "[Gen 1] Random Battle", mod: 'gen1', team: 'random', - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], }, { name: "[Gen 1] Challenge Cup", @@ -1324,7 +1303,7 @@ let Formats = [ team: 'randomCC', searchShow: false, challengeShow: false, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'HP Percentage Mod', 'Cancel Mod'], }, // RoA Spotlight @@ -1343,7 +1322,7 @@ let Formats = [ mod: 'gen5', // searchShow: false, - ruleset: ['Pokemon', 'Team Preview', 'Standard Ubers'], + ruleset: ['Obtainable', 'Team Preview', 'Standard Ubers'], }, { name: "[Gen 6] Monotype", @@ -1354,7 +1333,7 @@ let Formats = [ mod: 'gen6', // searchShow: false, - ruleset: ['Pokemon', 'Standard', 'Swagger Clause', 'Same Type Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Swagger Clause', 'Same Type Clause', 'Team Preview'], banlist: [ 'Aegislash', 'Altaria-Mega', 'Arceus', 'Blaziken', 'Charizard-Mega-X', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Dialga', 'Genesect', 'Gengar-Mega', 'Giratina', 'Greninja', 'Groudon', 'Ho-Oh', 'Hoopa-Unbound', 'Kangaskhan-Mega', 'Kyogre', @@ -1373,7 +1352,7 @@ let Formats = [ mod: 'gen4', // searchShow: false, maxLevel: 5, - ruleset: ['Pokemon', 'Standard', 'Little Cup'], + ruleset: ['Obtainable', 'Standard', 'Little Cup'], banlist: [ 'LC Uber', 'Misdreavus', 'Murkrow', 'Scyther', 'Sneasel', 'Tangela', 'Yanma', 'Berry Juice', 'Deep Sea Tooth', 'Dragon Rage', 'Hypnosis', 'Sonic Boom', @@ -1396,7 +1375,7 @@ let Formats = [ ], mod: 'gen6', - ruleset: ['Pokemon', 'Standard', 'Team Preview', 'Swagger Clause'], + ruleset: ['Obtainable', 'Standard', 'Team Preview', 'Swagger Clause'], banlist: ['Uber', 'Arena Trap', 'Shadow Tag', 'Soul Dew', 'Baton Pass'], }, { @@ -1407,7 +1386,7 @@ let Formats = [ ], mod: 'gen5', - ruleset: ['Pokemon', 'Standard', 'Evasion Abilities Clause', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Evasion Abilities Clause', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], banlist: ['Uber', 'Arena Trap', 'Drizzle ++ Swift Swim', 'Drought ++ Chlorophyll', 'Sand Rush', 'Shadow Tag', 'Soul Dew'], }, { @@ -1419,7 +1398,7 @@ let Formats = [ ], mod: 'gen4', - ruleset: ['Pokemon', 'Standard', 'Baton Pass Clause'], + ruleset: ['Obtainable', 'Standard', 'Baton Pass Clause'], banlist: ['Uber', 'Sand Veil', 'Soul Dew'], }, { @@ -1430,7 +1409,7 @@ let Formats = [ ], mod: 'gen3', - ruleset: ['Pokemon', 'Standard', '3 Baton Pass Clause'], + ruleset: ['Obtainable', 'Standard', '3 Baton Pass Clause'], banlist: ['Uber', 'Smeargle + Baton Pass'], }, { @@ -1441,7 +1420,7 @@ let Formats = [ ], mod: 'gen2', - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], banlist: ['Uber'], }, { @@ -1452,7 +1431,7 @@ let Formats = [ ], mod: 'gen1', - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], banlist: ['Uber'], }, @@ -1472,7 +1451,7 @@ let Formats = [ mod: 'gen6', searchShow: false, - ruleset: ['Pokemon', 'Standard', 'Swagger Clause', 'Team Preview', 'Mega Rayquaza Clause'], + ruleset: ['Obtainable', 'Standard', 'Swagger Clause', 'Team Preview', 'Mega Rayquaza Clause'], }, { name: "[Gen 6] UU", @@ -1532,7 +1511,7 @@ let Formats = [ mod: 'gen6', searchShow: false, maxLevel: 5, - ruleset: ['Pokemon', 'Standard', 'Team Preview', 'Little Cup'], + ruleset: ['Obtainable', 'Standard', 'Team Preview', 'Little Cup'], banlist: ['LC Uber', 'Gligar', 'Misdreavus', 'Scyther', 'Sneasel', 'Tangela', 'Baton Pass', 'Dragon Rage', 'Sonic Boom', 'Swagger'], }, { @@ -1544,8 +1523,7 @@ let Formats = [ mod: 'gen6', searchShow: false, - ruleset: ['Pokemon', 'Endless Battle Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased'], + ruleset: ['Obtainable', 'Team Preview', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 6] 1v1", @@ -1560,9 +1538,9 @@ let Formats = [ validate: [1, 3], battle: 1, }, - ruleset: ['Pokemon', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['Obtainable', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], banlist: [ - 'Illegal', 'Unreleased', 'Arceus', 'Blaziken', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', + 'Arceus', 'Blaziken', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', 'Ho-Oh', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-White', 'Lugia', 'Mewtwo', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Soul Dew', 'Perish Song', @@ -1578,7 +1556,7 @@ let Formats = [ mod: 'gen6', searchShow: false, - ruleset: ['[Gen 6] OU', 'Allow CAP'], + ruleset: ['[Gen 6] OU', '+CAP'], }, { name: "[Gen 6] Battle Spot Singles", @@ -1594,7 +1572,7 @@ let Formats = [ validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], requirePentagon: true, }, { @@ -1626,7 +1604,7 @@ let Formats = [ mod: 'gen6', gameType: 'doubles', searchShow: false, - ruleset: ['Pokemon', 'Standard Doubles', 'Swagger Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard Doubles', 'Swagger Clause', 'Team Preview'], banlist: ['DUber', 'Soul Dew', 'Dark Void', 'Gravity ++ Grass Whistle', 'Gravity ++ Hypnosis', 'Gravity ++ Lovely Kiss', 'Gravity ++ Sing', 'Gravity ++ Sleep Powder'], }, { @@ -1644,9 +1622,9 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'], + ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'], banlist: [ - 'Illegal', 'Unreleased', 'Mew', 'Celebi', 'Jirachi', 'Deoxys', 'Deoxys-Attack', 'Deoxys-Defense', 'Deoxys-Speed', 'Phione', 'Manaphy', 'Darkrai', + 'Mew', 'Celebi', 'Jirachi', 'Deoxys', 'Deoxys-Attack', 'Deoxys-Defense', 'Deoxys-Speed', 'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Shaymin-Sky', 'Arceus', 'Victini', 'Keldeo', 'Meloetta', 'Genesect', 'Diancie', 'Hoopa', 'Hoopa-Unbound', 'Volcanion', 'Soul Dew', ], requirePentagon: true, @@ -1675,7 +1653,7 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], requirePentagon: true, }, { @@ -1705,7 +1683,7 @@ let Formats = [ teamLength: { validate: [6, 6], }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], requirePentagon: true, }, { @@ -1738,7 +1716,7 @@ let Formats = [ mod: 'gen5', searchShow: false, - ruleset: ['Pokemon', 'Standard', 'Evasion Abilities Clause', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Evasion Abilities Clause', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], banlist: ['Uber', 'OU', 'UUBL', 'Arena Trap', 'Drought', 'Sand Stream', 'Snow Warning'], }, { @@ -1775,7 +1753,7 @@ let Formats = [ mod: 'gen5', searchShow: false, maxLevel: 5, - ruleset: ['Pokemon', 'Standard', 'Team Preview', 'Little Cup'], + ruleset: ['Obtainable', 'Standard', 'Team Preview', 'Little Cup'], banlist: ['Berry Juice', 'Soul Dew', 'Dragon Rage', 'Sonic Boom', 'LC Uber', 'Sand Rush', 'Gligar', 'Murkrow', 'Scyther', 'Sneasel', 'Tangela'], }, { @@ -1802,7 +1780,7 @@ let Formats = [ validate: [1, 3], battle: 1, }, - ruleset: ['Pokemon', 'Standard', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], banlist: ['Uber', 'Whimsicott', 'Focus Sash', 'Soul Dew', 'Perish Song'], unbanlist: ['Genesect', 'Landorus', 'Manaphy', 'Thundurus', 'Tornadus-Therian'], }, @@ -1816,7 +1794,7 @@ let Formats = [ validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], banlist: ['Dark Void', 'Sky Drop'], }, { @@ -1849,7 +1827,7 @@ let Formats = [ mod: 'gen5', gameType: 'doubles', searchShow: false, - ruleset: ['Pokemon', 'Standard', 'Evasion Abilities Clause', 'Swagger Clause', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Evasion Abilities Clause', 'Swagger Clause', 'Team Preview'], banlist: ['DUber', 'Soul Dew', 'Dark Void', 'Sky Drop'], }, { @@ -1863,7 +1841,7 @@ let Formats = [ validate: [4, 6], battle: 4, }, - ruleset: ['Pokemon', 'Standard GBU'], + ruleset: ['Obtainable', 'Standard GBU'], banlist: ['Dark Void', 'Sky Drop'], }, { @@ -1896,7 +1874,7 @@ let Formats = [ mod: 'gen4', searchShow: false, - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], banlist: ['Arceus'], }, { @@ -1929,8 +1907,7 @@ let Formats = [ mod: 'gen4', searchShow: false, - ruleset: ['Pokemon', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased'], + ruleset: ['Obtainable', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 4] Custom Game", @@ -1993,7 +1970,7 @@ let Formats = [ mod: 'gen3', searchShow: false, - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], banlist: ['Wobbuffet + Leftovers'], }, { @@ -2005,7 +1982,7 @@ let Formats = [ mod: 'gen3', searchShow: false, - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], banlist: ['Uber', 'OU', 'UUBL', 'Smeargle + Ingrain'], }, { @@ -2037,7 +2014,7 @@ let Formats = [ gameType: 'doubles', searchShow: false, debug: true, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Obtainable', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 2] Ubers", @@ -2048,7 +2025,7 @@ let Formats = [ mod: 'gen2', searchShow: false, - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], }, { name: "[Gen 2] UU", @@ -2088,7 +2065,7 @@ let Formats = [ mod: 'gen1', searchShow: false, - ruleset: ['Pokemon', 'Standard'], + ruleset: ['Obtainable', 'Standard'], }, { name: "[Gen 1] UU", @@ -2111,8 +2088,8 @@ let Formats = [ mod: 'gen1', searchShow: false, - ruleset: ['Pokemon', 'Allow Tradeback', 'Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Uber', 'Unreleased', 'Illegal', + ruleset: ['Obtainable', 'Allow Tradeback', 'Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'HP Percentage Mod', 'Cancel Mod'], + banlist: ['Uber', 'Nidoking + Fury Attack + Thrash', 'Exeggutor + Poison Powder + Stomp', 'Exeggutor + Sleep Powder + Stomp', 'Exeggutor + Stun Spore + Stomp', 'Jolteon + Focus Energy + Thunder Shock', 'Flareon + Focus Energy + Ember', ], @@ -2122,7 +2099,7 @@ let Formats = [ mod: 'stadium', searchShow: false, - ruleset: ['Pokemon', 'Standard', 'Team Preview'], + ruleset: ['Obtainable', 'Standard', 'Team Preview'], banlist: ['Uber', 'Nidoking + Fury Attack + Thrash', 'Exeggutor + Poison Powder + Stomp', 'Exeggutor + Sleep Powder + Stomp', 'Exeggutor + Stun Spore + Stomp', 'Jolteon + Focus Energy + Thunder Shock', 'Flareon + Focus Energy + Ember', diff --git a/data/formats-data.js b/data/formats-data.js index 306ec7b36f..13fc639fc2 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -3471,12 +3471,6 @@ let BattleFormatsData = { randomDoubleBattleMoves: ["swordsdance", "willowisp", "xscissor", "shadowsneak", "shadowclaw", "protect"], eventPokemon: [ {"generation": 3, "level": 50, "moves": ["spite", "confuseray", "shadowball", "grudge"], "pokeball": "pokeball"}, - {"generation": 3, "level": 20, "shiny": 1, "moves": ["doubleteam", "furycutter", "screech"]}, - {"generation": 3, "level": 25, "shiny": 1, "moves": ["swordsdance"]}, - {"generation": 3, "level": 31, "shiny": 1, "moves": ["slash"]}, - {"generation": 3, "level": 38, "shiny": 1, "moves": ["agility"]}, - {"generation": 3, "level": 45, "shiny": 1, "moves": ["batonpass"]}, - {"generation": 4, "level": 52, "shiny": 1, "moves": ["xscissor"]}, ], tier: "(PU)", doublesTier: "(DUU)", @@ -7806,18 +7800,12 @@ let BattleFormatsData = { necrozmaduskmane: { randomBattleMoves: ["swordsdance", "sunsteelstrike", "photongeyser", "earthquake", "knockoff", "autotomize"], randomDoubleBattleMoves: ["swordsdance", "sunsteelstrike", "photongeyser", "earthquake", "knockoff", "rockslide"], - eventPokemon: [ - {"generation": 7, "level": 65, "moves": ["sunsteelstrike"]}, - ], eventOnly: true, tier: "Uber", doublesTier: "DUber", }, necrozmadawnwings: { randomBattleMoves: ["calmmind", "moongeistbeam", "photongeyser", "heatwave", "powergem", "trickroom"], - eventPokemon: [ - {"generation": 7, "level": 65, "moves": ["moongeistbeam"]}, - ], eventOnly: true, tier: "Uber", doublesTier: "DUber", diff --git a/data/learnsets.js b/data/learnsets.js index abdf8405c5..0bbf4e914c 100644 --- a/data/learnsets.js +++ b/data/learnsets.js @@ -11476,6 +11476,7 @@ let BattleLearnsets = { laserfocus: ["7T"], lowkick: ["7T", "6T", "5T", "4T"], lowsweep: ["7M", "6M", "5M"], + machpunch: ["3C"], meditate: ["7L5", "7L1", "7V", "7V", "6L5", "5L5", "4L5", "3L6"], megakick: ["7L53", "7L1", "7V", "7V", "6L1", "5L53", "4L49", "3T", "3L46", "3S0"], megapunch: ["7V", "3T"], @@ -11557,6 +11558,7 @@ let BattleLearnsets = { headbutt: ["7V", "4T"], helpinghand: ["7T", "6T", "5T", "4T", "3S0"], hiddenpower: ["7M", "7V", "6M", "5M", "4M", "3M"], + highjumpkick: ["3C"], icepunch: ["7T", "7L36", "7V", "7V", "6T", "6L36", "5T", "5L36", "4T", "4L31", "3T", "3L26"], laserfocus: ["7T"], lowkick: ["7T", "6T", "5T", "4T"], @@ -11638,6 +11640,7 @@ let BattleLearnsets = { headbutt: ["7V", "4T"], helpinghand: ["7T", "6T", "5T", "5S0", "4T"], hiddenpower: ["7M", "7V", "6M", "5M", "4M", "3M"], + highjumpkick: ["3C"], laserfocus: ["7T"], lowkick: ["7T", "6T", "5T", "4T"], lowsweep: ["7M", "6M", "5M"], @@ -18804,6 +18807,7 @@ let BattleLearnsets = { metronome: ["7L5", "7L1", "7V", "6L1", "5L1", "4L1", "3T", "3L1"], mimic: ["3T"], mudslap: ["7V", "4T", "3T"], + nastyplot: ["5C", "4C"], naturalgift: ["4M"], ominouswind: ["4T"], protect: ["7M", "7V", "6M", "5M", "4M", "3M"], @@ -27318,23 +27322,23 @@ let BattleLearnsets = { shedinja: {learnset: { absorb: ["7L5", "7L1"], aerialace: ["7M", "6M", "5M", "4M", "3M"], - agility: ["3S4"], + agility: ["4R", "3R"], allyswitch: ["7T"], - batonpass: ["3S5"], + batonpass: ["4R", "3R"], bugbite: ["7T", "6T", "5T", "4T"], confide: ["7M", "6M"], confuseray: ["7L29", "6L29", "5L31", "4L31", "3L31", "3S0"], cut: ["6M", "5M", "4M", "3M"], dig: ["6M", "5M", "4M", "3M"], doubleedge: ["3T"], - doubleteam: ["7M", "6M", "5M", "4M", "3M", "3S1"], + doubleteam: ["7M", "6M", "5M", "4M", "3M"], dreameater: ["7M", "6M", "5M", "4M", "3T"], endure: ["4M", "3T"], facade: ["7M", "6M", "5M", "4M", "3M"], falseswipe: ["7M", "6M", "5M", "4M"], flash: ["6M", "5M", "4M", "3M"], frustration: ["7M", "6M", "5M", "4M", "3M"], - furycutter: ["4T", "3T", "3S1"], + furycutter: ["4T", "3T"], furyswipes: ["7L13", "6L13", "5L14", "4L14", "3L14"], gigadrain: ["7T", "6T", "5T", "4M", "3M"], gigaimpact: ["7M", "6M", "5M", "4M"], @@ -27358,12 +27362,12 @@ let BattleLearnsets = { sandattack: ["7L9", "7L1", "6L9", "5L9", "4L9", "3L9"], sandstorm: ["7M", "6M", "5M", "4M", "3M"], scratch: ["7L1", "6L1", "5L1", "4L1", "3L1"], - screech: ["3S1"], + screech: ["4R", "3R"], secretpower: ["6M", "4M", "3M"], shadowball: ["7M", "7L33", "6M", "6L33", "5M", "5L59", "4M", "4L59", "3M", "3L38", "3S0"], shadowclaw: ["7M", "6M", "5M", "4M"], shadowsneak: ["7L21", "6L21", "5L38", "4L38"], - slash: ["3S3"], + slash: ["4R", "3R"], sleeptalk: ["7M", "6M", "5T", "4M", "3T"], snore: ["7T", "6T", "5T", "4T", "3T"], solarbeam: ["7M", "6M", "5M", "4M", "3M"], @@ -27374,13 +27378,13 @@ let BattleLearnsets = { suckerpunch: ["4T"], sunnyday: ["7M", "6M", "5M", "4M", "3M"], swagger: ["7M", "6M", "5M", "4M", "3T"], - swordsdance: ["3S2"], + swordsdance: ["4R", "3R"], telekinesis: ["7T", "5M"], thief: ["7M", "6M", "5M", "4M", "3M"], toxic: ["7M", "6M", "5M", "4M", "3M"], trick: ["7T", "6T", "5T", "4T"], willowisp: ["7M", "6M", "5M", "4M"], - xscissor: ["7M", "6M", "5M", "4M", "4S6"], + xscissor: ["7M", "6M", "5M", "4M"], }}, whismur: {learnset: { astonish: ["7L8", "6L8", "5L11", "4L11", "3L11"], @@ -39483,19 +39487,19 @@ let BattleLearnsets = { willowisp: ["7M", "6M", "5M", "4M"], }}, rotomheat: {learnset: { - overheat: ["7L1", "6L1", "5T", "4T"], + overheat: ["7R", "6R", "5R", "4R"], }}, rotomwash: {learnset: { - hydropump: ["7L1", "6L1", "5T", "4T"], + hydropump: ["7R", "6R", "5R", "4R"], }}, rotomfrost: {learnset: { - blizzard: ["7L1", "6L1", "5T", "4T"], + blizzard: ["7R", "6R", "5R", "4R"], }}, rotomfan: {learnset: { - airslash: ["7L1", "6L1", "5T", "4T"], + airslash: ["7R", "6R", "5R", "4R"], }}, rotommow: {learnset: { - leafstorm: ["7L1", "6L1", "5T", "4T"], + leafstorm: ["7R", "6R", "5R", "4R"], }}, uxie: {learnset: { acrobatics: ["7M", "6M", "5M"], @@ -60779,160 +60783,10 @@ let BattleLearnsets = { xscissor: ["7M"], }}, necrozmaduskmane: {learnset: { - aerialace: ["7M"], - allyswitch: ["7T"], - autotomize: ["7L47"], - brickbreak: ["7M"], - brutalswing: ["7M"], - bulldoze: ["7M"], - calmmind: ["7M"], - chargebeam: ["7M", "7L1"], - confide: ["7M"], - confusion: ["7L1"], - darkpulse: ["7M"], - doubleteam: ["7M"], - dragonpulse: ["7T"], - earthpower: ["7T"], - earthquake: ["7M"], - embargo: ["7M"], - facade: ["7M"], - flashcannon: ["7M"], - fling: ["7M"], - frustration: ["7M"], - gigaimpact: ["7M"], - gravity: ["7T", "7L31"], - gyroball: ["7M"], - heatwave: ["7T"], - hiddenpower: ["7M"], - hyperbeam: ["7M"], - hypervoice: ["7T"], - irondefense: ["7T", "7L59"], - ironhead: ["7T"], - knockoff: ["7T"], - lightscreen: ["7M"], - magnetrise: ["7T"], - metalclaw: ["7L1"], - mirrorshot: ["7L1"], - moonlight: ["7L1"], - morningsun: ["7L1"], - nightslash: ["7L23"], - outrage: ["7T"], - photongeyser: ["7L50"], - powergem: ["7L43"], - prismaticlaser: ["7L73"], - protect: ["7M"], - psychic: ["7M"], - psychocut: ["7L37"], - psyshock: ["7M"], - recycle: ["7T"], - reflect: ["7M"], - rest: ["7M"], - return: ["7M"], - rockblast: ["7L19"], - rockpolish: ["7M"], - rockslide: ["7M"], - rocktomb: ["7M"], - round: ["7M"], - shadowclaw: ["7M"], - shockwave: ["7T"], - signalbeam: ["7T"], - slash: ["7L7"], - sleeptalk: ["7M"], - smartstrike: ["7M"], - snore: ["7T"], - solarbeam: ["7M"], - stealthrock: ["7T", "7L53"], - stoneedge: ["7M"], - storedpower: ["7L13"], - substitute: ["7M"], - sunsteelstrike: ["7S0"], - swagger: ["7M"], - swordsdance: ["7M"], - telekinesis: ["7T"], - thief: ["7M"], - thunderwave: ["7M"], - toxic: ["7M"], - trickroom: ["7M"], - wringout: ["7L67"], - xscissor: ["7M"], + sunsteelstrike: ["7R"], }}, necrozmadawnwings: {learnset: { - aerialace: ["7M"], - allyswitch: ["7T"], - autotomize: ["7L47"], - brickbreak: ["7M"], - brutalswing: ["7M"], - bulldoze: ["7M"], - calmmind: ["7M"], - chargebeam: ["7M", "7L1"], - confide: ["7M"], - confusion: ["7L1"], - darkpulse: ["7M"], - doubleteam: ["7M"], - dragonpulse: ["7T"], - earthpower: ["7T"], - earthquake: ["7M"], - embargo: ["7M"], - facade: ["7M"], - flashcannon: ["7M"], - fling: ["7M"], - frustration: ["7M"], - gigaimpact: ["7M"], - gravity: ["7T", "7L31"], - gyroball: ["7M"], - heatwave: ["7T"], - hiddenpower: ["7M"], - hyperbeam: ["7M"], - hypervoice: ["7T"], - irondefense: ["7T", "7L59"], - ironhead: ["7T"], - knockoff: ["7T"], - lightscreen: ["7M"], - magnetrise: ["7T"], - metalclaw: ["7L1"], - mirrorshot: ["7L1"], - moongeistbeam: ["7S0"], - moonlight: ["7L1"], - morningsun: ["7L1"], - nightslash: ["7L23"], - outrage: ["7T"], - photongeyser: ["7L50"], - powergem: ["7L43"], - prismaticlaser: ["7L73"], - protect: ["7M"], - psychic: ["7M"], - psychocut: ["7L37"], - psyshock: ["7M"], - recycle: ["7T"], - reflect: ["7M"], - rest: ["7M"], - return: ["7M"], - rockblast: ["7L19"], - rockpolish: ["7M"], - rockslide: ["7M"], - rocktomb: ["7M"], - round: ["7M"], - shadowclaw: ["7M"], - shockwave: ["7T"], - signalbeam: ["7T"], - slash: ["7L7"], - sleeptalk: ["7M"], - smartstrike: ["7M"], - snore: ["7T"], - solarbeam: ["7M"], - stealthrock: ["7T", "7L53"], - stoneedge: ["7M"], - storedpower: ["7L13"], - substitute: ["7M"], - swagger: ["7M"], - swordsdance: ["7M"], - telekinesis: ["7T"], - thief: ["7M"], - thunderwave: ["7M"], - toxic: ["7M"], - trickroom: ["7M"], - wringout: ["7L67"], - xscissor: ["7M"], + moongeistbeam: ["7R"], }}, magearna: {learnset: { afteryou: ["7T"], diff --git a/data/mods/gen1/rulesets.js b/data/mods/gen1/rulesets.js index 2b197e6e67..edd519b95b 100644 --- a/data/mods/gen1/rulesets.js +++ b/data/mods/gen1/rulesets.js @@ -2,58 +2,15 @@ /**@type {{[k: string]: ModdedFormatsData}} */ let BattleFormats = { - pokemon: { - effectType: 'ValidatorRule', - name: 'Pokemon', - onValidateSet(set, format) { - let template = this.getTemplate(set.species); - let problems = []; - if (set.species === set.name) delete set.name; - - if (template.gen > this.gen) { - problems.push(set.species + ' does not exist in gen ' + this.gen + '.'); - } else if (template.isNonstandard) { - problems.push(set.species + ' is not a real Pokemon.'); - } - if (set.moves) { - for (const setMoveid of set.moves) { - let move = this.getMove(setMoveid); - if (move.gen > this.gen) { - problems.push(move.name + ' does not exist in gen ' + this.gen + '.'); - } else if (move.isNonstandard) { - problems.push(move.name + ' is not a real move.'); - } - } - } - if (set.moves && set.moves.length > 4) { - problems.push((set.name || set.species) + ' has more than four moves.'); - } - - if (set.evs) set.evs['spd'] = set.evs['spa']; - if (set.ivs) set.ivs['spd'] = set.ivs['spa']; - - // Let's manually delete items. - set.item = ''; - - // Automatically set ability to None - set.ability = 'None'; - - // They also get a useless nature, since that didn't exist - set.nature = 'Serious'; - - // No shinies - set.shiny = false; - - return problems; - }, - }, - standard: { - effectType: 'ValidatorRule', - name: 'Standard', - ruleset: ['Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal', 'Dig', 'Fly', + validatemoves: { + inherit: true, + banlist: [ + // https://www.smogon.com/forums/threads/implementing-all-old-gens-in-ps-testers-required.3483261/post-5420130 + // confirmed by Marty 'Kakuna + Poison Sting + Harden', 'Kakuna + String Shot + Harden', 'Beedrill + Poison Sting + Harden', 'Beedrill + String Shot + Harden', + + // https://www.smogon.com/forums/threads/rby-and-gsc-illegal-movesets.78638/ 'Nidoking + Fury Attack + Thrash', 'Exeggutor + Poison Powder + Stomp', 'Exeggutor + Sleep Powder + Stomp', 'Exeggutor + Stun Spore + Stomp', 'Eevee + Tackle + Growl', @@ -61,22 +18,12 @@ let BattleFormats = { 'Jolteon + Tackle + Growl', 'Jolteon + Focus Energy + Thunder Shock', 'Flareon + Tackle + Growl', 'Flareon + Focus Energy + Ember', ], - onValidateSet(set) { - // limit one of each move in Standard - let moves = []; - if (set.moves) { - /**@type {{[k: string]: true}} */ - let hasMove = {}; - for (const setMoveid of set.moves) { - let move = this.getMove(setMoveid); - let moveid = move.id; - if (hasMove[moveid]) continue; - hasMove[moveid] = true; - moves.push(setMoveid); - } - } - set.moves = moves; - }, + }, + standard: { + effectType: 'ValidatorRule', + name: 'Standard', + ruleset: ['Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'HP Percentage Mod', 'Cancel Mod'], + banlist: ['Dig', 'Fly'], }, }; diff --git a/data/mods/gen2/rulesets.js b/data/mods/gen2/rulesets.js index 07582acc3d..79d3c40092 100644 --- a/data/mods/gen2/rulesets.js +++ b/data/mods/gen2/rulesets.js @@ -2,67 +2,9 @@ /**@type {{[k: string]: ModdedFormatsData}} */ let BattleFormats = { - pokemon: { - effectType: 'ValidatorRule', - name: 'Pokemon', - onValidateSet(set, format) { - let template = this.getTemplate(set.species); - let problems = []; - if (set.species === set.name) delete set.name; - - if (template.gen > this.gen) { - problems.push(set.species + ' does not exist in gen ' + this.gen + '.'); - } else if (template.isNonstandard) { - problems.push(set.species + ' is not a real Pokemon.'); - } - let hasSD = false; - if (set.item) { - let item = this.getItem(set.item); - if (item.gen > this.gen) { - problems.push(item.name + ' does not exist in gen ' + this.gen + '.'); - } else if (item.isNonstandard) { - problems.push(item.name + ' is not a real item.'); - } - } - if (set.moves) { - for (const setMoveid of set.moves) { - let move = this.getMove(setMoveid); - if (move.gen > this.gen) { - problems.push(move.name + ' does not exist in gen ' + this.gen + '.'); - } else if (move.isNonstandard) { - problems.push(move.name + ' is not a real move.'); - } - if (move.id === 'swordsdance') hasSD = true; - } - } - if (set.moves && set.moves.length > 4) { - problems.push((set.name || set.species) + ' has more than four moves.'); - } - - // Automatically set ability to None - set.ability = 'None'; - - if (set.ivs && toID(set.item) === 'thickclub' && set.species === 'Marowak' && hasSD && (!set.level || set.level === 100)) { - if (!set.evs) set.evs = {hp: 252, atk: 252, def: 252, spa: 252, spd: 252, spe: 252}; - if (set.evs.atk === undefined) set.evs.atk = 252; - if (set.ivs.atk === undefined) set.ivs.atk = 30; - 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; - } - } - - // They all also get a useless nature, since that didn't exist - set.nature = 'Serious'; - - return problems; - }, - }, - standard: { - effectType: 'ValidatorRule', - name: 'Standard', - ruleset: ['Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal', + validatemoves: { + inherit: true, + banlist: [ 'Hypnosis + Mean Look', 'Hypnosis + Spider Web', 'Lovely Kiss + Mean Look', @@ -74,22 +16,11 @@ let BattleFormats = { 'Spore + Mean Look', 'Spore + Spider Web', ], - onValidateSet(set) { - // limit one of each move in Standard - let moves = []; - if (set.moves) { - /**@type {{[k: string]: true}} */ - let hasMove = {}; - for (const setMoveid of set.moves) { - let move = this.getMove(setMoveid); - let moveid = move.id; - if (hasMove[moveid]) continue; - hasMove[moveid] = true; - moves.push(setMoveid); - } - } - set.moves = moves; - }, + }, + standard: { + effectType: 'ValidatorRule', + name: 'Standard', + ruleset: ['Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'HP Percentage Mod', 'Cancel Mod'], }, }; diff --git a/data/mods/gen3/rulesets.js b/data/mods/gen3/rulesets.js index d6434e013c..19fa519079 100644 --- a/data/mods/gen3/rulesets.js +++ b/data/mods/gen3/rulesets.js @@ -7,7 +7,6 @@ let BattleFormats = { name: 'Standard', desc: "The standard ruleset for all offical Smogon singles tiers (Ubers, OU, etc.)", ruleset: ['Sleep Clause Mod', 'Switch Priority Clause Mod', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Moves Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal'], }, }; diff --git a/data/mods/gen4/rulesets.js b/data/mods/gen4/rulesets.js index 8b2ee5d7c4..6facb00d96 100644 --- a/data/mods/gen4/rulesets.js +++ b/data/mods/gen4/rulesets.js @@ -2,10 +2,9 @@ /**@type {{[k: string]: ModdedFormatsData}} */ let BattleFormats = { - pokemon: { + validatestats: { inherit: true, - onValidateSet(set, format) { - if (!format || !this.getRuleTable(format).has('-illegal')) return; + onValidateSet(set) { let template = this.getTemplate(set.species); let item = this.getItem(set.item); if (item && item.id === 'griseousorb' && template.num !== 487) { diff --git a/data/mods/gen5/rulesets.js b/data/mods/gen5/rulesets.js index 2e0aa1127c..bdd71190a3 100644 --- a/data/mods/gen5/rulesets.js +++ b/data/mods/gen5/rulesets.js @@ -2,7 +2,7 @@ /**@type {{[k: string]: ModdedFormatsData}} */ let BattleFormats = { - pokemon: { + validatemoves: { inherit: true, banlist: [ 'Chansey + Charm + Seismic Toss', diff --git a/data/mods/letsgo/rulesets.js b/data/mods/letsgo/rulesets.js index 2d9fa8c2f3..d5fc157425 100644 --- a/data/mods/letsgo/rulesets.js +++ b/data/mods/letsgo/rulesets.js @@ -2,177 +2,11 @@ /**@type {{[k: string]: ModdedFormatsData}} */ let BattleFormats = { - pokemon: { - effectType: 'ValidatorRule', - name: 'Pokemon', - onValidateTeam(team, format) { - let problems = []; - if (team.length > 6) problems.push('Your team has more than six Pok\u00E9mon.'); - // Unlike Pokemon like Kyurem-B and Kyurem-W, the two Starter Pokemon cannot be hacked onto other games. - let hasStarter = 0; - for (const set of team) { - if (set.species === 'Pikachu-Starter' || set.species === 'Eevee-Starter') { - hasStarter++; - if (hasStarter > 1) { - problems.push(`You can only have one of Pikachu-Starter or Eevee-Starter on a team.`); - break; - } - } - } - return problems; - }, - onChangeSet(set, format) { - let template = this.getTemplate(set.species); - let baseTemplate = this.getTemplate(template.baseSpecies); - let problems = []; - let allowAVs = !!(format && this.getRuleTable(format).has('allowavs')); - let allowCAP = !!(format && this.getRuleTable(format).has('allowcap')); - - if (set.species === set.name) delete set.name; - const validNum = (baseTemplate.num <= 151 && baseTemplate.num >= 1) || [808, 809].includes(baseTemplate.num); - if (!validNum) { - problems.push( - `Only Pok\u00E9mon whose base formes are from Gen 1, Meltan, and Melmetal can be used.`, - `(${set.species} is from Gen ${baseTemplate.gen === 1 ? 7 : baseTemplate.gen}.)` - ); - } - if (template.forme && (!['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(template.forme) || template.species === 'Pikachu-Alola')) { - problems.push(`${set.species}'s forme ${template.forme} is not available in Let's Go.`); - } - if (set.moves) { - for (const moveid of set.moves) { - let move = this.getMove(moveid); - if (move.gen > this.gen) { - problems.push(move.name + ' does not exist in gen ' + this.gen + '.'); - } else if (!allowCAP && move.isNonstandard) { - problems.push(move.name + ' does not exist.'); - } - } - } - if (set.moves && set.moves.length > 4) { - problems.push((set.name || set.species) + ' has more than four moves.'); - } - if (set.level && set.level > 100) { - problems.push((set.name || set.species) + ' is higher than level 100.'); - } - - if (!allowCAP || !template.tier.startsWith('CAP')) { - if (template.isNonstandard && template.num > -5000) { - problems.push(set.species + ' does not exist.'); - } - } - - if (!allowAVs && set.evs) { - const statNames = {hp: 'HP', atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed'}; - for (let k in set.evs) { - // @ts-ignore - if (set.evs[k]) { - // @ts-ignore - problems.push(`${set.name || set.species} has ${set.evs[k]} AVs/EVs in ${statNames[k]}, but AVs and EVs are not allowed in this format.`); - break; - } - // @ts-ignore - set.evs[k] = 0; - } - } - - set.ability = 'No Ability'; - // Temporary hack to allow mega evolution - if (set.item) { - let item = this.getItem(set.item); - if (item.megaEvolves !== template.baseSpecies) { - problems.push(`Items aren't allowed in Let's Go.`); - } - } - - if (!set.happiness || set.happiness !== 70) { - set.happiness = 70; - } - - // ----------- legality line ------------------------------------------ - if (!this.getRuleTable(format).has('-illegal')) return problems; - // everything after this line only happens if we're doing legality enforcement - - // Pokestar studios - if (template.num <= -5000 && template.isNonstandard) { - problems.push(`${set.species} cannot be obtained by legal means.`); - } - - // Legendary Pokemon must have at least 3 perfect IVs in gen 6 - if (set.ivs && this.gen >= 6 && (baseTemplate.gen >= 6 || format.requirePentagon) && (template.eggGroups[0] === 'Undiscovered' || template.species === 'Manaphy') && !template.prevo && !template.nfe && - // exceptions - template.species !== 'Unown' && template.baseSpecies !== 'Pikachu' && (template.baseSpecies !== 'Diancie' || !set.shiny)) { - let perfectIVs = 0; - for (let i in set.ivs) { - // @ts-ignore - if (set.ivs[i] >= 31) perfectIVs++; - } - let reason = (format.requirePentagon ? " and this format requires gen " + this.gen + " Pokémon" : " in gen 6"); - if (perfectIVs < 3) problems.push((set.name || set.species) + " must have at least three perfect IVs because it's a legendary" + reason + "."); - } - - // limit one of each move - let moves = []; - if (set.moves) { - /**@type {{[k: string]: true}} */ - let hasMove = {}; - for (const moveId of set.moves) { - let move = this.getMove(moveId); - let moveid = move.id; - if (hasMove[moveid]) continue; - hasMove[moveid] = true; - moves.push(moveId); - } - } - set.moves = moves; - - let battleForme = template.battleOnly && template.species; - if (battleForme) { - if (template.isMega) set.species = template.baseSpecies; - if (template.requiredMove && !set.moves.includes(toID(template.requiredMove))) { - problems.push(`${template.species} transforms in-battle with ${template.requiredMove}.`); // Meloetta-Pirouette, Rayquaza-Mega - } - } else { - if (template.requiredMove && !set.moves.includes(toID(template.requiredMove))) { - problems.push(`${(set.name || set.species)} needs to have the move ${template.requiredMove}.`); // Keldeo-Resolute - } - } - - return problems; - }, - }, allowavs: { effectType: 'ValidatorRule', name: 'Allow AVs', desc: "Tells formats with the 'letsgo' mod to take Awakening Values into consideration when calculating stats", - onChangeSet(set, format) { - /**@type {string[]} */ - let problems = ([]); - let avs = /** @type {StatsTable} */(this.getAwakeningValues(set)); - if (set.evs) { - for (let k in set.evs) { - // @ts-ignore - avs[k] = set.evs[k]; - // @ts-ignore - if (typeof avs[k] !== 'number' || avs[k] < 0) { - // @ts-ignore - avs[k] = 0; - } - } - } - - // Pokemon cannot have more than 200 Awakening Values in a stat. It is impossible to hack more than 200 AVs onto a stat, so legality doesn't matter. - for (let av in avs) { - let statNames = {hp: 'HP', atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed'}; - // @ts-ignore - if (avs[av] > 200) { - // @ts-ignore - problems.push(`${set.name || set.species} has more than 200 Awakening Values in its ${statNames[av]}.`); - } - } - return problems; - }, - // Partially implemented in the modified pokemon rule above + // implemented in TeamValidator#validateStats }, }; diff --git a/data/mods/stadium/rulesets.js b/data/mods/stadium/rulesets.js index cc21291a77..4deeaab14d 100644 --- a/data/mods/stadium/rulesets.js +++ b/data/mods/stadium/rulesets.js @@ -6,23 +6,6 @@ let BattleFormats = { effectType: 'ValidatorRule', name: 'Standard', ruleset: ['Sleep Clause Mod', 'Freeze Clause Mod', 'Species Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Exact HP Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal'], - onValidateSet(set) { - // limit one of each move in Standard - let moves = []; - if (set.moves) { - /**@type {{[k: string]: true}} */ - let hasMove = {}; - for (const setMoveid of set.moves) { - let move = this.getMove(setMoveid); - let moveid = move.id; - if (hasMove[moveid]) continue; - hasMove[moveid] = true; - moves.push(setMoveid); - } - } - set.moves = moves; - }, }, }; diff --git a/data/rulesets.js b/data/rulesets.js index e15355ac3b..7f56e66ea8 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -14,28 +14,26 @@ let BattleFormats = { name: 'Standard', desc: "The standard ruleset for all offical Smogon singles tiers (Ubers, OU, etc.)", ruleset: ['Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Moves Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal'], }, standardnext: { effectType: 'ValidatorRule', name: 'Standard NEXT', desc: "The standard ruleset for the NEXT mod", - ruleset: ['Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Illegal', 'Soul Dew'], + ruleset: ['+Unreleased', 'Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'HP Percentage Mod', 'Cancel Mod'], + banlist: ['Soul Dew'], }, standardubers: { effectType: 'ValidatorRule', name: 'Standard Ubers', desc: "The standard ruleset for [Gen 5] Ubers", ruleset: ['Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal'], }, standardgbu: { effectType: 'ValidatorRule', name: 'Standard GBU', desc: "The standard ruleset for all official in-game Pokémon tournaments and Battle Spot", ruleset: ['Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal', 'Battle Bond', + banlist: ['Battle Bond', 'Mewtwo', 'Mew', 'Lugia', 'Ho-Oh', 'Celebi', 'Kyogre', 'Groudon', 'Rayquaza', 'Jirachi', 'Deoxys', @@ -55,7 +53,7 @@ let BattleFormats = { name: 'Minimal GBU', desc: "The standard ruleset for official tournaments, but without Restricted Legendary bans", ruleset: ['Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal', 'Battle Bond', + banlist: ['Battle Bond', 'Mew', 'Celebi', 'Jirachi', 'Deoxys', @@ -75,137 +73,70 @@ let BattleFormats = { name: 'Standard Doubles', desc: "The standard ruleset for all official Smogon doubles tiers", ruleset: ['Species Clause', 'Nickname Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Abilities Clause', 'Evasion Moves Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'], - banlist: ['Unreleased', 'Illegal'], }, - pokemon: { + validate: { effectType: 'ValidatorRule', - name: 'Pokemon', - desc: "Applies the basic limitations of pokemon games: level 100, 6 pokemon, 4 moves, no CAP, no future-gen pokemon/moves/etc - but does not include illegal move/ability validation", - onValidateTeam(team, format) { - let problems = []; - if (team.length > 6) problems.push('Your team has more than six Pok\u00E9mon.'); - // ----------- legality line ------------------------------------------ - if (!format || !this.getRuleTable(format).has('-illegal')) return problems; - // everything after this line only happens if we're doing legality enforcement - let kyurems = 0; - let ndm = 0; - let ndw = 0; + name: 'Obtainable', + desc: "Makes sure the team is possible to obtain in-game.", + ruleset: ['Obtainable Moves', 'Obtainable Abilities', 'Obtainable Formes', 'Obtainable Misc'], + banlist: ['Unreleased', 'Nonexistent'], + }, + validatemoves: { + effectType: 'ValidatorRule', + name: 'Obtainable Moves', + desc: "Makes sure moves are learnable by the species.", + banlist: [ + 'Chansey + Charm + Seismic Toss', 'Chansey + Charm + Psywave', + 'Blissey + Charm + Seismic Toss', 'Blissey + Charm + Psywave', + 'Shiftry + Leaf Blade + Sucker Punch', + ], + // Mostly hardcoded in team-validator.ts + }, + validateabilities: { + effectType: 'ValidatorRule', + name: 'Obtainable Abilities', + desc: "Makes sure abilities match the species.", + // Hardcoded in team-validator.ts + }, + validateformes: { + effectType: 'ValidatorRule', + name: 'Obtainable Formes', + desc: "Makes sure in-battle formes only appear in-battle.", + // Mostly hardcoded in team-validator.ts + onValidateTeam(team) { + let kyuremCount = 0; + let necrozmaDMCount = 0; + let necrozmaDWCount = 0; for (const set of team) { if (set.species === 'Kyurem-White' || set.species === 'Kyurem-Black') { - if (kyurems > 0) { - problems.push('You cannot have more than one Kyurem-Black/Kyurem-White.'); - break; + if (kyuremCount > 0) { + return ['You cannot have more than one Kyurem-Black/Kyurem-White.']; } - kyurems++; + kyuremCount++; } if (set.species === 'Necrozma-Dusk-Mane') { - if (ndm > 0) { - problems.push('You cannot have more than one Necrozma-Dusk-Mane.'); - break; + if (necrozmaDMCount > 0) { + return ['You cannot have more than one Necrozma-Dusk-Mane.']; } - ndm++; + necrozmaDMCount++; } if (set.species === 'Necrozma-Dawn-Wings') { - if (ndw > 0) { - problems.push('You cannot have more than one Necrozma-Dawn-Wings.'); - break; + if (necrozmaDWCount > 0) { + return ['You cannot have more than one Necrozma-Dawn-Wings.']; } - ndw++; + necrozmaDWCount++; } } - return problems; + return []; }, - onChangeSet(set, format) { - let item = this.getItem(set.item); + }, + validatemisc: { + effectType: 'ValidatorRule', + name: 'Obtainable Misc', + desc: "Validate all obtainability things that aren't moves/abilities (Hidden Power type, gender, stats, etc).", + // Mostly hardcoded in team-validator.ts + onChangeSet(set) { let template = this.getTemplate(set.species); - let problems = []; - let totalEV = 0; - let allowCAP = !!(format && this.getRuleTable(format).has('allowcap')); - - if (set.species === set.name) delete set.name; - if (template.gen > this.gen) { - problems.push(set.species + ' does not exist in gen ' + this.gen + '.'); - } - if (template.gen && template.gen !== this.gen && template.tier === 'Illegal') { - problems.push(set.species + ' does not exist outside of gen ' + template.gen + '.'); - } - /**@type {Ability} */ - // @ts-ignore - let ability = {}; - if (set.ability) { - ability = this.getAbility(set.ability); - if (ability.gen > this.gen) { - problems.push(ability.name + ' does not exist in gen ' + this.gen + '.'); - } - } - if (set.moves) { - for (const moveid of set.moves) { - let move = this.getMove(moveid); - if (move.gen > this.gen) { - problems.push(move.name + ' does not exist in gen ' + this.gen + '.'); - } else if (!allowCAP && move.isNonstandard) { - problems.push(move.name + ' does not exist.'); - } - } - } - if (item.gen > this.gen) { - problems.push(item.name + ' does not exist in gen ' + this.gen + '.'); - } - if (set.moves && set.moves.length > 4) { - problems.push((set.name || set.species) + ' has more than four moves.'); - } - if (set.level && set.level > 100) { - problems.push((set.name || set.species) + ' is higher than level 100.'); - } - - if (!allowCAP || !template.tier.startsWith('CAP')) { - if (template.isNonstandard && template.num > -5000) { - problems.push(set.species + ' does not exist.'); - } - } - - if (!allowCAP && ability.isNonstandard) { - problems.push(ability.name + ' does not exist.'); - } - - if (item.isNonstandard) { - if (item.isNonstandard === 'Past' || item.isNonstandard === 'Future') { - problems.push(item.name + ' does not exist in this generation.'); - } else if (!allowCAP) { - problems.push(item.name + ' does not exist.'); - } - } - - if (set.evs) { - for (let k in set.evs) { - // @ts-ignore - if (typeof set.evs[k] !== 'number' || set.evs[k] < 0) { - // @ts-ignore - set.evs[k] = 0; - } - // @ts-ignore - totalEV += set.evs[k]; - } - } - - // In gen 6, it is impossible to battle other players with pokemon that break the EV limit - if (totalEV > 510 && this.gen === 6) { - problems.push((set.name || set.species) + " has more than 510 total EVs."); - } - - // ----------- legality line ------------------------------------------ - if (!this.getRuleTable(format).has('-illegal')) return problems; - // everything after this line only happens if we're doing legality enforcement - - // Pokestar studios - if (template.num <= -5000 && template.isNonstandard) { - problems.push(`${set.species} cannot be obtained by legal means.`); - } - - // only in gen 1 and 2 it was legal to max out all EVs - if (this.gen >= 3 && totalEV > 510) { - problems.push((set.name || set.species) + " has more than 510 total EVs."); - } if (template.gender) { if (set.gender !== template.gender) { @@ -217,20 +148,6 @@ let BattleFormats = { } } - // Legendary Pokemon must have at least 3 perfect IVs in gen 6 - let baseTemplate = this.getTemplate(template.baseSpecies); - if (set.ivs && this.gen >= 6 && (baseTemplate.gen >= 6 || format.requirePentagon) && (template.eggGroups[0] === 'Undiscovered' || template.species === 'Manaphy') && !template.prevo && !template.nfe && - // exceptions - template.species !== 'Unown' && template.baseSpecies !== 'Pikachu' && (template.baseSpecies !== 'Diancie' || !set.shiny)) { - let perfectIVs = 0; - for (let i in set.ivs) { - // @ts-ignore - if (set.ivs[i] >= 31) perfectIVs++; - } - let reason = (format.requirePentagon ? " and this format requires gen " + this.gen + " Pokémon" : " in gen 6"); - if (perfectIVs < 3) problems.push((set.name || set.species) + " must have at least three perfect IVs because it's a legendary" + reason + "."); - } - // limit one of each move let moves = []; if (set.moves) { @@ -245,78 +162,7 @@ let BattleFormats = { } } set.moves = moves; - - let battleForme = template.battleOnly && template.species; - if (battleForme) { - if (template.requiredAbility && set.ability !== template.requiredAbility) { - problems.push("" + template.species + " transforms in-battle with " + template.requiredAbility + "."); // Darmanitan-Zen, Zygarde-Complete - } - if (template.requiredItems) { - if (template.species === 'Necrozma-Ultra') { - problems.push(`Necrozma-Ultra must start the battle as Necrozma-Dawn-Wings or Necrozma-Dusk-Mane holding Ultranecrozium Z.`); // Necrozma-Ultra transforms from one of two formes, and neither one is the base forme - } else if (!template.requiredItems.includes(item.name)) { - problems.push(`${template.species} transforms in-battle with ${Chat.plural(template.requiredItems.length, "either ") + template.requiredItems.join(" or ")}.`); // Mega or Primal - } - } - if (template.requiredMove && set.moves.indexOf(toID(template.requiredMove)) < 0) { - problems.push(`${template.species} transforms in-battle with ${template.requiredMove}.`); // Meloetta-Pirouette, Rayquaza-Mega - } - if (!format.noChangeForme) set.species = template.baseSpecies; // Fix battle-only forme - } else { - if (template.requiredAbility && set.ability !== template.requiredAbility) { - problems.push(`${(set.name || set.species)} needs the ability ${template.requiredAbility}.`); // No cases currently. - } - if (template.requiredItems && !template.requiredItems.includes(item.name)) { - problems.push(`${(set.name || set.species)} needs to hold ${Chat.plural(template.requiredItems.length, "either ") + template.requiredItems.join(" or ")}.`); // Memory/Drive/Griseous Orb/Plate/Z-Crystal - Forme mismatch - } - if (template.requiredMove && set.moves.indexOf(toID(template.requiredMove)) < 0) { - problems.push(`${(set.name || set.species)} needs to have the move ${template.requiredMove}.`); // Keldeo-Resolute - } - - // 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 && template.species === this.getTemplate(item.forcedForme).baseSpecies && !format.noChangeForme) { - set.species = item.forcedForme; - } - } - - if (template.species === 'Pikachu-Cosplay') { - /**@type {{[k: string]: string}} */ - let cosplay = {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 (set.species !== template.species) { - // Autofixed forme. - template = this.getTemplate(set.species); - - if (!this.getRuleTable(format).has('ignoreillegalabilities') && !format.noChangeAbility) { - // Ensure that the ability is (still) legal. - let legalAbility = false; - for (let i in template.abilities) { - // @ts-ignore - if (template.abilities[i] !== set.ability) continue; - legalAbility = true; - break; - } - if (!legalAbility) { // Default to first ability. - set.ability = template.abilities['0']; - } - } - } - - return problems; }, - banlist: [ - 'Chansey + Charm + Seismic Toss', 'Chansey + Charm + Psywave', - 'Blissey + Charm + Seismic Toss', 'Blissey + Charm + Psywave', - 'Shiftry + Leaf Blade + Sucker Punch', - ], }, hoennpokedex: { effectType: 'ValidatorRule', @@ -852,7 +698,7 @@ let BattleFormats = { effectType: 'ValidatorRule', name: 'STABmons Move Legality', desc: "Allows Pokémon to use any move that they or a previous evolution/out-of-battle forme share a type with", - checkLearnset(move, template, lsetData, set) { + checkLearnset(move, template, setSources, set) { const restrictedMoves = this.format.restrictedMoves || []; if (!move.isZ && !restrictedMoves.includes(move.name)) { let dex = this.dex; @@ -871,16 +717,10 @@ let BattleFormats = { } if (types.includes(move.type)) return null; } - return this.checkLearnset(move, template, lsetData, set); + return this.checkLearnset(move, template, setSources, set); }, unbanlist: ['Shiftry + Leaf Blade + Sucker Punch'], }, - allowcap: { - effectType: 'ValidatorRule', - name: 'Allow CAP', - desc: "Allows the use of Pokémon, abilities, moves, and items made by the Create-A-Pokémon project", - // Implemented in the 'pokemon' ruleset - }, allowtradeback: { effectType: 'ValidatorRule', name: 'Allow Tradeback', diff --git a/server/chat-plugins/datasearch.js b/server/chat-plugins/datasearch.js index 07ff80a808..3286ff2ca3 100644 --- a/server/chat-plugins/datasearch.js +++ b/server/chat-plugins/datasearch.js @@ -594,7 +594,7 @@ function runDexsearch(target, cmd, canAll, message) { const accumulateKeyCount = (count, searchData) => count + (typeof searchData === 'object' ? Object.keys(searchData).length : 0); searches.sort((a, b) => Object.values(a).reduce(accumulateKeyCount, 0) - Object.values(b).reduce(accumulateKeyCount, 0)); - let lsetData = {}; + let pokemonSources = {}; for (const alts of searches) { if (alts.skip) continue; for (let mon in dex) { @@ -723,9 +723,10 @@ function runDexsearch(target, cmd, canAll, message) { } if (matched) continue; + const validator = TeamValidator.get(`gen${maxGen}ou`); for (let move in alts.moves) { - if (!lsetData[mon]) lsetData[mon] = {fastCheck: true, sources: [], sourcesBefore: maxGen}; - if (!TeamValidator.get(`gen${maxGen}ou`).checkLearnset(move, mon, lsetData[mon]) === alts.moves[move]) { + if (!pokemonSources[mon]) pokemonSources[mon] = validator.allSources(); + if (!validator.checkLearnset(move, mon, pokemonSources[mon], true) === alts.moves[move]) { matched = true; break; } @@ -1575,13 +1576,12 @@ function runLearn(target, cmd) { formatName = `Gen ${gen}`; if (format.requirePentagon) formatName += ' Pentagon'; } - let lsetData = {set: {}, sources: [], sourcesBefore: gen}; - const validator = TeamValidator.get(format); + let template = validator.dex.getTemplate(targets.shift()); - let move = {}; + let setSources = validator.allSources(template); + let set = {level: cmd === 'learn5' ? 5 : 100}; let all = (cmd === 'learnall'); - if (cmd === 'learn5') lsetData.set.level = 5; if (!template.exists || template.id === 'missingno') { return {error: `Pok\u00e9mon '${template.id}' not found.`}; @@ -1596,34 +1596,36 @@ function runLearn(target, cmd) { } let lsetProblem; + let moveNames = []; for (const arg of targets) { if (['ha', 'hidden', 'hiddenability'].includes(toID(arg))) { - lsetData.isHidden = true; + setSources.isHidden = true; continue; } - move = validator.dex.getMove(arg); + let move = validator.dex.getMove(arg); + moveNames.push(move.name); if (!move.exists || move.id === 'magikarpsrevenge') { return {error: `Move '${move.id}' not found.`}; } if (move.gen > gen) { return {error: `${move.name} didn't exist yet in generation ${gen}.`}; } - lsetProblem = validator.checkLearnset(move, template, lsetData); + lsetProblem = validator.checkLearnset(move, template, setSources, set); if (lsetProblem) { lsetProblem.moveName = move.name; break; } } - let problems = validator.reconcileLearnset(template, lsetData, lsetProblem); + let problems = validator.reconcileLearnset(template, setSources, lsetProblem); let buffer = `In ${formatName}, `; - buffer += `${template.name}` + (problems ? ` can't learn ` : ` can learn `) + (targets.length > 1 ? `these moves` : move.name); + buffer += `${template.name}` + (problems ? ` can't learn ` : ` can learn `) + Chat.toListString(moveNames); if (!problems) { let sourceNames = {E: "egg", S: "event", D: "dream world", V: "virtual console transfer from gen 1-2", X: "egg, traded back", Y: "event, traded back"}; - let sourcesBefore = lsetData.sourcesBefore; - if (lsetData.sources || sourcesBefore < gen) buffer += " only when obtained"; + let sourcesBefore = setSources.sourcesBefore; + if (setSources.sources || sourcesBefore < gen) buffer += " only when obtained"; buffer += " from:"; } else if (targets.length > 1 || problems.length > 1) { @@ -1725,6 +1727,7 @@ if (!PM.isParentProcess) { } global.Dex = require('../../.sim-dist/dex').Dex; + global.Chat = require('../../.server-dist/chat').Chat; global.toID = Dex.getId; Dex.includeData(); global.TeamValidator = require('../../.sim-dist/team-validator').TeamValidator; diff --git a/server/chat.ts b/server/chat.ts index 94e9aeee1b..833a9b0d19 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -1615,12 +1615,23 @@ export const Chat = new class { } /** - * Takes an array and turns it into a sentence string by adding commas and the word 'and' at the end + * Takes an array and turns it into a sentence string by adding commas and the word "and" */ toListString(arr: string[]) { if (!arr.length) return ''; if (arr.length === 1) return arr[0]; - return `${arr.slice(0, -1).join(", ")} and ${arr.slice(-1)}`; + if (arr.length === 2) return `${arr[0]} and ${arr[1]}`; + return `${arr.slice(0, -1).join(", ")}, and ${arr.slice(-1)[0]}`; + } + + /** + * Takes an array and turns it into a sentence string by adding commas and the word "or" + */ + toOrList(arr: string[]) { + if (!arr.length) return ''; + if (arr.length === 1) return arr[0]; + if (arr.length === 2) return `${arr[0]} or ${arr[1]}`; + return `${arr.slice(0, -1).join(", ")}, or ${arr.slice(-1)[0]}`; } collapseLineBreaksHTML(htmlContent: string) { diff --git a/sim/battle.ts b/sim/battle.ts index abf61e4c04..9506508b07 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -1399,7 +1399,7 @@ export class Battle extends Dex.ModdedDex { continue; } const ruleTable = this.getRuleTable(this.getFormat()); - if (!ruleTable.has('-illegal') && !this.getFormat().team) { + if ((ruleTable.has('+hackmons') || !ruleTable.has('validateabilities')) && !this.getFormat().team) { // hackmons format continue; } else if (abilitySlot === 'H' && template.unreleasedHidden) { diff --git a/sim/dex-data.ts b/sim/dex-data.ts index 67c902f3a7..72fa4726cb 100644 --- a/sim/dex-data.ts +++ b/sim/dex-data.ts @@ -160,8 +160,10 @@ export type ComplexTeamBan = ComplexBan; * - '-[thing]' or '-[category]:[thing]' ban a thing * - '+[thing]' or '+[category]:[thing]' allow a thing (override a ban) * [category] is one of: item, move, ability, species, basespecies + * + * The value is the name of the parent rule (blank for the active format). */ -export class RuleTable extends Map { +export class RuleTable extends Map { complexBans: ComplexBan[]; complexTeamBans: ComplexTeamBan[]; // tslint:disable-next-line:ban-types @@ -176,14 +178,23 @@ export class RuleTable extends Map { this.timer = null; } - check(thing: string, setHas: {[id: string]: true} | null = null): string { + isBanned(thing: string) { + if (this.has(`+${thing}`)) return false; + return this.has(`-${thing}`); + } + + check(thing: string, setHas: {[id: string]: true} | null = null) { + if (this.has(`+${thing}`)) return ''; if (setHas) setHas[thing] = true; return this.getReason(`-${thing}`); } - getReason(key: string): string { + getReason(key: string): string | null { const source = this.get(key); - if (source === undefined) return ''; + if (source === undefined) return null; + if (key === '-nonexistent' || key.startsWith('validate')) { + return 'not obtainable'; + } return source ? `banned by ${source}` : `banned`; } diff --git a/sim/dex.ts b/sim/dex.ts index fedcd03c99..cd246b7ead 100644 --- a/sim/dex.ts +++ b/sim/dex.ts @@ -409,6 +409,13 @@ export class ModdedDex { template.doublesTier = 'Illegal'; template.isNonstandard = 'Future'; } + if (this.currentMod === 'letsgo' && !template.isNonstandard) { + const isLetsGo = ( + (template.num <= 151 || ['Meltan', 'Melmetal'].includes(template.name)) && + (!template.forme || ['Alola', 'Mega', 'Mega-X', 'Mega-Y'].includes(template.forme)) + ); + if (!isLetsGo) template.isNonstandard = 'Past'; + } } else { template = new Data.Template({ id, name, exists: false, tier: 'Illegal', doublesTier: 'Illegal', isNonstandard: 'Custom', @@ -608,6 +615,10 @@ export class ModdedDex { if (item.gen > this.gen) { (item as any).isNonstandard = 'Future'; } + // hack for allowing mega evolution in LGPE + if (this.currentMod === 'letsgo' && !item.isNonstandard && !item.megaStone) { + (item as any).isNonstandard = 'Past'; + } } else { item = new Data.Item({id, name, exists: false}); } @@ -634,6 +645,12 @@ export class ModdedDex { if (ability.gen > this.gen) { (ability as any).isNonstandard = 'Future'; } + if (this.currentMod === 'letsgo' && ability.id !== 'noability') { + (ability as any).isNonstandard = 'Past'; + } + if ((this.currentMod === 'letsgo' || this.gen <= 2) && ability.id === 'noability') { + (ability as any).isNonstandard = null; + } } else { ability = new Data.Ability({id, name, exists: false}); } @@ -888,7 +905,7 @@ export class ModdedDex { validateBanRule(rule: string) { let id = toID(rule); if (id === 'unreleased') return 'unreleased'; - if (id === 'illegal') return 'illegal'; + if (id === 'nonexistent') return 'nonexistent'; const matches = []; let matchTypes = ['pokemon', 'move', 'ability', 'item', 'pokemontag']; for (const matchType of matchTypes) { @@ -916,6 +933,8 @@ export class ModdedDex { 'duber', 'dou', 'dbl', 'duu', 'dnu', // custom tags 'mega', + // illegal/nonstandard reasons + 'glitch', 'past', 'future', 'lgpe', 'pokestar', 'custom', ]; if (validTags.includes(ruleid)) matches.push('pokemontag:' + ruleid); continue; diff --git a/sim/globals.ts b/sim/globals.ts index 485198fbcd..65d3beeeb4 100644 --- a/sim/globals.ts +++ b/sim/globals.ts @@ -5,6 +5,7 @@ type Pokemon = import('./pokemon').Pokemon type PRNGSeed = import('./prng').PRNGSeed; type Side = import('./side').Side type TeamValidator = import('./team-validator').TeamValidator +type PokemonSources = import('./team-validator').PokemonSources type ID = '' | string & {__isID: true}; interface AnyObject {[k: string]: any} @@ -46,6 +47,7 @@ type PokemonSet = { * - L = start or level-up, 3rd char+ is the level * - M = TM/HM * - T = tutor + * - R = restricted (special moves like Rotom moves) * - E = egg * - S = event, 3rd char+ is the index in .eventPokemon * - D = Dream World, only 5D is valid @@ -60,48 +62,6 @@ type PokemonSet = { */ type MoveSource = string; -/** - * 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-7. - * 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 .eventPokemon - * - D = Dream World, only 5D is valid - * - V = Virtual Console transfer, only 7V is valid - * - * Designed to match MoveSource where possible. - */ -type PokemonSource = string; - -/** - * Keeps track of how a pokemon with a given set might be obtained. - * - * `sources` is a list of possible PokemonSources, and a nonzero - * sourcesBefore means the Pokemon is compatible with all possible - * PokemonSources from that gen or earlier. - * - * `limitedEgg` tracks 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. - */ -type PokemonSources = { - sources: PokemonSource[] - sourcesBefore: number - babyOnly?: string - sketchMove?: string - hm?: string - restrictiveMoves?: string[] - limitedEgg?: (string | 'self')[] - isHidden?: boolean - fastCheck?: true -} - type EventInfo = { generation: number, level?: number, @@ -1015,8 +975,6 @@ interface FormatsData extends EventMethods { maxForcedLevel?: number maxLevel?: number mod?: string - noChangeAbility?: boolean - noChangeForme?: boolean onBasePowerPriority?: number onModifyMovePriority?: number onSwitchInPriority?: number @@ -1035,7 +993,7 @@ interface FormatsData extends EventMethods { timer?: Partial tournamentShow?: boolean unbanlist?: string[] - checkLearnset?: (this: TeamValidator, move: Move, template: Template, lsetData: PokemonSources, set: PokemonSet) => {type: string, [any: string]: any} | null + checkLearnset?: (this: TeamValidator, move: Move, template: Template, setSources: PokemonSources, set: PokemonSet) => {type: string, [any: string]: any} | null onAfterMega?: (this: Battle, pokemon: Pokemon) => void onBegin?: (this: Battle) => void onChangeSet?: (this: ModdedDex, set: PokemonSet, format: Format, setHas?: AnyObject, teamHas?: AnyObject) => string[] | void diff --git a/sim/team-validator.ts b/sim/team-validator.ts index 10c0f3ed2f..ac117b5e0e 100644 --- a/sim/team-validator.ts +++ b/sim/team-validator.ts @@ -9,6 +9,167 @@ import {Dex} from './dex'; +/** + * 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-7. + * 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 .eventPokemon + * - D = Dream World, only 5D is valid + * - V = Virtual Console transfer, only 7V 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; + babyOnly?: string; + sketchMove?: string; + hm?: string; + restrictiveMoves?: string[]; + /** Obscure learn methods */ + restrictedMove?: ID; + /** + * `limitedEgg` tracks 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. + * + * The sentinel value `'self'` is used for Dragonite's ExtremeSpeed, + * which can only be bred from an event Dragonite, making it + * automatically incompatible with every other Dragonite egg move. + */ + limitedEgg?: (ID | 'self')[] | null; + isHidden: boolean | null; + + constructor(sourcesBefore = 0, sourcesAfter = 0) { + this.sources = []; + this.sourcesBefore = sourcesBefore; + this.sourcesAfter = sourcesAfter; + this.isHidden = null; + this.limitedEgg = undefined; + } + size() { + if (this.sourcesBefore) return Infinity; + return this.sources.length; + } + add(source: PokemonSource, limitedEgg?: ID | 'self') { + if (this.sources[this.sources.length - 1] !== source) this.sources.push(source); + if (limitedEgg && this.limitedEgg !== null) { + this.limitedEgg = [limitedEgg]; + } + } + addGen(sourceGen: number) { + this.sourcesBefore = Math.max(this.sourcesBefore, sourceGen); + this.limitedEgg = null; + } + minSourceGen() { + 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 (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), 10); + 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), 10); + 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.limitedEgg) { + if (!this.limitedEgg) { + this.limitedEgg = other.limitedEgg; + } else { + this.limitedEgg.push(...other.limitedEgg); + } + } + if (other.sourcesAfter > this.sourcesAfter) this.sourcesAfter = other.sourcesAfter; + if (other.isHidden) this.isHidden = true; + } +} + export class TeamValidator { readonly format: Format; readonly dex: ModdedDex; @@ -50,17 +211,26 @@ export class TeamValidator { // 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. - // The usual limit of 6 pokemon is handled elsewhere - currently - // in the cartridge-compliant set validator: rulesets.js:pokemon if (team.length > 24) { problems.push(`Your team has more than than 24 Pok\u00E9mon, which the simulator can't handle.`); return problems; } + if (ruleTable.isBanned('nonexistent') && team.length > 6) { + problems.push(`Your team has more than than 6 Pok\u00E9mon.`); + return problems; + } const teamHas: {[k: string]: number} = {}; + let lgpeStarterCount = 0; 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.`]; const 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 (setProblems) { problems = problems.concat(setProblems); } @@ -107,6 +277,7 @@ export class TeamValidator { validateSet(set: PokemonSet, teamHas: AnyObject): string[] | null { const format = this.format; const dex = this.dex; + const ruleTable = this.ruleTable; let problems: string[] = []; if (!set) { @@ -141,6 +312,9 @@ export class TeamValidator { // Just remember to set the level back to forcedLevel at the end of the file. set.level = maxLevel; } + if ((set.level > 100 || set.level < 1) && ruleTable.isBanned('nonexistent')) { + problems.push((set.name || set.species) + ' is higher than level 100.'); + } const nameTemplate = dex.getTemplate(set.name); if (nameTemplate.exists && nameTemplate.name.toLowerCase() === set.name.toLowerCase()) { @@ -149,12 +323,21 @@ export class TeamValidator { } set.name = set.name || template.baseSpecies; let name = set.species; - if (set.species !== set.name && template.baseSpecies !== set.name) name = `${set.name} (${set.species})`; - let isHidden = false; - const lsetData: PokemonSources = {sources: [], sourcesBefore: dex.gen}; + if (set.species !== set.name && template.baseSpecies !== set.name) { + name = `${set.name} (${set.species})`; + } + const setSources = this.allSources(template); const setHas: {[k: string]: true} = {}; - const ruleTable = this.ruleTable; + + const allowEVs = dex.currentMod !== 'letsgo'; + const capEVs = dex.gen > 2 && (ruleTable.has('validatemisc') || dex.gen === 6); + if (!set.evs) set.evs = TeamValidator.fillStats(null, allowEVs && !capEVs ? 252 : 0); + if (!set.ivs) set.ivs = TeamValidator.fillStats(null, 31); + + if (ruleTable.has('validateformes')) { + problems.push(...this.validateForme(set)); + } for (const [rule] of ruleTable) { const subformat = dex.getFormat(rule); @@ -171,7 +354,7 @@ export class TeamValidator { item = dex.getItem(set.item); ability = dex.getAbility(set.ability); - if (ability.id === 'battlebond' && template.id === 'greninja' && !ruleTable.has('ignoreillegalabilities')) { + if (ability.id === 'battlebond' && template.id === 'greninja' && ruleTable.has('validateabilities')) { template = dex.getTemplate('greninjaash'); if (set.gender && set.gender !== 'M') { problems.push(`Battle Bond Greninja must be male.`); @@ -206,82 +389,48 @@ export class TeamValidator { } } if (set.happiness !== undefined && isNaN(set.happiness)) { - problems.push(`${name} has an invalid happiness.`); + problems.push(`${name} has an invalid happiness value.`); } if (set.hpType && (!dex.getType(set.hpType).exists || ['normal', 'fairy'].includes(toID(set.hpType)))) { problems.push(`${name}'s Hidden Power type (${set.hpType}) is invalid.`); } - let banReason = ruleTable.check('pokemon:' + template.id, setHas); - let templateOverride = ruleTable.has('+pokemon:' + template.id); - if (!templateOverride) { - banReason = banReason || ruleTable.check('basepokemon:' + toID(template.baseSpecies), setHas); - } - if (banReason) { - return [`${set.species} is ${banReason}.`]; - } - templateOverride = templateOverride || ruleTable.has('+basepokemon:' + toID(template.baseSpecies)); let postMegaTemplate = template; if (item.megaEvolves === template.species) { if (!item.megaStone) throw new Error(`Item ${item.name} has no base form for mega evolution`); postMegaTemplate = dex.getTemplate(item.megaStone); } - if (['Mega', 'Mega-X', 'Mega-Y'].includes(postMegaTemplate.forme)) { - templateOverride = ruleTable.has('+pokemon:' + postMegaTemplate.id); - banReason = ruleTable.check('pokemon:' + postMegaTemplate.id, setHas); - if (banReason) { - problems.push(`${postMegaTemplate.species} is ${banReason}.`); - } else if (!templateOverride) { - banReason = ruleTable.check('pokemontag:mega', setHas); - if (banReason) problems.push(`Mega evolutions are ${banReason}.`); - } - } - if (!templateOverride) { - if (ruleTable.has('-unreleased') && postMegaTemplate.isUnreleased) { - problems.push(`${name} (${postMegaTemplate.species}) is unreleased.`); - } else if (ruleTable.has('-illegal') && postMegaTemplate.tier === 'Illegal') { - problems.push(`${name} (${postMegaTemplate.species}) is not obtainable in this game.`); - } else if (postMegaTemplate.tier) { - let tag = postMegaTemplate.tier === '(PU)' ? 'ZU' : postMegaTemplate.tier; - banReason = ruleTable.check('pokemontag:' + toID(tag), setHas); - if (banReason) { - problems.push(`${postMegaTemplate.species} is in ${tag}, which is ${banReason}.`); - } else if (postMegaTemplate.doublesTier) { - tag = postMegaTemplate.doublesTier === '(DUU)' ? 'DNU' : postMegaTemplate.doublesTier; - banReason = ruleTable.check('pokemontag:' + toID(tag), setHas); - if (banReason) { - problems.push(`${postMegaTemplate.species} is in ${tag}, which is ${banReason}.`); - } + + let problem = this.checkSpecies(set, template, postMegaTemplate, setHas); + if (problem) problems.push(problem); + + problem = this.checkItem(set, item, setHas); + if (problem) problems.push(problem); + if (ruleTable.has('validatemisc')) { + if (dex.gen <= 1 || ruleTable.has('allowavs')) { + if (item.id) { + // no items allowed + set.item = ''; } } } - banReason = ruleTable.check('ability:' + toID(set.ability), setHas); - if (banReason) { - problems.push(`${name}'s ability ${set.ability} is ${banReason}.`); - } - banReason = ruleTable.check('item:' + toID(set.item), setHas); - if (banReason) { - problems.push(`${name}'s item ${set.item} is ${banReason}.`); - } - if (ruleTable.has('-unreleased') && item.isUnreleased && !ruleTable.has('+item:' + item.id)) { - problems.push(`${name}'s item ${set.item} is unreleased.`); - } - if (!set.ability) set.ability = 'No Ability'; - setHas[toID(set.ability)] = true; - if (ruleTable.has('-illegal')) { - // Don't check abilities for metagames with All Abilities - if (dex.gen <= 2) { + if (ruleTable.has('validateabilities')) { + if (dex.gen <= 2 || ruleTable.has('allowavs')) { set.ability = 'No Ability'; - } else if (!ruleTable.has('ignoreillegalabilities')) { - if (!ability.name) { + } else { + if (!ability.name || ability.name === 'No Ability') { problems.push(`${name} needs to have an ability.`); } else if (!Object.values(template.abilities).includes(ability.name)) { - problems.push(`${name} can't have ${set.ability}.`); + if (postMegaTemplate.abilities[0] === ability.name) { + set.ability = template.abilities[0]; + } else { + problems.push(`${name} can't have ${set.ability}.`); + } } if (ability.name === template.abilities['H']) { - isHidden = true; + setSources.isHidden = true; if (template.unreleasedHidden && ruleTable.has('-unreleased')) { problems.push(`${name}'s Hidden Ability is unreleased.`); @@ -292,15 +441,25 @@ export class TeamValidator { (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 && (template.maleOnlyHidden || template.gender === 'N')) { - problems.push(`${name} must be at least level 10 with its Hidden Ability.`); + problems.push(`${name} must be at least level 10 to have a Hidden Ability.`); } if (template.maleOnlyHidden) { + if (set.gender && set.gender !== 'M') { + problems.push(`${name} must be male to have a Hidden Ability.`); + } set.gender = 'M'; - lsetData.sources = ['5D']; + setSources.sources = ['5D']; } + } else { + setSources.isHidden = false; } } } + + ability = dex.getAbility(set.ability); + problem = this.checkAbility(set, ability, setHas); + if (problem) problems.push(problem); + if (set.moves && Array.isArray(set.moves)) { set.moves = set.moves.filter(val => val); } @@ -311,42 +470,31 @@ export class TeamValidator { // 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. - // The usual limit of 4 moves is handled elsewhere - currently - // in the cartridge-compliant set validator: rulesets.js:pokemon if (set.moves.length > 24) { problems.push(`${name} has more than 24 moves, which the simulator can't handle.`); return problems; } + if (ruleTable.isBanned('nonexistent') && set.moves.length > 4) { + problems.push(`${name} has more than 4 moves.`); + return problems; + } - set.ivs = TeamValidator.fillStats(set.ivs, 31); - let ivs: StatsTable = set.ivs; - const maxedIVs = Object.values(ivs).every(stat => stat === 31); + if (ruleTable.isBanned('nonexistent')) { + problems.push(...this.validateStats(set, template, setSources)); + } let lsetProblem = null; for (const moveName of set.moves) { if (!moveName) continue; const move = dex.getMove(Dex.getString(moveName)); if (!move.exists) return [`"${move.name}" is an invalid move.`]; - banReason = ruleTable.check('move:' + move.id, setHas); - if (banReason) { - problems.push(`${name}'s move ${move.name} is ${banReason}.`); - } - // Note that we don't error out on multiple Hidden Power types - // That is checked in rulesets.js rule Pokemon - if (move.id === 'hiddenpower' && move.type !== 'Normal' && !set.hpType) { - set.hpType = move.type; - } + problem = this.checkMove(set, move, setHas); + if (problem) problems.push(problem); - if (ruleTable.has('-unreleased')) { - if (move.isUnreleased && !ruleTable.has('+move:' + move.id)) { - problems.push(`${name}'s move ${move.name} is unreleased.`); - } - } - - if (ruleTable.has('-illegal')) { + if (ruleTable.has('validatemoves')) { const checkLearnset = (ruleTable.checkLearnset && ruleTable.checkLearnset[0] || this.checkLearnset); - lsetProblem = checkLearnset.call(this, move, template, lsetData, set); + lsetProblem = checkLearnset.call(this, move, template, setSources, set); if (lsetProblem) { lsetProblem.moveName = move.name; break; @@ -354,113 +502,31 @@ export class TeamValidator { } } - const canBottleCap = (dex.gen >= 7 && set.level === 100); - if (set.hpType && maxedIVs && ruleTable.has('pokemon')) { - if (dex.gen <= 2) { - const HPdvs = dex.getType(set.hpType).HPdvs; - ivs = set.ivs = {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30}; - let statName: StatName; - for (statName in HPdvs) { - ivs[statName] = HPdvs[statName]! * 2; - } - ivs.hp = -1; - } else if (!canBottleCap) { - ivs = set.ivs = TeamValidator.fillStats(dex.getType(set.hpType).HPivs, 31); - } - } - if (set.hpType === 'Fighting' && ruleTable.has('pokemon')) { - if (template.gen >= 6 && template.eggGroups[0] === 'Undiscovered' && !template.nfe && - (template.baseSpecies !== 'Diancie' || !set.shiny)) { - // 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.`); - } - } - const ivHpType = dex.getHiddenPower(set.ivs).type; - if (!canBottleCap && ruleTable.has('pokemon') && set.hpType && set.hpType !== ivHpType) { - problems.push(`${name} has Hidden Power ${set.hpType}, but its IVs are for Hidden Power ${ivHpType}.`); - } - if (dex.gen <= 2) { - // validate DVs - 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 && !template.gender) { - // Gen 2 gender is calculated from the Atk DV. - // High Atk DV <-> M. The meaning of "high" depends on the gender ratio. - let genderThreshold = template.genderRatio.F * 16; - if (genderThreshold === 4) genderThreshold = 5; - if (genderThreshold === 8) genderThreshold = 7; - - 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 (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).`); - } - } - } - if (dex.gen <= 2 || dex.gen !== 6 && (format.id.endsWith('hackmons') || format.name.includes('BH'))) { - if (!set.evs) set.evs = TeamValidator.fillStats(null, 252); - const evTotal = (set.evs.hp || 0) + (set.evs.atk || 0) + (set.evs.def || 0) + - (set.evs.spa || 0) + (set.evs.spd || 0) + (set.evs.spe || 0); - if (evTotal === 508 || evTotal === 510) { - problems.push(`${name} has exactly 510 EVs, but this format does not restrict you to 510 EVs: you can max out every EV (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).`); - } - } - const noEVs = (!set.evs || !Object.values(set.evs).some(value => value > 0)); - if (noEVs && !format.debug && !format.id.includes('letsgo')) { - 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).`); - } - - lsetData.isHidden = isHidden; - const lsetProblems = this.reconcileLearnset(template, lsetData, lsetProblem, name); + const lsetProblems = this.reconcileLearnset(template, setSources, lsetProblem, name); if (lsetProblems) problems.push(...lsetProblems); - if (!lsetData.sourcesBefore && lsetData.sources.length && - lsetData.sources.every(source => 'SVD'.includes(source.charAt(1)))) { + if (!setSources.sourcesBefore && setSources.sources.length && + setSources.sources.every(source => 'SVD'.includes(source.charAt(1)))) { // Every source is restricted let legal = false; - for (const source of lsetData.sources) { + for (const source of setSources.sources) { if (this.validateSource(set, source, template)) continue; legal = true; break; } if (!legal) { - if (lsetData.sources.length > 1) { + 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, lsetData.sources[0], template, ` because it has a move only available` + set, setSources.sources[0], template, ` because it has a move only available` ); - // @ts-ignore validateEvent must have returned an array because it was passed a because param if (eventProblems) problems.push(...eventProblems); } - } else if (ruleTable.has('-illegal') && template.eventOnly) { - const eventTemplate = !template.learnset && template.baseSpecies !== template.species && - template.id !== 'zygarde10' ? dex.getTemplate(template.baseSpecies) : template; + } else if (ruleTable.has('validatemisc') && template.eventOnly) { + const eventTemplate = !template.eventPokemon && template.baseSpecies !== template.species ? + dex.getTemplate(template.baseSpecies) : template; const eventPokemon = eventTemplate.eventPokemon; if (!eventPokemon) throw new Error(`Event-only template ${template.species} has no eventPokemon table`); let legal = false; @@ -494,24 +560,26 @@ export class TeamValidator { if (eventProblems) problems.push(...eventProblems); } } - if (ruleTable.has('-illegal') && set.level < (template.evoLevel || 0)) { + if (ruleTable.has('validatemisc') && set.level < (template.evoLevel || 0)) { // FIXME: Event pokemon given at a level under what it normally can be attained at gives a false positive problems.push(`${name} must be at least level ${template.evoLevel} to be evolved.`); } - if (ruleTable.has('-illegal') && template.id === 'keldeo' && set.moves.includes('secretsword') && + if (ruleTable.has('validatemoves') && template.id === 'keldeo' && set.moves.includes('secretsword') && (format.requirePlus || format.requirePentagon)) { problems.push(`${name} has Secret Sword, which is only compatible with Keldeo-Ordinary obtained from Gen 5.`); } - const hasGen3moves = !lsetData.sources && lsetData.sourcesBefore <= 3; - if (hasGen3moves && dex.getAbility(set.ability).gen === 4 && !template.prevo && dex.gen <= 5) { - problems.push(`${name} has a gen 4 ability and isn't evolved - it can't use moves from gen 3.`); + const requiresGen3Source = setSources.maxSourceGen() <= 3; + if (requiresGen3Source && dex.getAbility(set.ability).gen === 4 && !template.prevo && dex.gen <= 5) { + problems.push(`${name} has a Gen 4 ability and isn't evolved - it can't use moves from Gen 3.`); } - if (!lsetData.sources && lsetData.sourcesBefore < 6 && lsetData.sourcesBefore >= 3 && - (isHidden || dex.gen <= 5) && template.gen <= lsetData.sourcesBefore) { - const oldAbilities = dex.mod('gen' + lsetData.sourcesBefore).getTemplate(set.species).abilities; - if (ability.name !== oldAbilities['0'] && ability.name !== oldAbilities['1'] && !oldAbilities['H']) { - problems.push(`${name} has moves incompatible with its ability.`); - } + if (setSources.maxSourceGen() < 5 && setSources.isHidden) { + problems.push(`${name} has a Hidden Ability - it can't use moves from before Gen 5.`); + } + if ( + template.maleOnlyHidden && 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) { @@ -533,10 +601,8 @@ export class TeamValidator { problems.push(`${name} is limited to ${limit} of ${rule}${clause}.`); } else if (!limit && count >= bans.length) { const clause = source ? ` by ${source}` : ``; - if (source === 'Pokemon') { - if (ruleTable.has('-illegal')) { - problems.push(`${name} has the combination of ${rule}, which is impossible to obtain legitimately.`); - } + 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}.`); } @@ -562,6 +628,206 @@ export class TeamValidator { return problems; } + validateStats(set: PokemonSet, template: Template, setSources: PokemonSources) { + const ruleTable = this.ruleTable; + const dex = this.dex; + + const allowEVs = dex.currentMod !== 'letsgo'; + const allowAVs = ruleTable.has('allowavs'); + const capEVs = dex.gen > 2 && (ruleTable.has('validatemisc') || dex.gen === 6); + const canBottleCap = dex.gen >= 7 && (set.level === 100 || !ruleTable.has('validatemisc')); + + if (!set.evs) set.evs = TeamValidator.fillStats(null, allowEVs && !capEVs ? 252 : 0); + if (!set.ivs) set.ivs = TeamValidator.fillStats(null, 31); + + const problems = []; + const name = set.name || set.species; + const statTable = { + hp: 'HP', atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed', + }; + + const maxedIVs = Object.values(set.ivs).every(stat => stat === 31); + for (const moveName of set.moves) { + const move = dex.getMove(moveName); + if (move.id === 'hiddenpower' && move.type !== 'Normal') { + if (!set.hpType) { + set.hpType = move.type; + } else if (set.hpType !== move.type && ruleTable.has('validatemisc')) { + problems.push(`${name}'s Hidden Power type ${set.hpType} is incompatible with Hidden Power ${move.type}`); + } + } + } + if (set.hpType && maxedIVs && ruleTable.has('validatemisc')) { + if (dex.gen <= 2) { + const HPdvs = dex.getType(set.hpType).HPdvs; + set.ivs = {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30}; + let statName: StatName; + for (statName in HPdvs) { + set.ivs[statName] = HPdvs[statName]! * 2; + } + set.ivs.hp = -1; + } else if (!canBottleCap) { + set.ivs = TeamValidator.fillStats(dex.getType(set.hpType).HPivs, 31); + } + } + + const cantBreedNorEvolve = (template.eggGroups[0] === 'Undiscovered' && !template.prevo && !template.nfe); + const isLegendary = (cantBreedNorEvolve && ![ + 'Unown', 'Pikachu', + ].includes(template.baseSpecies)) || [ + 'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', 'Manaphy', 'Meltan', 'Melmetal', + ].includes(template.baseSpecies); + const diancieException = template.species === 'Diancie' && set.shiny; + const has3PerfectIVs = setSources.minSourceGen() >= 6 && isLegendary && !diancieException; + + if (set.hpType === 'Fighting' && ruleTable.has('validatemisc')) { + 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) { + let perfectIVs = 0; + for (const stat in set.ivs) { + if (set.ivs[stat as 'hp'] >= 31) perfectIVs++; + } + if (perfectIVs < 3) { + const reason = (this.format.requirePentagon ? ` and this format requires gen ${dex.gen} Pokémon` : ` in gen 6`); + problems.push(`${name} must have at least three perfect IVs because it's a legendary${reason}.`); + } + } + + 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 && !template.gender) { + // Gen 2 gender is calculated from the Atk DV. + // High Atk DV <-> M. The meaning of "high" depends on the gender ratio. + let genderThreshold = template.genderRatio.F * 16; + if (genderThreshold === 4) genderThreshold = 5; + if (genderThreshold === 8) genderThreshold = 7; + + 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'; + } + + if (dex.currentMod === 'letsgo') { // 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 ${statTable[stat as 'hp']}.`); + } + } + } else { // EVs + for (const stat in set.evs) { + if (set.evs[stat as 'hp'] > 255) { + problems.push(`${name} has more than 255 EVs in ${statTable[stat as 'hp']}.`); + } + } + } + + let totalEV = 0; + for (const stat in set.evs) totalEV += set.evs[stat as 'hp']; + + if (!this.format.debug) { + if ((allowEVs || allowAVs) && 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 (allowEVs && !capEVs && [508, 510].includes(totalEV)) { + problems.push(`${name} has exactly 510 EVs, but this format does not restrict you to 510 EVs: you can max out every EV (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 (allowEVs && capEVs && totalEV > 510) { + problems.push(`${name} has more than 510 total EVs.`); + } + + 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, template: Template, because: string, from?: string + ): string[] | undefined; + validateSource(set: PokemonSet, source: PokemonSource, template: Template): true | undefined; /** * Returns array of error messages if invalid, undefined if valid * @@ -599,9 +865,287 @@ export class TeamValidator { throw new Error(`Unidentified source ${source} passed to validateSource`); } + // @ts-ignore complicated fancy return signature return this.validateEvent(set, eventData, eventTemplate, because, from); } + validateForme(set: PokemonSet) { + const dex = this.dex; + const name = set.name || set.species; + + const problems = []; + const item = dex.getItem(set.item); + const template = dex.getTemplate(set.species); + const battleForme = template.battleOnly && template.species; + + if (battleForme) { + if (template.requiredAbility && set.ability !== template.requiredAbility) { + // Darmanitan-Zen, Zygarde-Complete + problems.push(`${template.species} transforms in-battle with ${template.requiredAbility}.`); + } + if (template.requiredItems) { + if (template.species === 'Necrozma-Ultra') { + // Necrozma-Ultra transforms from one of two formes, and neither one is the base forme + problems.push(`Necrozma-Ultra must start the battle as Necrozma-Dawn-Wings or Necrozma-Dusk-Mane holding Ultranecrozium Z.`); + } else if (!template.requiredItems.includes(item.name)) { + // Mega or Primal + problems.push(`${template.species} transforms in-battle with ${Chat.toOrList(template.requiredItems)}.`); + } + } + if (template.requiredMove && !set.moves.includes(toID(template.requiredMove))) { + // Meloetta-Pirouette, Rayquaza-Mega + problems.push(`${template.species} transforms in-battle with ${template.requiredMove}.`); + } + set.species = template.baseSpecies; // Fix battle-only forme + } else { + if (template.requiredAbility) { + // Impossible! + throw new Error(`Species ${template.name} has a required ability despite not being a battle-only forme; it should just be in its abilities table.`); + } + if (template.requiredItems && !template.requiredItems.includes(item.name)) { + // Memory/Drive/Griseous Orb/Plate/Z-Crystal - Forme mismatch + problems.push(`${name} needs to hold ${Chat.toOrList(template.requiredItems)}.`); + } + if (template.requiredMove && !set.moves.includes(toID(template.requiredMove))) { + // Keldeo-Resolute + problems.push(`${name} needs to have the move ${template.requiredMove}.`); + } + + // 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 && template.species === dex.getTemplate(item.forcedForme).baseSpecies) { + set.species = item.forcedForme; + } + } + + if (template.species === '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; + } + } + } + return problems; + } + + checkSpecies(set: PokemonSet, template: Template, postMegaTemplate: Template, setHas: {[k: string]: true}) { + const dex = this.dex; + const ruleTable = this.ruleTable; + + setHas['pokemon:' + template.id] = true; + setHas['basepokemon:' + toID(template.baseSpecies)] = true; + + let isMega = false; + if (postMegaTemplate !== template) { + setHas['pokemon:' + postMegaTemplate.id] = true; + setHas['pokemontag:mega'] = true; + isMega = true; + } + + const tier = postMegaTemplate.tier === '(PU)' ? 'ZU' : postMegaTemplate.tier; + const tierTag = 'pokemontag:' + toID(tier); + setHas[tierTag] = true; + + const doublesTier = postMegaTemplate.doublesTier === '(DUU)' ? 'DNU' : postMegaTemplate.doublesTier; + const doublesTierTag = 'pokemontag:' + toID(doublesTier); + setHas[doublesTierTag] = true; + + let banReason = ruleTable.check('pokemon:' + template.id); + if (banReason) { + return `${template.species} is ${banReason}.`; + } + if (banReason === '') return null; + + if (isMega) { + banReason = ruleTable.check('pokemon:' + postMegaTemplate.id); + if (banReason) { + return `${postMegaTemplate.species} is ${banReason}.`; + } + if (banReason === '') return null; + + banReason = ruleTable.check('pokemontag:mega', setHas); + if (banReason) { + return `Mega evolutions are ${banReason}.`; + } + } + + banReason = ruleTable.check('basepokemon:' + toID(template.baseSpecies)); + if (banReason) { + return `${template.species} is ${banReason}.`; + } + if (banReason === '') { + // don't allow nonstandard templates when whitelisting standard base species + // i.e. unbanning Pichu doesn't mean allowing Pichu-Spiky-Eared outside of Gen 4 + const baseTemplate = dex.getTemplate(template.baseSpecies); + if (baseTemplate.isNonstandard === template.isNonstandard) { + return null; + } + } + + banReason = ruleTable.check(tierTag); + if (banReason) { + return `${postMegaTemplate.species} is in ${tier}, which is ${banReason}.`; + } + if (banReason === '') return null; + + banReason = ruleTable.check(doublesTierTag); + if (banReason) { + return `${postMegaTemplate.species} is in ${doublesTier}, which is ${banReason}.`; + } + if (banReason === '') return null; + + // obtainability + if (postMegaTemplate.isNonstandard) { + banReason = ruleTable.check('pokemontag:' + toID(postMegaTemplate.isNonstandard)); + if (banReason) { + return `${postMegaTemplate.species} is tagged ${postMegaTemplate.isNonstandard}, which is ${banReason}.`; + } + if (banReason === '') return null; + } + + if ( + postMegaTemplate.isNonstandard === 'Pokestar' && dex.gen === 5 || + postMegaTemplate.isNonstandard === 'Glitch' && dex.gen === 1 + ) { + banReason = ruleTable.check('pokemontag:hackmons', setHas); + if (banReason) { + return `${postMegaTemplate.species} is not obtainable without hacking.`; + } + if (banReason === '') return null; + } else if (postMegaTemplate.isNonstandard) { + banReason = ruleTable.check('nonexistent', setHas); + if (banReason) { + if (['Past', 'Future'].includes(postMegaTemplate.isNonstandard)) { + return `${postMegaTemplate.species} does not exist in Gen ${dex.gen}.`; + } + return `${postMegaTemplate.species} does not exist in this game.`; + } + if (banReason === '') return null; + } else if (postMegaTemplate.isUnreleased) { + banReason = ruleTable.check('unreleased', setHas); + if (banReason) { + return `${postMegaTemplate.species} is unreleased.`; + } + if (banReason === '') return null; + } + + return null; + } + + checkItem(set: PokemonSet, item: Item, setHas: {[k: string]: true}) { + const dex = this.dex; + const ruleTable = this.ruleTable; + + setHas['move:' + item.id] = true; + + let banReason = ruleTable.check('item:' + item.id); + if (banReason) { + return `${set.name}'s item ${item.name} is ${banReason}.`; + } + if (banReason === '') return null; + + // obtainability + if (item.isNonstandard) { + banReason = ruleTable.check('pokemontag:' + toID(item.isNonstandard)); + if (banReason) { + return `${set.name}'s item ${item.name} is tagged ${item.isNonstandard}, which is ${banReason}.`; + } + if (banReason === '') return null; + + 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; + } else if (item.isUnreleased) { + banReason = ruleTable.check('unreleased', setHas); + if (banReason) { + return `${set.name}'s item ${item.name} is unreleased.`; + } + 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; + + // obtainability + if (move.isNonstandard) { + banReason = ruleTable.check('pokemontag:' + toID(move.isNonstandard)); + if (banReason) { + return `${set.name}'s move ${move.name} is tagged ${move.isNonstandard}, which is ${banReason}.`; + } + if (banReason === '') return null; + + 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; + + let banReason = ruleTable.check('ability:' + ability.id); + if (banReason) { + return `${set.name}'s ability ${ability.name} is ${banReason}.`; + } + if (banReason === '') return null; + + // 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; + } + + validateEvent(set: PokemonSet, eventData: EventInfo, eventTemplate: Template): true | undefined; + validateEvent( + set: PokemonSet, eventData: EventInfo, eventTemplate: Template, because: string, from?: string + ): string[] | undefined; /** * Returns array of error messages if invalid, undefined if valid * @@ -715,7 +1259,7 @@ export class TeamValidator { } // Event-related ability restrictions only matter if we care about illegal abilities const ruleTable = this.ruleTable; - if (!ruleTable.has('ignoreillegalabilities')) { + if (ruleTable.has('validateabilities')) { if (dex.gen <= 5 && eventData.abilities && eventData.abilities.length === 1 && !eventData.isHidden) { if (template.species === eventTemplate.species) { // has not evolved, abilities must match @@ -753,8 +1297,18 @@ export class TeamValidator { if (eventData.gender) set.gender = eventData.gender; } + allSources(template?: Template) { + let minPastGen = ( + this.format.requirePlus ? 7 : + this.format.requirePentagon ? 6 : + this.dex.gen >= 3 ? 3 : 1 + ); + if (template) minPastGen = Math.max(minPastGen, template.gen); + return new PokemonSources(this.dex.gen, minPastGen); + } + reconcileLearnset( - species: Template, lsetData: PokemonSources, problem: {type: string, moveName: string, [key: string]: any} | null, + species: Template, setSources: PokemonSources, problem: {type: string, moveName: string, [key: string]: any} | null, name: string = species.species ) { const dex = this.dex; @@ -765,7 +1319,7 @@ export class TeamValidator { if (problem.type === 'incompatibleAbility') { problemString += ` can only be learned in past gens without Hidden Abilities.`; } else if (problem.type === 'incompatible') { - problemString = `${name}'s moves ${(lsetData.restrictiveMoves || []).join(', ')} are incompatible.`; + problemString = `${name}'s moves ${(setSources.restrictiveMoves || []).join(', ')} are incompatible.`; } else if (problem.type === 'oversketched') { const plural = (parseInt(problem.maxSketches, 10) === 1 ? '' : 's'); problemString += ` can't be Sketched because it can only Sketch ${problem.maxSketches} move${plural}.`; @@ -781,24 +1335,24 @@ export class TeamValidator { if (problems.length) return problems; - if (lsetData.isHidden) { - lsetData.sources = lsetData.sources.filter(source => + if (setSources.isHidden) { + setSources.sources = setSources.sources.filter(source => parseInt(source.charAt(0), 10) >= 5 ); - if (lsetData.sourcesBefore < 5) lsetData.sourcesBefore = 0; - if (!lsetData.sourcesBefore && !lsetData.sources.length) { + if (setSources.sourcesBefore < 5) setSources.sourcesBefore = 0; + if (!setSources.sourcesBefore && !setSources.sources.length) { problems.push(`${name} has a hidden ability - it can't have moves only learned before gen 5.`); return problems; } } - if (lsetData.limitedEgg && lsetData.limitedEgg.length > 1 && !lsetData.sourcesBefore && lsetData.sources) { - // console.log("limitedEgg 1: " + lsetData.limitedEgg); + if (setSources.limitedEgg && setSources.limitedEgg.length > 1 && !setSources.sourcesBefore && setSources.sources) { + // console.log("limitedEgg 1: " + setSources.limitedEgg); // Multiple gen 2-5 egg moves // This code hasn't been closely audited for multi-gen interaction, but // since egg moves don't get removed between gens, it's unlikely to have // any serious problems. - const limitedEgg = [...new Set(lsetData.limitedEgg)]; + const limitedEgg = [...new Set(setSources.limitedEgg)]; if (limitedEgg.length <= 1) { // Only one source, can't conflict with anything else } else if (limitedEgg.includes('self')) { @@ -810,7 +1364,7 @@ export class TeamValidator { // They're probably incompatible if all potential fathers learn more than // one limitedEgg move from another egg. let validFatherExists = false; - for (const source of lsetData.sources) { + for (const source of setSources.sources) { if (source.charAt(1) === 'S' || source.charAt(1) === 'D') continue; let eggGen = parseInt(source.charAt(0), 10); if (source.charAt(1) !== 'E' || eggGen === 6) { @@ -880,12 +1434,12 @@ export class TeamValidator { // TODO: hardcode false positives for our heuristic // in theory, this heuristic doesn't have false negatives const newSources = []; - for (const source of lsetData.sources) { + for (const source of setSources.sources) { if (source.charAt(1) === 'S') { newSources.push(source); } } - lsetData.sources = newSources; + setSources.sources = newSources; if (!newSources.length) { const moveNames = limitedEgg.map(id => dex.getMove(id).name); problems.push(`${name}'s past gen egg moves ${moveNames.join(', ')} do not have a valid father. (Is this incorrect? If so, post the chainbreeding instructions in Bug Reports)`); @@ -894,9 +1448,9 @@ export class TeamValidator { } } - if (lsetData.babyOnly && lsetData.sources.length) { - const babyid = lsetData.babyOnly; - lsetData.sources = lsetData.sources.filter(source => { + if (setSources.babyOnly && setSources.sources.length) { + const babyid = setSources.babyOnly; + setSources.sources = setSources.sources.filter(source => { if (source.charAt(1) === 'S') { const sourceSpeciesid = source.split(' ')[1]; if (sourceSpeciesid !== babyid) return false; @@ -906,7 +1460,7 @@ export class TeamValidator { } return true; }); - if (!lsetData.sources.length && !lsetData.sourcesBefore) { + if (!setSources.sources.length && !setSources.sourcesBefore) { const babySpecies = dex.getTemplate(babyid).species; problems.push(`${name}'s event/egg moves are from an evolution, and are incompatible with its moves from ${babySpecies}.`); } @@ -918,15 +1472,21 @@ export class TeamValidator { checkLearnset( move: Move, species: Template, - lsetData: PokemonSources = {sources: [], sourcesBefore: this.dex.gen}, - set: AnyObject = {} + setSources = this.allSources(species), + set: AnyObject | true = {} ): {type: string, [key: string]: any} | null { const dex = this.dex; + let fastCheck = false; + if (set === true) { + fastCheck = true; + set = {}; + } + if (!setSources.size()) throw new Error(`Bad sources passed to checkLearnset`); const moveid = toID(move); - if (moveid === 'constructor') return {type: 'invalid'}; move = dex.getMove(moveid); - let template: Template | null = dex.getTemplate(species); + const baseTemplate = dex.getTemplate(species); + let template: Template | null = baseTemplate; const format = this.format; const ruleTable = dex.getRuleTable(format); @@ -934,8 +1494,6 @@ export class TeamValidator { const level = set.level || 100; let incompatibleAbility = false; - let isHidden = false; - if (set.ability && dex.getAbility(set.ability).name === template.abilities['H']) isHidden = true; let limit1 = true; let sketch = false; @@ -952,17 +1510,8 @@ export class TeamValidator { // 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. - // We apply several optimizations to this algorithm. The most - // important is that with, for instance, a TM move, that Pokemon - // could have been obtained from any gen at or before that TM's gen. - // Instead of adding every possible source before or during that gen, - // we keep track of a maximum gen variable, intended to mean "any - // source at or before this gen is possible." - - // set of possible sources of a pokemon with this move, represented as an array - const sources: PokemonSource[] = []; - // the equivalent of adding "every source at or before this gen" to sources - let sourcesBefore = 0; + // set of possible sources of a pokemon with this move + const moveSources = new PokemonSources(); /** * The minimum past gen the format allows @@ -978,11 +1527,6 @@ export class TeamValidator { * (This is everything except in Gen 1 Tradeback) */ const noFutureGen = !ruleTable.has('allowtradeback'); - /** - * If a move can only be learned from a gen 2-5 egg, we have to check chainbreeding validity - * limitedEgg is false if there are any legal non-egg sources for the move, and true otherwise - */ - let limitedEgg = null; let tradebackEligible = false; while (template && template.species && !alreadyChecked[template.speciesid]) { @@ -1000,8 +1544,8 @@ export class TeamValidator { break; } const checkingPrevo = template.baseSpecies !== species.baseSpecies; - if (checkingPrevo && !sources.length && !sourcesBefore) { - if (!lsetData.babyOnly || !template.prevo) { + if (checkingPrevo && !moveSources.size()) { + if (!setSources.babyOnly || !template.prevo) { babyOnly = template.speciesid; } } @@ -1025,10 +1569,10 @@ export class TeamValidator { // 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.) - // `sources.push(source)` + // `moveSources.add(source)` // means we can learn it only if obtained that exact way described // in source - // `sourcesBefore = Math.max(sourcesBefore, learnedGen)` + // `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.) @@ -1038,9 +1582,12 @@ export class TeamValidator { if (noFutureGen && learnedGen > dex.gen) continue; // redundant - if (learnedGen <= sourcesBefore) continue; + if (learnedGen <= moveSources.sourcesBefore) continue; - if (learnedGen < 7 && isHidden && !dex.mod('gen' + learnedGen).getTemplate(template.species).abilities['H']) { + if ( + learnedGen < 7 && setSources.isHidden && + !dex.mod('gen' + learnedGen).getTemplate(template.species).abilities['H'] + ) { // check if the Pokemon's hidden ability was available incompatibleAbility = true; continue; @@ -1066,7 +1613,7 @@ export class TeamValidator { } else if ((!template.gender || template.gender === 'F') && learnedGen >= 2) { // available as egg move learned = learnedGen + 'Eany'; - limitedEgg = false; + moveSources.limitedEgg = null; // falls through to E check below } else { // this move is unavailable, skip it @@ -1074,26 +1621,25 @@ export class TeamValidator { } } - if ('LMT'.includes(learned.charAt(1))) { - if (learnedGen === dex.gen) { + if ('LMTR'.includes(learned.charAt(1))) { + if (learnedGen === dex.gen && learned.charAt(1) !== 'R') { // current-gen level-up, TM or tutor moves: // always available - if (babyOnly) lsetData.babyOnly = babyOnly; + if (babyOnly) setSources.babyOnly = 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; - sourcesBefore = Math.max(sourcesBefore, learnedGen); - limitedEgg = false; + moveSources.addGen(learnedGen); } else if (learned.charAt(1) === 'E') { // egg moves: // only if that was the source - if ((learnedGen >= 6 && !noPastGenBreeding) || lsetData.fastCheck) { + if ((learnedGen >= 6 && !noPastGenBreeding) || fastCheck) { // gen 6 doesn't have egg move incompatibilities except for certain cases with baby Pokemon learned = learnedGen + 'E' + (template.prevo ? template.id : ''); - sources.push(learned); - limitedEgg = false; + moveSources.add(learned); continue; } // it's a past gen; egg moves can only be inherited from the father @@ -1101,8 +1647,9 @@ export class TeamValidator { let eggGroups = template.eggGroups; if (!eggGroups) continue; if (eggGroups[0] === 'Undiscovered') eggGroups = dex.getTemplate(template.evos[0]).eggGroups; + if (eggGroups[0] === 'Undiscovered' || !eggGroups.length) continue; let atLeastOne = false; - const fromSelf = (learned.substr(1) === 'Eany'); + const levelUpEgg = (learned.substr(1) === 'Eany'); const eggGroupsSet = new Set(eggGroups); learned = learned.substr(0, 2); // loop through pokemon for possible fathers to inherit the egg move from @@ -1118,12 +1665,11 @@ export class TeamValidator { if (!father.learnset) continue; // unless it's supposed to be self-breedable, can't inherit from self, prevos, evos, etc // only basic pokemon have egg moves, so by now all evolutions should be in alreadyChecked - if (!fromSelf && alreadyChecked[father.speciesid]) continue; - if (!fromSelf && father.evos.includes(template.id)) continue; - if (!fromSelf && father.prevo === template.id) continue; + if (!levelUpEgg && alreadyChecked[father.speciesid]) continue; + if (!levelUpEgg && father.prevo === template.id && template.evos.length === 1) continue; // father must be able to learn the move const fatherSources = father.learnset[moveid] || father.learnset['sketch']; - if (!fromSelf && !fatherSources) continue; + if (!levelUpEgg && !fatherSources) continue; // must be able to breed with father if (!father.eggGroups.some(eggGroup => eggGroupsSet.has(eggGroup))) continue; @@ -1142,24 +1688,21 @@ export class TeamValidator { atLeastOne = true; if (tradebackEligible && learnedGen === 2 && move.gen <= 1) { // can tradeback - sources.push('1ET' + father.id); + moveSources.add('1ET' + father.id, moveid); } - sources.push(learned + father.id); - if (limitedEgg !== false) limitedEgg = true; + moveSources.add(learned + father.id, moveid); } if (atLeastOne && noPastGenBreeding) { // gen 6+ doesn't have egg move incompatibilities except for certain cases with baby Pokemon learned = learnedGen + 'E' + (template.prevo ? template.id : ''); - sources.push(learned); - limitedEgg = false; + moveSources.add(learned); continue; } // chainbreeding with itself // e.g. ExtremeSpeed Dragonite if (!atLeastOne) { if (noPastGenBreeding) continue; - sources.push(learned + template.id); - limitedEgg = 'self'; + moveSources.add(learned + template.id, 'self'); } } else if (learned.charAt(1) === 'S') { // event moves: @@ -1168,17 +1711,17 @@ export class TeamValidator { // 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 - sources.push('1ST' + learned.slice(2) + ' ' + template.id); + moveSources.add('1ST' + learned.slice(2) + ' ' + template.id); } - sources.push(learned + ' ' + template.id); + moveSources.add(learned + ' ' + template.id); } else if (learned.charAt(1) === 'D') { // DW moves: // only if that was the source - sources.push(learned); + moveSources.add(learned); } else if (learned.charAt(1) === 'V') { // Virtual Console moves: // only if that was the source - if (sources[sources.length - 1] !== learned) sources.push(learned); + moveSources.add(learned); } } } @@ -1195,7 +1738,7 @@ export class TeamValidator { } } if (getGlitch) { - sourcesBefore = Math.max(sourcesBefore, 4); + moveSources.addGen(4); if (move.gen < 5) { limit1 = false; } @@ -1208,9 +1751,9 @@ export class TeamValidator { } else if (template.prevo) { template = dex.getTemplate(template.prevo); if (template.gen > Math.max(2, dex.gen)) template = null; - if (template && !template.abilities['H']) isHidden = false; - } else if (template.baseSpecies !== template.species && template.baseSpecies === 'Rotom') { - // only Rotom inherit learnsets from base + if (template && !template.abilities['H'] && setSources.isHidden) template = null; + } else if (template.baseSpecies !== template.species && ['Rotom', 'Necrozma'].includes(template.baseSpecies)) { + // only Rotom and Necrozma inherit learnsets from base template = dex.getTemplate(template.baseSpecies); } else { template = null; @@ -1219,71 +1762,35 @@ export class TeamValidator { if (limit1 && sketch) { // limit 1 sketch move - if (lsetData.sketchMove) { + if (setSources.sketchMove) { return {type: 'oversketched', maxSketches: 1}; } - lsetData.sketchMove = moveid; + setSources.sketchMove = moveid; } if (blockedHM) { // Limit one of Defog/Whirlpool to be transferred - if (lsetData.hm) return {type: 'incompatible'}; - lsetData.hm = moveid; + if (setSources.hm) return {type: 'incompatible'}; + setSources.hm = moveid; } - if (!lsetData.restrictiveMoves) { - lsetData.restrictiveMoves = []; + if (!setSources.restrictiveMoves) { + setSources.restrictiveMoves = []; } - lsetData.restrictiveMoves.push(move.name); + setSources.restrictiveMoves.push(move.name); // Now that we have our list of possible sources, intersect it with the current list - if (!sourcesBefore && !sources.length) { + if (!moveSources.size()) { if (minPastGen > 1 && sometimesPossible) return {type: 'pastgen', gen: minPastGen}; if (incompatibleAbility) return {type: 'incompatibleAbility'}; return {type: 'invalid'}; } - if (sourcesBefore || lsetData.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 (sourcesBefore > lsetData.sourcesBefore) { - for (const oldSource of lsetData.sources) { - const oldSourceGen = parseInt(oldSource.charAt(0), 10); - if (oldSourceGen <= sourcesBefore) { - sources.push(oldSource); - } - } - } else if (lsetData.sourcesBefore > sourcesBefore) { - for (const source of sources) { - const sourceGen = parseInt(source.charAt(0), 10); - if (sourceGen <= lsetData.sourcesBefore) { - lsetData.sources.push(source); - } - } - } - lsetData.sourcesBefore = sourcesBefore = Math.min(sourcesBefore, lsetData.sourcesBefore); - } - if (lsetData.sources.length) { - if (sources.length) { - const sourcesSet = new Set(sources); - const intersectSources = lsetData.sources.filter(source => sourcesSet.has(source)); - lsetData.sources = intersectSources; - } else { - lsetData.sources = []; - } - } - if (!lsetData.sources.length && !sourcesBefore) { + setSources.intersectWith(moveSources); + if (!setSources.size()) { return {type: 'incompatible'}; } - if (limitedEgg) { - // lsetData.limitedEgg = [moveid] of egg moves with potential breeding incompatibilities - // 'self' is a possible entry (namely, ExtremeSpeed on Dragonite) meaning it's always - // incompatible with any other egg move - if (!lsetData.limitedEgg) lsetData.limitedEgg = []; - lsetData.limitedEgg.push(limitedEgg === true ? moveid : limitedEgg); - } - - if (babyOnly) lsetData.babyOnly = babyOnly; + if (babyOnly) setSources.babyOnly = babyOnly; return null; } diff --git a/test/common.js b/test/common.js index 120eff127b..2463ee2c1d 100644 --- a/test/common.js +++ b/test/common.js @@ -80,8 +80,8 @@ class TestTools { if (!format.ruleset) format.ruleset = []; if (!format.banlist) format.banlist = []; - if (options.pokemon) format.ruleset.push('Pokemon'); - if (options.legality) format.banlist.push('Illegal', 'Unreleased'); + if (options.pokemon) format.ruleset.push('-Nonexistent'); + if (options.legality) format.ruleset.push('Obtainable'); if (options.preview) format.ruleset.push('Team Preview'); if (options.sleepClause) format.ruleset.push('Sleep Clause Mod'); if (options.cancel) format.ruleset.push('Cancel Mod'); diff --git a/test/sim/team-validator.js b/test/sim/team-validator.js index abade92f94..06a4324ebb 100644 --- a/test/sim/team-validator.js +++ b/test/sim/team-validator.js @@ -107,6 +107,52 @@ describe('Team Validator', function () { assert(illegal); }); + it('should handle weird things', function () { + // Necrozma-DW should use Necrozma's events, plus Moongeist Beam + let team = [ + {species: 'necrozmadawnwings', ability: 'prismarmor', shiny: true, moves: ['moongeistbeam', 'metalclaw'], evs: {hp: 1}}, + ]; + let illegal = TeamValidator.get('gen7anythinggoes').validateTeam(team); + assert.strictEqual(illegal, null); + + // Shedinja should be able to take one level-up move from ninjask in gen 3-4 + + team = [ + {species: 'shedinja', ability: 'wonderguard', moves: ['silverwind', 'swordsdance'], evs: {hp: 1}}, + ]; + illegal = TeamValidator.get('gen4ou').validateTeam(team); + assert.strictEqual(illegal, null); + + team = [ + {species: 'shedinja', ability: 'wonderguard', moves: ['silverwind', 'batonpass'], evs: {hp: 1}}, + ]; + illegal = TeamValidator.get('gen3ou').validateTeam(team); + assert.strictEqual(illegal, null); + + team = [ + {species: 'shedinja', ability: 'wonderguard', moves: ['silverwind', 'swordsdance', 'batonpass'], evs: {hp: 1}}, + {species: 'charmander', ability: 'blaze', moves: ['flareblitz', 'dragondance'], evs: {hp: 1}}, + ]; + illegal = TeamValidator.get('gen4ou').validateTeam(team); + assert(illegal); + + // Chansey can't have Chansey-only egg moves as well as Happiny-only level-up moves + + team = [ + {species: 'chansey', ability: 'naturalcure', moves: ['charm', 'seismictoss'], evs: {hp: 1}}, + ]; + illegal = TeamValidator.get('gen7ou').validateTeam(team); + assert(illegal); + + // male-only hidden abilities are incompatible with egg moves in Gen 5 + + team = [ + {species: 'combusken', ability: 'speedboost', moves: ['batonpass'], evs: {hp: 1}}, + ]; + illegal = TeamValidator.get('gen5ou').validateTeam(team); + assert(illegal); + }); + it('should reject illegal egg move combinations', function () { let team = [ {species: 'azumarill', ability: 'hugepower', moves: ['bellydrum', 'aquajet'], evs: {hp: 1}}, @@ -150,6 +196,12 @@ describe('Team Validator', function () { illegal = TeamValidator.get('gen3ou').validateTeam(team); assert(illegal); + team = [ + {species: 'hitmontop', ability: 'intimidate', moves: ["highjumpkick", 'machpunch'], evs: {hp: 1}}, + ]; + illegal = TeamValidator.get('gen3ou').validateTeam(team); + assert.strictEqual(illegal, null); + team = [ {species: 'snorlax', ability: 'immunity', moves: ['curse', 'pursuit'], evs: {hp: 1}}, ]; @@ -247,6 +299,16 @@ describe('Team Validator', function () { /********************************************************* * Custom rules *********************************************************/ + it('should support legality tags', function () { + let team = [ + {species: 'kitsunoh', ability: 'frisk', moves: ['shadowstrike'], evs: {hp: 1}}, + ]; + let illegal = TeamValidator.get('gen7anythinggoes').validateTeam(team); + assert(illegal); + illegal = TeamValidator.get('gen7anythinggoes@@@+cap').validateTeam(team); + assert.strictEqual(illegal, null); + }); + it('should allow Pokemon to be banned', function () { let team = [ {species: 'pikachu', ability: 'static', moves: ['agility', 'protect', 'thunder', 'thunderbolt'], evs: {hp: 1}},