Support value rules (#8267)

`teamLength`, `maxLevel`, `cupLevelLimit`, and `minSourceGen` no longer
exist as properties of `Format`. Instead, they're value rules that
become properties of `RuleTable`, and can be specified as custom rules
and inherited through rulesets like anything else.

See the PR for a full reckoning of changes:

https://github.com/smogon/pokemon-showdown/pull/8267
This commit is contained in:
Guangcong Luo 2021-05-06 01:16:16 -07:00 committed by GitHub
parent 1bf172fe7e
commit 55980d416c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 639 additions and 589 deletions

View File

@ -53,6 +53,10 @@ Bans are just a `-` followed by the thing you want to ban.
`- Unreleased` - ban all things that will probably be released eventually (Venusaur in Gen 8)
`- Mythical` - ban all Mythical Pokémon (such as Mew, Celebi)
`- Restricted Legendary` - ban all Restricted Legendary Pokémon (such as Zekrom, Eternatus)
`- all items` - ban all items
`- all abilities, + No Ability` - ban all abilities (No Ability needs to be specifically allowed to allow Pokemon with no abilities)
@ -202,6 +206,35 @@ Custom rules can have more complicated behavior. They can also include other rul
`Mimic Glitch` - allow Pokémon with access to Assist, Copycat, Metronome, Mimic, or Transform to gain access to almost any other move
Value rules
-----------
Certain rules can specify a value.
`Max Level = 50` - ban Pokémon above level 50
`Min Level = 10` - ban Pokémon below level 10
`Max Total Level = 200` - only allow Pokémon whose combined levels add up to 200 or below (if combined with `Picked Team Size`, only the picked team needs to be below that combined level)
`Max Team Size = 4` - you must bring a team with at most 4 pokemon (before Team Preview, in games with Team Preview)
`Min Team Size = 4` - you must bring a team with at least 4 pokemon (before Team Preview, in games with Team Preview)
`Picked Team Size = 4` - in Team Preview, you must pick exactly 4 pokemon (if this exists, `Min Team Size` will default to this number)
`Min Source Gen = 7` - only allow pokemon obtained in this generation or later
`Adjust Level = 50` - change all levels to 50, like in some in-game formats (unlike `Max Level`, this still allows moves learned above level 50)
`Adjust Level Down = 50` - change Pokémon with level above 50 to level 50 (but leave Pokémon below 50 alone), like in some in-game formats (unlike `Max Level`, this still allows moves learned above level 50)
`Force Monotype = Water` - require all Pokémon to be Water-type
`EV Limits = Atk 0-100 / Def 50-150` - require EVs to be in those ranges
In-battle rules
---------------
@ -247,9 +280,7 @@ Note: Most formats already come with one standard ruleset. Removing and adding m
`Standard NEXT` - the standard ruleset for NEXT. Allows some unreleased Pokémon and includes the Evasion Moves Clause, Nickname Clause, Sleep Clause Mod, Species Clause, OHKO Clause, HP Percentage Mod, and Cancel Mod. Bans Soul Dew.
`Standard GBU` - the standard ruleset for in-game formats, such as Battle Spot. Includes Species Clause, Item Clause, Nickname Clause, Team Preview, and Cancel Mod. Bans mythical Pokémon and restricted legendaries (e.g. Zekrom, Reshiram, Zygarde, Eternatus)
`Minimal GBU` - the standard ruleset for in-game formats but without restricted legendary bans. Still bans mythical Pokémon.
`Flat Rules` - the standard ruleset for in-game formats, such as Battle Spot. Includes Species Clause, Item Clause, Nickname Clause, and Team Preview. Bans mythical Pokémon and restricted legendaries (e.g. Zekrom, Reshiram, Zygarde, Eternatus)
`Standard NatDex` - the standard ruleset for National Dex formats. Allows the National Dex. Includes Nickname Clause, HP Percentage Mod, Cancel Mod, Endless Battle Clause.
@ -261,11 +292,19 @@ Removing rules
Put `!` in front of a rule to remove it, like:
`!Team Preview` - do not use Team Preview
`! Team Preview` - do not use Team Preview
You can use this to remove individual parts of rules, like:
`Obtainable, !Obtainable Moves` - require pokemon to be obtained legitimately, except for moves, which they can use whatever
`Obtainable, ! Obtainable Moves` - require pokemon to be obtained legitimately, except for moves, which they can use whatever
For value rules, you just put `!` in front of the rule name, no `=`:
`Flat Rules, ! Picked Team Size` - use Flat Rules, but players can pick 6
To prevent mistakes, value rules can't be changed without being removed first. Use `!!` to remove and replace a value rule:
`Flat Rules, !! Picked Team Size = 5` - use Flat Rules, but players can pick 5
Multiple rules

View File

@ -66,10 +66,10 @@ export const Formats: FormatList = [
searchShow: false,
tournamentShow: false,
rated: false,
teamLength: {
battle: 3,
},
ruleset: ['Obtainable', 'Species Clause', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod'],
ruleset: [
'Max Team Size = 3',
'Obtainable', 'Species Clause', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod',
],
},
{
name: "[Gen 8] OU",
@ -157,7 +157,6 @@ export const Formats: FormatList = [
],
mod: 'gen8',
maxLevel: 5,
ruleset: ['Little Cup', 'Standard', 'Dynamax Clause'],
banlist: [
'Corsola-Galar', 'Cutiefly', 'Drifloon', 'Gastly', 'Gothita', 'Rufflet', 'Scyther', 'Sneasel', 'Swirlix', 'Tangela', 'Vulpix-Alola', 'Woobat',
@ -192,11 +191,10 @@ export const Formats: FormatList = [
],
mod: 'gen8',
teamLength: {
validate: [1, 3],
battle: 1,
},
ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Dynamax Clause', 'Endless Battle Clause'],
ruleset: [
'Picked Team Size = 1', 'Max Team Size = 3',
'Obtainable', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Dynamax Clause', 'Endless Battle Clause',
],
banlist: [
'Calyrex-Ice', 'Calyrex-Shadow', 'Cinderace', 'Dialga', 'Dragonite', 'Eternatus', 'Giratina', 'Giratina-Origin', 'Groudon', 'Ho-Oh', 'Kyogre', 'Kyurem-Black',
'Kyurem-White', 'Lugia', 'Lunala', 'Magearna', 'Marshadow', 'Melmetal', 'Mew', 'Mewtwo', 'Mimikyu', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia',
@ -236,7 +234,6 @@ export const Formats: FormatList = [
mod: 'gen8',
searchShow: false,
maxLevel: 5,
ruleset: ['[Gen 8] LC'],
banlist: [
// LC OU
@ -267,13 +264,7 @@ export const Formats: FormatList = [
],
mod: 'gen8',
forcedLevel: 50,
teamLength: {
validate: [3, 6],
battle: 3,
},
ruleset: ['Standard GBU'],
minSourceGen: 8,
ruleset: ['Flat Rules', '!! Adjust Level = 50', 'Min Source Gen = 8'],
},
{
name: "[Gen 8] Custom Game",
@ -281,14 +272,9 @@ export const Formats: FormatList = [
mod: 'gen8',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// Sw/Sh Doubles
@ -349,7 +335,6 @@ export const Formats: FormatList = [
mod: 'gen8',
gameType: 'doubles',
searchShow: false,
maxLevel: 5,
ruleset: ['Standard Doubles', 'Little Cup', 'Dynamax Clause', 'Swagger Clause', 'Sleep Clause Mod'],
banlist: ['Corsola-Galar', 'Cutiefly', 'Scyther', 'Sneasel', 'Swirlix', 'Tangela', 'Vulpix', 'Vulpix-Alola'],
},
@ -358,13 +343,7 @@ export const Formats: FormatList = [
mod: 'gen8',
gameType: 'doubles',
forcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Standard GBU', 'VGC Timer'],
minSourceGen: 8,
ruleset: ['Flat Rules', '!! Adjust Level = 50', 'Min Source Gen = 8', 'VGC Timer'],
},
{
name: "[Gen 8] VGC 2020",
@ -372,13 +351,7 @@ export const Formats: FormatList = [
mod: 'gen8dlc1',
gameType: 'doubles',
searchShow: false,
forcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Standard GBU', 'VGC Timer'],
minSourceGen: 8,
ruleset: ['Flat Rules', '!! Adjust Level = 50', 'Min Source Gen = 8', 'VGC Timer'],
},
{
name: "[Gen 8] 2v2 Doubles",
@ -389,11 +362,10 @@ export const Formats: FormatList = [
mod: 'gen8',
gameType: 'doubles',
teamLength: {
validate: [2, 4],
battle: 2,
},
ruleset: ['Standard Doubles', 'Accuracy Moves Clause', 'Dynamax Clause', 'Sleep Clause Mod'],
ruleset: [
'Picked Team Size = 2', 'Max Team Size = 4',
'Standard Doubles', 'Accuracy Moves Clause', 'Dynamax Clause', 'Sleep Clause Mod',
],
banlist: [
'Calyrex-Ice', 'Calyrex-Shadow', 'Dialga', 'Eternatus', 'Giratina', 'Giratina-Origin', 'Groudon', 'Ho-Oh', 'Jirachi', 'Kyogre',
'Kyurem-White', 'Lugia', 'Lunala', 'Magearna', 'Marshadow', 'Melmetal', 'Mewtwo', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia',
@ -409,11 +381,10 @@ export const Formats: FormatList = [
mod: 'gen8',
gameType: 'doubles',
teamLength: {
validate: [2, 2],
battle: 2,
},
ruleset: ['HP Percentage Mod', 'Cancel Mod'],
ruleset: [
'Max Team Size = 2',
'HP Percentage Mod', 'Cancel Mod',
],
banlist: [
'Pokestar Spirit', 'Shedinja + Sturdy', 'Battle Bond', 'Cheek Pouch', 'Cursed Body', 'Dry Skin', 'Fluffy', 'Fur Coat', 'Gorilla Tactics',
'Grassy Surge', 'Huge Power', 'Ice Body', 'Iron Barbs', 'Libero', 'Moody', 'Neutralizing Gas', 'Parental Bond', 'Perish Body', 'Poison Heal',
@ -450,15 +421,10 @@ export const Formats: FormatList = [
mod: 'gen8',
gameType: 'doubles',
searchShow: false,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
debug: true,
teamLength: {
validate: [2, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// National Dex
@ -1341,12 +1307,8 @@ export const Formats: FormatList = [
],
mod: 'gen8',
forcedLevel: 100,
teamLength: {
validate: [6, 6],
},
searchShow: false,
ruleset: ['Standard', '!OHKO Clause'],
ruleset: ['Standard', '!OHKO Clause', 'Picked Team Size = 6', 'Adjust Level = 100'],
banlist: [
'Sandshrew-Alola', 'Shedinja', 'Infiltrator', 'Magic Guard', 'Choice Scarf',
'Explosion', 'Final Gambit', 'Healing Wish', 'Lunar Dance', 'Magic Room', 'Memento', 'Misty Explosion', 'Self-Destruct',
@ -1629,11 +1591,7 @@ export const Formats: FormatList = [
],
team: 'randomBSSFactory',
teamLength: {
validate: [3, 6],
battle: 3,
},
ruleset: ['Obtainable', 'Standard GBU'],
ruleset: ['Flat Rules'],
},
{
name: "[Gen 8] Super Staff Bros 4",
@ -1699,10 +1657,7 @@ export const Formats: FormatList = [
mod: 'gen8',
team: 'randomCC',
teamLength: {
battle: 1,
},
ruleset: ['[Gen 8] Challenge Cup', 'Team Preview', 'Dynamax Clause'],
ruleset: ['[Gen 8] Challenge Cup', 'Team Preview', 'Dynamax Clause', 'Picked Team Size = 1'],
},
{
name: "[Gen 8] Challenge Cup 2v2",
@ -1710,11 +1665,8 @@ export const Formats: FormatList = [
mod: 'gen8',
team: 'randomCC',
gameType: 'doubles',
teamLength: {
battle: 2,
},
searchShow: false,
ruleset: ['[Gen 8] Challenge Cup 1v1'],
ruleset: ['[Gen 8] Challenge Cup 1v1', '!! Picked Team Size = 2'],
},
{
name: "[Gen 8] Hackmons Cup",
@ -1742,10 +1694,10 @@ export const Formats: FormatList = [
mod: 'gen8',
team: 'randomCAP1v1',
teamLength: {
battle: 1,
},
ruleset: ['Species Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod', 'Dynamax Clause'],
ruleset: [
'Picked Team Size = 1',
'Species Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod', 'Dynamax Clause',
],
},
{
name: "[Gen 7] Random Battle",
@ -1787,11 +1739,7 @@ export const Formats: FormatList = [
mod: 'gen7',
team: 'randomBSSFactory',
searchShow: false,
teamLength: {
validate: [3, 6],
battle: 3,
},
ruleset: ['Obtainable', 'Standard GBU'],
ruleset: ['Flat Rules'],
},
{
name: "[Gen 7] Hackmons Cup",
@ -1918,7 +1866,6 @@ export const Formats: FormatList = [
mod: 'gen4',
// searchShow: false,
maxLevel: 5,
ruleset: ['Standard', 'Little Cup', 'Sleep Moves Clause'],
banlist: [
'Meditite', 'Misdreavus', 'Murkrow', 'Scyther', 'Sneasel', 'Tangela', 'Yanma',
@ -2232,7 +2179,6 @@ export const Formats: FormatList = [
mod: 'gen7',
searchShow: false,
maxLevel: 5,
ruleset: ['Little Cup', 'Standard', 'Swagger Clause'],
banlist: [
'Aipom', 'Cutiefly', 'Drifloon', 'Gligar', 'Gothita', 'Meditite', 'Misdreavus', 'Murkrow', 'Porygon',
@ -2249,11 +2195,10 @@ export const Formats: FormatList = [
mod: 'gen7',
searchShow: false,
teamLength: {
validate: [1, 3],
battle: 1,
},
ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Swagger Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Endless Battle Clause'],
ruleset: [
'Picked Team Size = 1', 'Max Team Size = 3',
'Obtainable', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Swagger Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Endless Battle Clause',
],
banlist: [
'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Giratina-Origin', 'Groudon', 'Ho-Oh', 'Kangaskhan-Mega',
'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', 'Mimikyu', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane',
@ -2313,13 +2258,8 @@ export const Formats: FormatList = [
mod: 'gen7',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [3, 6],
battle: 3,
},
ruleset: ['Standard GBU'],
minSourceGen: 6,
ruleset: ['Flat Rules', 'Min Source Gen = 6'],
banlist: ['Battle Bond'],
},
{
name: "[Gen 7 Let's Go] OU",
@ -2330,8 +2270,7 @@ export const Formats: FormatList = [
mod: 'letsgo',
searchShow: false,
forcedLevel: 50,
ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod'],
ruleset: ['Adjust Level = 50', 'Obtainable', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Sleep Clause Mod'],
banlist: ['Uber'],
},
{
@ -2340,14 +2279,9 @@ export const Formats: FormatList = [
mod: 'gen7',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// US/UM Doubles
@ -2391,14 +2325,9 @@ export const Formats: FormatList = [
mod: 'gen7',
gameType: 'doubles',
searchShow: false,
forcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Minimal GBU', 'VGC Timer'],
banlist: ['Unown'],
minSourceGen: 7,
ruleset: ['Flat Rules', '!! Adjust Level = 50', 'Min Source Gen = 7', 'VGC Timer', 'Limit Two Restricted'],
restricted: ['Restricted Legendary'],
banlist: ['Unown', 'Battle Bond'],
},
{
name: "[Gen 7] VGC 2018",
@ -2411,11 +2340,6 @@ export const Formats: FormatList = [
mod: 'gen7',
gameType: 'doubles',
searchShow: false,
forcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
timer: {
starting: 5 * 60,
addPerTurn: 0,
@ -2425,9 +2349,8 @@ export const Formats: FormatList = [
timeoutAutoChoose: true,
dcTimerBank: false,
},
ruleset: ['Standard GBU'],
banlist: ['Oranguru + Symbiosis', 'Passimian + Defiant', 'Unown', 'Custap Berry', 'Enigma Berry', 'Jaboca Berry', 'Micle Berry', 'Rowap Berry'],
minSourceGen: 7,
ruleset: ['Flat Rules', '!! Adjust Level = 50', 'Min Source Gen = 7'],
banlist: ['Oranguru + Symbiosis', 'Passimian + Defiant', 'Unown', 'Custap Berry', 'Enigma Berry', 'Jaboca Berry', 'Micle Berry', 'Rowap Berry', 'Battle Bond'],
},
{
name: "[Gen 7] VGC 2017",
@ -2440,11 +2363,6 @@ export const Formats: FormatList = [
mod: 'vgc17',
gameType: 'doubles',
searchShow: false,
forcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
timer: {
starting: 15 * 60,
addPerTurn: 0,
@ -2454,12 +2372,14 @@ export const Formats: FormatList = [
timeoutAutoChoose: true,
dcTimerBank: false,
},
ruleset: ['Obtainable', 'Alola Pokedex', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'],
ruleset: [
'Picked Team Size = 4', 'Min Source Gen = 7', 'Adjust Level = 50',
'Obtainable', 'Alola Pokedex', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod',
],
banlist: [
'Solgaleo', 'Lunala', 'Necrozma', 'Magearna', 'Marshadow', 'Zygarde', 'Mega',
'Custap Berry', 'Enigma Berry', 'Jaboca Berry', 'Micle Berry', 'Rowap Berry',
],
minSourceGen: 7,
},
{
name: "[Gen 7] Battle Spot Doubles",
@ -2472,13 +2392,8 @@ export const Formats: FormatList = [
mod: 'gen7',
gameType: 'doubles',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Standard GBU'],
minSourceGen: 6,
ruleset: ['Flat Rules', 'Min Source Gen = 6'],
banlist: ['Battle Bond'],
},
{
name: "[Gen 7] Doubles Custom Game",
@ -2486,15 +2401,10 @@ export const Formats: FormatList = [
mod: 'gen7',
gameType: 'doubles',
searchShow: false,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
debug: true,
teamLength: {
validate: [2, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// OR/AS Singles
@ -2559,7 +2469,6 @@ export const Formats: FormatList = [
mod: 'gen6',
searchShow: false,
maxLevel: 5,
ruleset: ['Standard', 'Little Cup'],
banlist: [
'Drifloon', 'Gligar', 'Meditite', 'Misdreavus', 'Murkrow', 'Scyther', 'Sneasel', 'Swirlix', 'Tangela', 'Yanma',
@ -2593,11 +2502,10 @@ export const Formats: FormatList = [
mod: 'gen6',
searchShow: false,
teamLength: {
validate: [1, 3],
battle: 1,
},
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'],
ruleset: [
'Max Team Size = 3', 'Picked Team Size = 1',
'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: [
'Arceus', 'Blaziken', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina',
'Giratina-Origin', 'Groudon', 'Ho-Oh', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-White', 'Lugia', 'Mewtwo',
@ -2638,13 +2546,8 @@ export const Formats: FormatList = [
mod: 'gen6',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [3, 6],
battle: 3,
},
ruleset: ['Obtainable', 'Standard GBU'],
minSourceGen: 6,
ruleset: ['Flat Rules', 'Min Source Gen = 6'],
banlist: ['Soul Dew'],
},
{
name: "[Gen 6] Custom Game",
@ -2652,14 +2555,9 @@ export const Formats: FormatList = [
mod: 'gen6',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// OR/AS Doubles/Triples
@ -2693,17 +2591,13 @@ export const Formats: FormatList = [
mod: 'gen6',
gameType: 'doubles',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'],
banlist: [
'Mew', 'Celebi', 'Jirachi', 'Deoxys', 'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Arceus',
'Victini', 'Keldeo', 'Meloetta', 'Genesect', 'Diancie', 'Hoopa', 'Volcanion', 'Soul Dew',
ruleset: [
'Picked Team Size = 4', 'Min Source Gen = 6', 'Adjust Level Down = 50',
'Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod',
],
banlist: [
'Mythical', 'Soul Dew',
],
minSourceGen: 6,
onValidateTeam(team) {
const legends = [
'Mewtwo', 'Lugia', 'Ho-Oh', 'Kyogre', 'Groudon', 'Rayquaza', 'Dialga', 'Palkia', 'Giratina', 'Reshiram', 'Zekrom', 'Kyurem', 'Xerneas', 'Yveltal', 'Zygarde',
@ -2726,13 +2620,8 @@ export const Formats: FormatList = [
mod: 'gen6',
gameType: 'doubles',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Standard GBU'],
minSourceGen: 6,
ruleset: ['Flat Rules', 'Min Source Gen = 6'],
banlist: ['Soul Dew'],
},
{
name: "[Gen 6] Doubles Custom Game",
@ -2740,15 +2629,10 @@ export const Formats: FormatList = [
mod: 'gen6',
gameType: 'doubles',
searchShow: false,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
debug: true,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
{
name: "[Gen 6] Battle Spot Triples",
@ -2760,12 +2644,7 @@ export const Formats: FormatList = [
mod: 'gen6',
gameType: 'triples',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [6, 6],
},
ruleset: ['Standard GBU'],
minSourceGen: 6,
ruleset: ['Flat Rules', 'Min Source Gen = 6'],
},
{
name: "[Gen 6] Triples Custom Game",
@ -2773,15 +2652,10 @@ export const Formats: FormatList = [
mod: 'gen6',
gameType: 'triples',
searchShow: false,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
debug: true,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// B2/W2 Singles
@ -2857,7 +2731,6 @@ export const Formats: FormatList = [
mod: 'gen5',
searchShow: false,
maxLevel: 5,
ruleset: ['Standard', 'Little Cup', 'Sleep Moves Clause'],
banlist: [
'Gligar', 'Meditite', 'Misdreavus', 'Murkrow', 'Scraggy', 'Scyther', 'Sneasel', 'Tangela', 'Vulpix', 'Yanma',
@ -2884,11 +2757,10 @@ export const Formats: FormatList = [
mod: 'gen5',
searchShow: false,
teamLength: {
validate: [1, 3],
battle: 1,
},
ruleset: ['Standard', 'Baton Pass Clause', 'Swagger Clause'],
ruleset: [
'Picked Team Size = 1', 'Max Team Size = 3',
'Standard', 'Baton Pass Clause', 'Swagger Clause',
],
banlist: ['Uber', 'Cottonee', 'Dragonite', 'Kyurem-Black', 'Whimsicott', 'Victini', 'Bright Powder', 'Focus Band', 'Focus Sash', 'Lax Incense', 'Quick Claw', 'Soul Dew', 'Perish Song'],
unbanlist: ['Genesect', 'Landorus', 'Manaphy', 'Thundurus', 'Tornadus-Therian'],
},
@ -2897,13 +2769,8 @@ export const Formats: FormatList = [
mod: 'gen5',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [3, 6],
battle: 3,
},
ruleset: ['Standard GBU'],
banlist: ['Dark Void', 'Sky Drop'],
ruleset: ['Flat Rules'],
banlist: ['Dark Void', 'Sky Drop', 'Soul Dew'],
},
{
name: "[Gen 5] Custom Game",
@ -2911,14 +2778,9 @@ export const Formats: FormatList = [
mod: 'gen5',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// B2/W2 Doubles
@ -2948,13 +2810,8 @@ export const Formats: FormatList = [
mod: 'gen5',
gameType: 'doubles',
searchShow: false,
maxForcedLevel: 50,
teamLength: {
validate: [4, 6],
battle: 4,
},
ruleset: ['Standard GBU'],
banlist: ['Dark Void', 'Sky Drop'],
ruleset: ['Flat Rules'],
banlist: ['Dark Void', 'Sky Drop', 'Soul Dew'],
},
{
name: "[Gen 5] Doubles Custom Game",
@ -2963,14 +2820,9 @@ export const Formats: FormatList = [
gameType: 'doubles',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
ruleset: ['Team Preview', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
{
name: "[Gen 5] Triples Custom Game",
@ -2979,9 +2831,7 @@ export const Formats: FormatList = [
gameType: 'triples',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
// no restrictions, for serious (other than team preview)
ruleset: ['Team Preview', 'Cancel Mod'],
},
@ -3055,11 +2905,10 @@ export const Formats: FormatList = [
mod: 'gen4',
searchShow: false,
teamLength: {
validate: [1, 3],
battle: 1,
},
ruleset: ['[Gen 4] OU', 'Accuracy Moves Clause', 'Sleep Moves Clause', 'Team Preview'],
ruleset: [
'Picked Team Size = 1', 'Max Team Size = 3',
'[Gen 4] OU', 'Accuracy Moves Clause', 'Sleep Moves Clause', 'Team Preview',
],
banlist: ['Latias', 'Porygon-Z', 'Snorlax', 'Focus Sash', 'Destiny Bond', 'Explosion', 'Perish Song', 'Self-Destruct'],
unbanlist: ['Wobbuffet', 'Wynaut', 'Sand Veil'],
},
@ -3076,14 +2925,9 @@ export const Formats: FormatList = [
mod: 'gen4',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions
ruleset: ['Cancel Mod'],
ruleset: ['Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// DPP Doubles
@ -3111,14 +2955,9 @@ export const Formats: FormatList = [
gameType: 'doubles',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
// no restrictions
ruleset: ['Cancel Mod'],
ruleset: ['Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
// Past Generations
@ -3173,11 +3012,10 @@ export const Formats: FormatList = [
mod: 'gen3',
searchShow: false,
teamLength: {
validate: [1, 3],
battle: 1,
},
ruleset: ['[Gen 3] OU', 'Accuracy Moves Clause', 'Sleep Moves Clause', 'Team Preview'],
ruleset: [
'Picked Team Size = 1', 'Max Team Size = 3',
'[Gen 3] OU', 'Accuracy Moves Clause', 'Sleep Moves Clause', 'Team Preview',
],
banlist: ['Slaking', 'Snorlax', 'Suicune', 'Destiny Bond', 'Explosion', 'Ingrain', 'Perish Song', 'Self-Destruct'],
},
{
@ -3186,13 +3024,8 @@ export const Formats: FormatList = [
mod: 'gen3',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
ruleset: ['HP Percentage Mod', 'Cancel Mod'],
ruleset: ['HP Percentage Mod', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
{
name: "[Gen 3] Doubles Custom Game",
@ -3201,10 +3034,7 @@ export const Formats: FormatList = [
gameType: 'doubles',
searchShow: false,
debug: true,
teamLength: {
validate: [1, 24],
},
ruleset: ['HP Percentage Mod', 'Cancel Mod'],
ruleset: ['HP Percentage Mod', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
{
name: "[Gen 2] Ubers",
@ -3242,16 +3072,11 @@ export const Formats: FormatList = [
],
mod: 'gen2',
cupLevelLimit: {
range: [50, 55],
total: 155,
},
teamLength: {
validate: [3, 6],
battle: 3,
},
searchShow: false,
ruleset: ['Obtainable', 'Stadium Sleep Clause', 'Freeze Clause Mod', 'Species Clause', 'Item Clause', 'Endless Battle Clause', 'Cancel Mod', 'Event Moves Clause', 'Nickname Clause', 'Team Preview', 'Cup Level Limit', 'Nintendo Cup 2000 Move Legality'],
ruleset: [
'Picked Team Size = 3', 'Min Level = 50', 'Max Level = 55', 'Max Total Level = 155',
'Obtainable', 'Stadium Sleep Clause', 'Freeze Clause Mod', 'Species Clause', 'Item Clause', 'Endless Battle Clause', 'Cancel Mod', 'Event Moves Clause', 'Nickname Clause', 'Team Preview', 'Nintendo Cup 2000 Move Legality',
],
banlist: ['Uber'],
},
{
@ -3271,13 +3096,8 @@ export const Formats: FormatList = [
mod: 'gen2',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
ruleset: ['HP Percentage Mod', 'Cancel Mod'],
ruleset: ['HP Percentage Mod', 'Cancel Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
{
name: "[Gen 1] Ubers",
@ -3340,16 +3160,11 @@ export const Formats: FormatList = [
],
mod: 'gen1jpn',
cupLevelLimit: {
range: [50, 55],
total: 155,
},
teamLength: {
validate: [3, 6],
battle: 3,
},
searchShow: false,
ruleset: ['Obtainable', 'Team Preview', 'Cup Level Limit', 'Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'HP Percentage Mod', 'Cancel Mod', 'Nintendo Cup 1997 Move Legality'],
ruleset: [
'Picked Team Size = 3', 'Min Level = 50', 'Max Level = 55', 'Max Total Level = 155',
'Obtainable', 'Team Preview', 'Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'HP Percentage Mod', 'Cancel Mod', 'Nintendo Cup 1997 Move Legality',
],
banlist: ['Uber'],
},
{
@ -3369,12 +3184,7 @@ export const Formats: FormatList = [
mod: 'gen1',
searchShow: false,
debug: true,
maxLevel: 9999,
battle: {trunc: Math.trunc},
defaultLevel: 100,
teamLength: {
validate: [1, 24],
},
ruleset: ['HP Percentage Mod', 'Cancel Mod', 'Desync Clause Mod'],
ruleset: ['HP Percentage Mod', 'Cancel Mod', 'Desync Clause Mod', 'Max Team Size = 24', 'Max Level = 9999', 'Default Level = 100'],
},
];

View File

@ -2,12 +2,10 @@ export const Rulesets: {[k: string]: ModdedFormatData} = {
standard: {
inherit: true,
ruleset: ['Obtainable', 'Team Preview', 'Sleep Clause Mod', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Moves Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'],
minSourceGen: 0, // auto
},
standarddoubles: {
inherit: true,
ruleset: ['Obtainable', 'Team Preview', 'Species Clause', 'Nickname Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Abilities Clause', 'Evasion Moves Clause', 'Gravity Sleep Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod'],
minSourceGen: 0, // auto
},
obtainablemoves: {
inherit: true,

View File

@ -63,7 +63,7 @@ export class RandomTeams {
factoryTier: string;
format: Format;
prng: PRNG;
maxLength?: number;
maxLength: number;
/**
* Checkers for move enforcement based on a Pokémon's types or other factors
@ -76,13 +76,8 @@ export class RandomTeams {
format = Dex.formats.get(format);
this.dex = Dex.forFormat(format);
this.gen = this.dex.gen;
if (format.teamLength) {
if (format.teamLength.validate && format.teamLength.validate[1]) {
this.maxLength = format.teamLength.validate[1];
} else {
this.maxLength = format.teamLength.battle;
}
}
const ruleTable = Dex.formats.getRuleTable(format);
this.maxLength = ruleTable.maxTeamSize;
this.factoryTier = '';
this.format = format;
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);

View File

@ -22,102 +22,40 @@ export const Rulesets: {[k: string]: FormatData} = {
],
banlist: ['Soul Dew'],
},
standardgbu: {
flatrules: {
effectType: 'ValidatorRule',
name: 'Standard GBU',
desc: "The standard ruleset for all official in-game Pokémon tournaments and Battle Spot",
ruleset: ['Obtainable', 'Team Preview', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Cancel Mod'],
banlist: ['Battle Bond',
'Mewtwo', 'Mew',
'Lugia', 'Ho-Oh', 'Celebi',
'Kyogre', 'Groudon', 'Rayquaza', 'Jirachi', 'Deoxys',
'Dialga', 'Palkia', 'Giratina', 'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Arceus',
'Victini', 'Reshiram', 'Zekrom', 'Kyurem', 'Keldeo', 'Meloetta', 'Genesect',
'Xerneas', 'Yveltal', 'Zygarde', 'Diancie', 'Hoopa', 'Volcanion',
'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', 'Necrozma', 'Magearna', 'Marshadow', 'Zeraora',
'Meltan', 'Melmetal', 'Zacian', 'Zamazenta', 'Eternatus', 'Zarude', 'Calyrex',
],
onValidateSet(set, format) {
if (this.gen < 7 && this.toID(set.item) === 'souldew') {
return [`${set.name || set.species} has Soul Dew, which is banned in ${format.name}.`];
name: 'Flat Rules',
desc: "The in-game Flat Rules: Adjust Level Down 50, Species Clause, Item Clause, -Mythical, -Restricted Legendary, Bring 6 Pick 3-6 depending on game type.",
ruleset: ['Obtainable', 'Team Preview', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Adjust Level Down = 50', 'Picked Team Size = Flat Rules Team Size'],
banlist: ['Mythical', 'Restricted Legendary'],
},
limittworestricted: {
effectType: 'ValidatorRule',
name: 'Limit Two Restricted',
desc: "Limit two restricted Pokémon (flagged with * in the rules list)",
onValidateTeam(team) {
const restrictedSpecies = [];
for (const set of team) {
const species = this.dex.species.get(set.species);
if (this.ruleTable.isRestrictedSpecies(species)) restrictedSpecies.push(species.name);
}
if (restrictedSpecies.length > 2) {
return [`You can only use up to two restricted Pok\u00E9mon (you have: ${restrictedSpecies.join(', ')})`];
}
},
},
minimalgbu: {
limitonerestricted: {
effectType: 'ValidatorRule',
name: 'Minimal GBU',
desc: "The standard ruleset for official tournaments, but two Restricted Legendaries are allowed",
ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'],
banlist: ['Battle Bond',
'Mew',
'Celebi',
'Jirachi', 'Deoxys',
'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Arceus',
'Victini', 'Keldeo', 'Meloetta', 'Genesect',
'Diancie', 'Hoopa', 'Volcanion',
'Magearna', 'Marshadow', 'Zeraora',
'Meltan', 'Melmetal', 'Zarude',
],
restricted: [
'Mewtwo',
'Lugia', 'Ho-Oh',
'Kyogre', 'Groudon', 'Rayquaza',
'Dialga', 'Palkia', 'Giratina',
'Reshiram', 'Zekrom', 'Kyurem',
'Xerneas', 'Yveltal', 'Zygarde',
'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', 'Necrozma',
'Zacian', 'Zamazenta', 'Eternatus', 'Calyrex',
],
onValidateSet(set, format) {
if (this.gen < 7 && this.toID(set.item) === 'souldew') {
return [`${set.name || set.species} has Soul Dew, which is banned in ${format.name}.`];
}
},
name: 'Limit One Restricted',
desc: "Limit one restricted Pokémon (flagged with * in the rules list)",
onValidateTeam(team) {
let n = 0;
const restrictedSpecies = [];
for (const set of team) {
const species = this.dex.species.get(set.species);
if (this.ruleTable.isRestrictedSpecies(species)) n++;
if (n > 2) return [`You can only use up to two restricted legendary Pok\u00E9mon.`];
if (this.ruleTable.isRestrictedSpecies(species)) restrictedSpecies.push(species.name);
}
},
},
singlerestrictedgbu: {
effectType: 'ValidatorRule',
name: 'Single Restricted GBU',
desc: "The standard ruleset for official tournaments, but one Restricted Legendary is allowed",
ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause', 'Team Preview', 'Cancel Mod'],
banlist: ['Battle Bond',
'Mew',
'Celebi',
'Jirachi', 'Deoxys',
'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Arceus',
'Victini', 'Keldeo', 'Meloetta', 'Genesect',
'Diancie', 'Hoopa', 'Volcanion',
'Magearna', 'Marshadow', 'Zeraora',
'Meltan', 'Melmetal', 'Zarude',
],
restricted: [
'Mewtwo',
'Lugia', 'Ho-Oh',
'Kyogre', 'Groudon', 'Rayquaza',
'Dialga', 'Palkia', 'Giratina',
'Reshiram', 'Zekrom', 'Kyurem',
'Xerneas', 'Yveltal', 'Zygarde',
'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', 'Necrozma',
'Zacian', 'Zamazenta', 'Eternatus', 'Calyrex',
],
onValidateSet(set, format) {
if (this.gen < 7 && this.toID(set.item) === 'souldew') {
return [`${set.name || set.species} has Soul Dew, which is banned in ${format.name}.`];
}
},
onValidateTeam(team) {
let n = 0;
for (const set of team) {
const species = this.dex.species.get(set.species);
if (this.ruleTable.isRestrictedSpecies(species)) n++;
if (n > 1) return [`You can only use up to one restricted legendary Pok\u00E9mon.`];
if (restrictedSpecies.length > 1) {
return [`You can only use one restricted Pok\u00E9mon (you have: ${restrictedSpecies.join(', ')})`];
}
},
},
@ -394,6 +332,61 @@ export const Rulesets: {[k: string]: FormatData} = {
}
},
},
forcemonotype: {
effectType: 'ValidatorRule',
name: 'Force Monotype',
hasValue: true,
onValidateRule(value) {
if (!this.dex.types.get(value).exists) throw new Error(`Misspelled type "${value}"`);
if (!this.dex.types.isName(value)) throw new Error(`Incorrectly capitalized type "${value}"`);
},
onValidateSet(set) {
const species = this.dex.species.get(set.species);
const type = this.ruleTable.valueRules.get('forcemonotype')!;
if (!species.types.includes(type)) {
return [`${set.species} must have type ${type}`];
}
},
},
evlimits: {
effectType: 'ValidatorRule',
name: 'EV Limits',
desc: "Require EVs to be in specific ranges, such as: \"EV Limits = Atk 0-124 / Def 100-252\"",
hasValue: true,
onValidateRule(value) {
if (!value) throw new Error(`To remove EV limits, use "! EV Limits"`);
const slashedParts = value.split('/');
const UINT_REGEX = /^[0-9]{1,4}$/;
return slashedParts.map(slashedPart => {
const parts = slashedPart.replace('-', ' - ').replace(/ +/g, ' ').trim().split(' ');
const [stat, low, hyphen, high] = parts;
if (parts.length !== 4 || !UINT_REGEX.test(low) || hyphen !== '-' || !UINT_REGEX.test(high)) {
throw new Error(`EV limits should be in the format "EV Limits = Atk 0-124 / Def 100-252"`);
}
const statid = toID(stat) as StatID;
if (!this.dex.stats.ids().includes(statid)) {
throw new Error(`Unrecognized stat name "${stat}" in "${value}"`);
}
return `${statid} ${low}-${high}`;
}).join(' / ');
},
onValidateSet(set) {
const limits = this.ruleTable.valueRules.get('evlimits')!;
const problems = [];
for (const limit of limits.split(' / ')) {
const [statid, range] = limit.split(' ') as [StatID, string];
const [low, high] = range.split('-').map(num => parseInt(num));
const ev = set.evs[statid];
if (ev < low || ev > high) {
problems.push(`${set.name || set.species}'s ${this.dex.stats.names[statid]} EV (${ev}) must be ${low}-${high}`);
}
}
return problems;
},
},
teampreview: {
effectType: 'Rule',
name: 'Team Preview',
@ -414,32 +407,19 @@ export const Rulesets: {[k: string]: FormatData} = {
effectType: 'Rule',
name: 'One vs One',
desc: "Only allows one Pok&eacute;mon in battle",
onValidateTeam(team, format) {
if (format.gameType !== 'singles') {
return [`One vs One is for singles formats.`, `(Use Two vs Two in doubles)`];
}
},
onFieldStart() {
if (this.format.gameType === 'singles') (this.format as any).teamLength = {battle: 1};
},
ruleset: ['Picked Team Size = 1'],
},
twovstwo: {
effectType: 'Rule',
name: 'Two vs Two',
desc: "Only allows two Pok&eacute;mon in battle",
onValidateTeam(team, format) {
if (format.gameType === 'triples') {
return [`Two vs Two is for non-triples formats.`];
}
},
onFieldStart() {
if (this.format.gameType !== 'triples') (this.format as any).teamLength = {battle: 2};
},
ruleset: ['Picked Team Size = 2'],
},
littlecup: {
effectType: 'ValidatorRule',
name: 'Little Cup',
desc: "Only allows Pok&eacute;mon that can evolve and don't have any prior evolutions",
ruleset: ['Max Level = 5'],
onValidateSet(set) {
const species = this.dex.species.get(set.species || set.name);
if (species.prevo && this.dex.species.get(species.prevo).gen <= this.gen) {
@ -448,10 +428,6 @@ export const Rulesets: {[k: string]: FormatData} = {
if (!species.nfe) {
return [set.species + " doesn't have an evolution family."];
}
// Temporary hack for LC past-gen formats and other mashups
if (set.level > 5) {
return [`${set.species} can't be above level 5 in Little Cup formats.`];
}
},
},
blitz: {
@ -1042,6 +1018,21 @@ export const Rulesets: {[k: string]: FormatData} = {
return -typeMod;
},
},
minsourcegen: {
effectType: 'ValidatorRule',
name: "Min Source Gen",
desc: "Pokemon must be obtained from this generation or later.",
hasValue: 'positive-integer',
onValidateRule(value) {
const minSourceGen = parseInt(value);
if (minSourceGen > this.dex.gen) {
// console.log(this.ruleTable);
throw new Error(`Invalid generation ${minSourceGen}${this.ruleTable.blame('minsourcegen')} for a Gen ${this.dex.gen} format`);
}
},
},
stabmonsmovelegality: {
effectType: 'ValidatorRule',
name: 'STABmons Move Legality',
@ -1303,41 +1294,120 @@ export const Rulesets: {[k: string]: FormatData} = {
return problems;
},
},
cuplevellimit: {
pickedteamsize: {
effectType: 'Rule',
name: 'Picked Team Size',
desc: "Team size that can be brought out of Team Preview",
hasValue: 'positive-integer',
// hardcoded in sim/side
onValidateRule() {
if (!this.ruleTable.has('teampreview')) {
throw new Error(`The "Picked Team Size" rule${this.ruleTable.blame('pickedteamsize')} requires Team Preview.`);
}
},
},
minteamsize: {
effectType: 'ValidatorRule',
name: 'Cup Level Limit',
name: "Min Team Size",
desc: "Minimum team size that can be brought into Team Preview (or into the battle, in formats without Team Preview)",
hasValue: 'positive-integer',
// hardcoded in sim/team-validator
},
maxteamsize: {
effectType: 'ValidatorRule',
name: "Max Team Size",
desc: "Maximum team size that can be brought into Team Preview (or into the battle, in formats without Team Preview)",
hasValue: 'positive-integer',
// hardcoded in sim/team-validator
},
maxtotallevel: {
effectType: 'Rule',
name: 'Max Total Level',
desc: "Teams are restricted to a total maximum Level limit and Pokemon are restricted to a set range of Levels",
onValidateTeam(team, format) {
if (!format.teamLength?.battle) return;
if (!format.cupLevelLimit) return;
hasValue: 'positive-integer',
onValidateTeam(team) {
const pickedTeamSize = this.ruleTable.pickedTeamSize || team.length;
const maxTotalLevel = this.ruleTable.maxTotalLevel;
if (maxTotalLevel === null) throw new Error("No maxTotalLevel specified.");
const teamLevels = [];
for (const set of team) {
teamLevels.push(set.level);
}
teamLevels.sort((a, b) => b - a);
let combinedLowestLevels = 0;
for (let i = 0; i < format.teamLength.battle; i++) {
combinedLowestLevels += teamLevels.pop()!;
teamLevels.sort((a, b) => a - b);
let totalLowestLevels = 0;
for (let i = 0; i < pickedTeamSize; i++) {
totalLowestLevels += teamLevels[i];
}
if (combinedLowestLevels > format.cupLevelLimit.total) {
if (totalLowestLevels > maxTotalLevel) {
const thePokemon = pickedTeamSize === team.length ?
`all ${team.length} Pokémon` : `the ${pickedTeamSize} lowest-leveled Pokémon`;
return [
`The combined levels of the ${format.teamLength.battle} lowest Leveled Pokemon of your team is ${combinedLowestLevels}, above the format's maximum combined level of ${format.cupLevelLimit.total}.`,
`The combined levels of ${thePokemon} of your team is ${totalLowestLevels}, above the format's total level limit of ${maxTotalLevel}${this.ruleTable.blame('maxtotallevel')}.`,
];
}
let minTotalWithHighestLevel = teamLevels[teamLevels.length - 1];
for (let i = 0; i < pickedTeamSize - 1; i++) {
minTotalWithHighestLevel += teamLevels[i];
}
if (minTotalWithHighestLevel > maxTotalLevel) {
return [
`Your highest level Pokémon is unusable, because there's no way to create a team with it whose total level is less than the format's total level limit of ${maxTotalLevel}${this.ruleTable.blame('maxtotallevel')}.`,
];
}
},
onValidateSet(set, format) {
if (!format.cupLevelLimit) return;
if (set.level < format.cupLevelLimit.range[0]) {
return [
`${set.name || set.species} is Level ${set.level}, below the format's minimum Level of ${format.cupLevelLimit.range[0]}.`,
];
onValidateRule(value) {
const ruleTable = this.ruleTable;
const maxTotalLevel = ruleTable.maxTotalLevel!;
const maxTeamSize = ruleTable.pickedTeamSize || ruleTable.maxTeamSize;
const maxTeamSizeBlame = ruleTable.pickedTeamSize ? ruleTable.blame('pickedteamsize') : ruleTable.blame('maxteamsize');
if (maxTotalLevel >= ruleTable.maxLevel * maxTeamSize) {
throw new Error(`A Max Total Level of ${maxTotalLevel}${ruleTable.blame('maxtotallevel')} is too high (and will have no effect) with ${maxTeamSize}${maxTeamSizeBlame} Pokémon at max level ${ruleTable.maxLevel}${ruleTable.blame('maxlevel')}`);
}
if (set.level > format.cupLevelLimit.range[1]) {
return [
`${set.name || set.species} is Level ${set.level}, above the format's maximum Level of ${format.cupLevelLimit.range[1]}.`,
];
if (maxTotalLevel <= ruleTable.minLevel * maxTeamSize) {
throw new Error(`A Max Total Level of ${maxTotalLevel}${ruleTable.blame('maxtotallevel')} is too low with ${maxTeamSize}${maxTeamSizeBlame} Pokémon at min level ${ruleTable.minLevel}${ruleTable.blame('minlevel')}`);
}
},
// hardcoded in sim/side
},
minlevel: {
effectType: 'ValidatorRule',
name: 'Min Level',
desc: "Minimum level of brought Pokémon",
hasValue: 'positive-integer',
// hardcoded in sim/team-validator
},
maxlevel: {
effectType: 'ValidatorRule',
name: 'Max Level',
desc: "Maximum level of brought Pokémon (if you're using both this and Adjust Level, this will control what level moves you have access to)",
hasValue: 'positive-integer',
// hardcoded in sim/team-validator
},
defaultlevel: {
effectType: 'ValidatorRule',
name: 'Default Level',
desc: "Default level of brought Pokémon (normally should be equal to Max Level, except Custom Games have a very high max level but still default to 100)",
hasValue: 'positive-integer',
// hardcoded in sim/team-validator
},
adjustlevel: {
effectType: 'ValidatorRule',
name: 'Adjust Level',
desc: "All Pokémon will be set to exactly this level (but unlike Max Level and Min Level, it will still be able to learn moves from above this level)",
hasValue: 'positive-integer',
mutuallyExclusiveWith: 'adjustleveldown',
// hardcoded in sim/team-validator
},
adjustleveldown: {
effectType: 'ValidatorRule',
name: 'Adjust Level Down',
desc: "Any Pokémon above this level will be set to this level (but unlike Max Level, it will still be able to learn moves from above this level)",
hasValue: 'positive-integer',
mutuallyExclusiveWith: 'adjustlevel',
// hardcoded in sim/team-validator
},
stadiumitemsclause: {
effectType: 'ValidatorRule',

View File

@ -1308,8 +1308,8 @@ export class GlobalRoomState {
if (format.searchShow) displayCode |= 2;
if (format.challengeShow) displayCode |= 4;
if (format.tournamentShow) displayCode |= 8;
const level = format.cupLevelLimit ? format.cupLevelLimit.range[0] :
format.maxLevel || format.maxForcedLevel || format.forcedLevel;
const ruleTable = Dex.formats.getRuleTable(format);
const level = ruleTable.adjustLevel || ruleTable.adjustLevelDown || ruleTable.maxLevel;
if (level === 50) displayCode |= 16;
this.formatList += ',' + displayCode.toString(16);
}

View File

@ -241,6 +241,7 @@ export class Battle {
const subFormat = this.dex.formats.get(rule);
if (subFormat.exists) {
const hasEventHandler = Object.keys(subFormat).some(
// skip event handlers that are handled elsewhere
val => val.startsWith('on') && !['onBegin', 'onValidateTeam', 'onChangeSet', 'onValidateSet'].includes(val)
);
if (hasEventHandler) this.field.addPseudoWeather(rule);
@ -1142,11 +1143,11 @@ export class Battle {
}
if (type === 'teampreview') {
// `chosenTeamSize = 6` means the format wants the user to select
// the entire team order, unlike `chosenTeamSize = undefined` which
// `pickedTeamSize = 6` means the format wants the user to select
// the entire team order, unlike `pickedTeamSize = undefined` which
// will only ask the user to select their lead(s).
const chosenTeamSize = this.format.teamLength?.battle;
this.add('teampreview' + (chosenTeamSize ? '|' + chosenTeamSize : ''));
const pickedTeamSize = this.ruleTable.pickedTeamSize;
this.add('teampreview' + (pickedTeamSize ? '|' + pickedTeamSize : ''));
}
const requests = this.getRequests(type);
@ -1186,7 +1187,7 @@ export class Battle {
case 'teampreview':
for (let i = 0; i < this.sides.length; i++) {
const side = this.sides[i];
const maxChosenTeamSize = this.format.teamLength?.battle;
const maxChosenTeamSize = this.ruleTable.pickedTeamSize || undefined;
requests[i] = {teamPreview: true, maxChosenTeamSize, side: side.getRequestData()};
}
break;

View File

@ -34,8 +34,19 @@ export class RuleTable extends Map<string, string> {
complexTeamBans: ComplexTeamBan[];
checkCanLearn: [TeamValidator['checkCanLearn'], string] | null;
timer: [Partial<GameTimerSettings>, string] | null;
minSourceGen: [number, string] | null;
tagRules: string[];
valueRules: Map<string, string>;
minTeamSize!: number;
maxTeamSize!: number;
pickedTeamSize!: number | null;
maxTotalLevel!: number | null;
minSourceGen!: number;
minLevel!: number;
maxLevel!: number;
defaultLevel!: number;
adjustLevel!: number | null;
adjustLevelDown!: number | null;
constructor() {
super();
@ -43,8 +54,8 @@ export class RuleTable extends Map<string, string> {
this.complexTeamBans = [];
this.checkCanLearn = null;
this.timer = null;
this.minSourceGen = null;
this.tagRules = [];
this.valueRules = new Map();
}
isBanned(thing: string) {
@ -60,7 +71,7 @@ export class RuleTable extends Map<string, string> {
for (const tagid in Tags) {
const tag = Tags[tagid];
if (this.has(`-pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return false;
if ((tag.speciesFilter || tag.genericFilter)!(species)) return true;
}
}
for (const tagid in Tags) {
@ -84,14 +95,14 @@ export class RuleTable extends Map<string, string> {
if (this.has(`*basepokemon:${toID(species.baseSpecies)}`)) return true;
for (const tagid in Tags) {
const tag = Tags[tagid];
if (tag.speciesFilter && this.has(`*pokemontag:${tagid}`)) {
if (tag.speciesFilter(species)) return false;
if (this.has(`*pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return true;
}
}
for (const tagid in Tags) {
const tag = Tags[tagid];
if (tag.speciesFilter && this.has(`+pokemontag:${tagid}`)) {
if (tag.speciesFilter(species)) return false;
if (this.has(`+pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return false;
}
}
return this.has(`*pokemontag:allpokemon`);
@ -138,6 +149,11 @@ export class RuleTable extends Map<string, string> {
return source ? `banned by ${source}` : `banned`;
}
blame(key: string): string {
const source = this.get(key);
return source ? ` from ${source}` : ``;
}
getComplexBanIndex(complexBans: ComplexBan[], rule: string): number {
const ruleId = toID(rule);
let complexBanIndex = -1;
@ -169,6 +185,86 @@ export class RuleTable extends Map<string, string> {
this.complexTeamBans.push([rule, source, limit, bans]);
}
}
/** After a RuleTable has been filled out, resolve its hardcoded numeric properties */
resolveNumbers(format: Format) {
const gameTypeMinTeamSize = ['triples', 'rotation'].includes(format.gameType as 'triples') ? 3 :
format.gameType === 'doubles' ? 2 :
1;
// NOTE: These numbers are pre-calculated here because they're hardcoded
// into the team validator and battle engine, and can affect validation
// in complicated ways.
// If you're making your own rule, it nearly definitely does not not
// belong here: `onValidateRule`, `onValidateSet`, and `onValidateTeam`
// should be enough for a validator rule, and the battle event system
// should be enough for a battle rule.
this.minTeamSize = Number(this.valueRules.get('minteamsize')) || 0;
this.maxTeamSize = Number(this.valueRules.get('maxteamsize')) || 6;
this.pickedTeamSize = Number(this.valueRules.get('pickedteamsize')) || null;
this.maxTotalLevel = Number(this.valueRules.get('maxtotallevel')) || null;
this.minSourceGen = Number(this.valueRules.get('minsourcegen')) || 1;
this.minLevel = Number(this.valueRules.get('minlevel')) || 1;
this.maxLevel = Number(this.valueRules.get('maxlevel')) || 100;
this.defaultLevel = Number(this.valueRules.get('defaultlevel')) || this.maxLevel;
this.adjustLevel = Number(this.valueRules.get('adjustlevel')) || null;
this.adjustLevelDown = Number(this.valueRules.get('adjustleveldown')) || null;
// sanity checks; these _could_ be inside `onValidateRule` but this way
// involves less string conversion.
if (this.minTeamSize && this.minTeamSize < gameTypeMinTeamSize) {
throw new Error(`Min team size ${this.minTeamSize}${this.blame('minteamsize')} must be at least ${gameTypeMinTeamSize} for a ${format.gameType} game.`);
}
if (this.pickedTeamSize && this.pickedTeamSize < gameTypeMinTeamSize) {
throw new Error(`Chosen team size ${this.pickedTeamSize}${this.blame('pickedteamsize')} must be at least ${gameTypeMinTeamSize} for a ${format.gameType} game.`);
}
if (this.minTeamSize && this.pickedTeamSize && this.minTeamSize < this.pickedTeamSize) {
throw new Error(`Min team size ${this.minTeamSize}${this.blame('minteamsize')} is lower than chosen team size ${this.pickedTeamSize}${this.blame('pickedteamsize')}.`);
}
if (!this.minTeamSize) this.minTeamSize = Math.max(gameTypeMinTeamSize, this.pickedTeamSize || 0);
if (this.maxTeamSize > 24) {
throw new Error(`Max team size ${this.maxTeamSize}${this.blame('maxteamsize')} can't be above 24.`);
}
if (this.minLevel > this.maxLevel) {
throw new Error(`Min level ${this.minLevel}${this.blame('minlevel')} should not be above max level ${this.maxLevel}${this.blame('maxlevel')}.`);
}
if (this.defaultLevel > this.maxLevel) {
throw new Error(`Default level ${this.defaultLevel}${this.blame('defaultlevel')} should not be above max level ${this.maxLevel}${this.blame('maxlevel')}.`);
}
if (this.defaultLevel < this.minLevel) {
throw new Error(`Default level ${this.defaultLevel}${this.blame('defaultlevel')} should not be below min level ${this.minLevel}${this.blame('minlevel')}.`);
}
if (this.adjustLevelDown && this.adjustLevelDown >= this.maxLevel) {
throw new Error(`Adjust Level Down ${this.adjustLevelDown}${this.blame('adjustleveldown')} will have no effect because it's not below max level ${this.maxLevel}${this.blame('maxlevel')}.`);
}
if (this.adjustLevel && this.valueRules.has('minlevel')) {
throw new Error(`Min Level ${this.minLevel}${this.blame('minlevel')} will have no effect because you're using Adjust Level ${this.adjustLevel}${this.blame('adjustlevel')}.`);
}
if ((format as any).cupLevelLimit) {
throw new Error(`cupLevelLimit.range[0], cupLevelLimit.range[1], cupLevelLimit.total are now rules, respectively: "Min Level = NUMBER", "Max Level = NUMBER", and "Max Total Level = NUMBER"`);
}
if ((format as any).teamLength) {
throw new Error(`teamLength.validate[0], teamLength.validate[1], teamLength.battle are now rules, respectively: "Min Team Size = NUMBER", "Max Team Size = NUMBER", and "Picked Team Size = NUMBER"`);
}
if ((format as any).minSourceGen) {
throw new Error(`minSourceGen is now a rule: "Min Source Gen = NUMBER"`);
}
if ((format as any).maxLevel) {
throw new Error(`maxLevel is now a rule: "Max Level = NUMBER"`);
}
if ((format as any).defaultLevel) {
throw new Error(`defaultLevel is now a rule: "Default Level = NUMBER"`);
}
if ((format as any).forcedLevel) {
throw new Error(`forcedLevel is now a rule: "Adjust Level = NUMBER"`);
}
if ((format as any).maxForcedLevel) {
throw new Error(`maxForcedLevel is now a rule: "Adjust Level Down = NUMBER"`);
}
}
}
export class Format extends BasicEffect implements Readonly<BasicEffect> {
@ -206,63 +302,20 @@ export class Format extends BasicEffect implements Readonly<BasicEffect> {
readonly customRules: string[] | null;
/** Table of rule names and banned effects. */
ruleTable: RuleTable | null;
/**
* The range of levels on Pokemon that players can bring to battle and
* the summative limit of those levels
*/
readonly cupLevelLimit?: {
/** Minimum and maximum level (redundant with maxLevel) */
range: [number, number],
/** Maximum total level that can be chosen in Team Preview */
total: number,
};
readonly teamLength?: {
/**
* Force this many pokemon to be chosen in Team Preview
* (simultaneously a minimum and a maximum.)
*/
battle?: number,
/**
* Require this many pokemon to be brought into Team Preview
* (or into the battle, for battles without Team Preview)
*/
validate?: [number, number],
};
/** An optional function that runs at the start of a battle. */
readonly onBegin?: (this: Battle) => void;
/** Pokemon must be obtained from this generation or later. */
readonly minSourceGen?: number;
/**
* Maximum possible level pokemon you can bring. Note that this is
* still 100 in VGC, because you can bring level 100 pokemon,
* they'll just be set to level 50. Can be above 100 in special
* formats.
*/
readonly maxLevel: number;
/**
* Default level of a pokemon without level specified. Mainly
* relevant to Custom Game where the default level is still 100
* even though higher level pokemon can be brought.
*/
readonly defaultLevel: number;
/**
* Forces all pokemon brought in to this level. Certain Game Freak
* formats will change level 1 and level 100 pokemon to level 50,
* which is what this does.
*
* You usually want maxForcedLevel instead, which will bring level
* 100 pokemon down, but not level 1 pokemon up.
*/
readonly forcedLevel?: number;
/**
* Forces all pokemon above this level down to this level. This
* will allow e.g. level 50 Hydreigon in Gen 5, which is not
* normally legal because Hydreigon doesn't evolve until level
* 64.
*/
readonly maxForcedLevel?: number;
readonly noLog: boolean;
/**
* Only applies to rules, not formats
*/
readonly hasValue?: boolean | 'integer' | 'positive-integer';
readonly onValidateRule?: (
this: {format: Format, ruleTable: RuleTable, dex: ModdedDex}, value: string
) => string | void;
/** ID of rule that can't be combined with this rule */
readonly mutuallyExclusiveWith?: string;
readonly battle?: ModdedBattleScriptsData;
readonly pokemon?: ModdedBattlePokemon;
readonly queue?: ModdedBattleQueue;
@ -318,14 +371,7 @@ export class Format extends BasicEffect implements Readonly<BasicEffect> {
this.unbanlist = data.unbanlist || [];
this.customRules = data.customRules || null;
this.ruleTable = null;
this.cupLevelLimit = data.cupLevelLimit || undefined;
this.teamLength = data.teamLength || undefined;
this.onBegin = data.onBegin || undefined;
this.minSourceGen = data.minSourceGen || undefined;
this.maxLevel = data.maxLevel || 100;
this.defaultLevel = data.defaultLevel || this.maxLevel;
this.forcedLevel = data.forcedLevel || undefined;
this.maxForcedLevel = data.maxForcedLevel || undefined;
this.noLog = !!data.noLog;
}
}
@ -552,14 +598,11 @@ export class DexFormats {
if (format.timer) {
ruleTable.timer = [format.timer, format.name];
}
if (format.minSourceGen) {
ruleTable.minSourceGen = [format.minSourceGen, format.name];
}
// apply rule repeals before other rules
// repeals is a ruleid:depth map
// repeals is a ruleid:depth map (positive: unused, negative: used)
for (const rule of ruleset) {
if (rule.startsWith('!')) {
if (rule.startsWith('!') && !rule.startsWith('!!')) {
const ruleSpec = this.validateRule(rule, format) as string;
if (!repeals) repeals = new Map();
repeals.set(ruleSpec.slice(1), depth);
@ -582,7 +625,7 @@ export class DexFormats {
continue;
}
if (rule.startsWith('!')) {
if (rule.startsWith('!') && !rule.startsWith('!!')) {
const repealDepth = repeals!.get(ruleSpec.slice(1));
if (repealDepth === undefined) throw new Error(`Multiple "${rule}" rules in ${format.name}`);
if (repealDepth === depth) {
@ -594,30 +637,96 @@ export class DexFormats {
if ('+*-'.includes(ruleSpec.charAt(0))) {
if (ruleTable.has(ruleSpec)) {
throw new Error(`Rule "${rule}" was added by "${format.name}" but already exists in "${ruleTable.get(ruleSpec) || format.name}"`);
throw new Error(`Rule "${rule}" in "${format.name}" already exists in "${ruleTable.get(ruleSpec) || format.name}"`);
}
for (const prefix of '+*-') ruleTable.delete(prefix + ruleSpec.slice(1));
ruleTable.set(ruleSpec, '');
continue;
}
const subformat = this.get(ruleSpec);
let [formatid, value] = ruleSpec.split('=');
const subformat = this.get(formatid);
const repealAndReplace = ruleSpec.startsWith('!!');
if (repeals?.has(subformat.id)) {
repeals.set(subformat.id, -Math.abs(repeals.get(subformat.id)!));
continue;
}
if (ruleTable.has(subformat.id)) {
throw new Error(`Rule "${rule}" was added by "${format.name}" but already exists in "${ruleTable.get(subformat.id) || format.name}"`);
if (subformat.hasValue) {
if (value === undefined) throw new Error(`Rule "${ruleSpec}" should have a value (like "${ruleSpec} = something")`);
if (value === 'Current Gen') value = `${this.dex.gen}`;
if (value === 'Flat Rules Team Size') {
value = String(
['doubles', 'rotation'].includes(format.gameType) ? 4 :
format.gameType === 'triples' ? 6 :
3
);
}
if (subformat.hasValue === 'integer' || subformat.hasValue === 'positive-integer') {
const intValue = parseInt(value);
if (isNaN(intValue) || value !== `${intValue}`) {
throw new Error(`In rule "${ruleSpec}", "${value}" must be an integer number.`);
}
}
if (subformat.hasValue === 'positive-integer') {
if (parseInt(value) <= 0) throw new Error(`In rule "${ruleSpec}", "${value}" must be positive.`);
}
const oldValue = ruleTable.valueRules.get(subformat.id);
if (oldValue === value) {
throw new Error(`Rule "${ruleSpec}" is redundant with existing rule "${subformat.id}=${value}"${ruleTable.blame(subformat.id)}.`);
} else if (repealAndReplace) {
if (oldValue === undefined) {
if (subformat.mutuallyExclusiveWith && ruleTable.valueRules.has(subformat.mutuallyExclusiveWith)) {
if (this.dex.formats.get(subformat.mutuallyExclusiveWith).ruleset.length) {
throw new Error(`This format does not support "!!"`);
}
ruleTable.valueRules.delete(subformat.mutuallyExclusiveWith);
ruleTable.delete(subformat.mutuallyExclusiveWith);
} else {
throw new Error(`Rule "${ruleSpec}" is not replacing anything (it should not have "!!")`);
}
}
} else {
if (oldValue !== undefined) {
throw new Error(`Rule "${ruleSpec}" conflicts with "${subformat.id}=${oldValue}"${ruleTable.blame(subformat.id)} (Use "!! ${ruleSpec}" to override "${subformat.id}=${oldValue}".)`);
}
if (subformat.mutuallyExclusiveWith && ruleTable.valueRules.has(subformat.mutuallyExclusiveWith)) {
const oldRule = `"${subformat.mutuallyExclusiveWith}=${ruleTable.valueRules.get(subformat.mutuallyExclusiveWith)}"`;
throw new Error(`Format can't simultaneously have "${ruleSpec}" and ${oldRule}${ruleTable.blame(subformat.mutuallyExclusiveWith)} (Use "!! ${ruleSpec}" to override ${oldRule}.)`);
}
}
ruleTable.valueRules.set(subformat.id, value);
} else {
if (value !== undefined) throw new Error(`Rule "${ruleSpec}" should not have a value (no equals sign)`);
if (repealAndReplace) throw new Error(`"!!" is not supported for this rule`);
if (ruleTable.has(subformat.id) && !repealAndReplace) {
throw new Error(`Rule "${rule}" in "${format.name}" already exists in "${ruleTable.get(subformat.id) || format.name}"`);
}
}
ruleTable.set(subformat.id, '');
if (!subformat.exists) continue;
if (depth > 16) {
throw new Error(`Excessive ruleTable recursion in ${format.name}: ${ruleSpec} of ${format.ruleset}`);
}
const subRuleTable = this.getRuleTable(subformat, depth + 1, repeals);
for (const [k, v] of subRuleTable) {
for (const [ruleid, sourceFormat] of subRuleTable) {
// don't check for "already exists" here; multiple inheritance is allowed
if (!repeals?.has(k)) {
ruleTable.set(k, v || subformat.name);
if (!repeals?.has(ruleid)) {
const newValue = subRuleTable.valueRules.get(ruleid);
const oldValue = ruleTable.valueRules.get(ruleid);
if (newValue !== undefined) {
// set a value
const subSubFormat = this.get(ruleid);
if (subSubFormat.mutuallyExclusiveWith && ruleTable.valueRules.has(subSubFormat.mutuallyExclusiveWith)) {
// mutually exclusive conflict!
throw new Error(`Rule "${ruleid}=${newValue}" from ${subformat.name}${subRuleTable.blame(ruleid)} conflicts with "${subSubFormat.mutuallyExclusiveWith}=${ruleTable.valueRules.get(subSubFormat.mutuallyExclusiveWith)}"${ruleTable.blame(subSubFormat.mutuallyExclusiveWith)} (Repeal one with ! before adding another)`);
}
if (newValue !== oldValue) {
if (oldValue !== undefined) {
// conflict!
throw new Error(`Rule "${ruleid}=${newValue}" from ${subformat.name}${subRuleTable.blame(ruleid)} conflicts with "${ruleid}=${oldValue}"${ruleTable.blame(ruleid)} (Repeal one with ! before adding another)`);
}
ruleTable.valueRules.set(ruleid, newValue);
}
}
ruleTable.set(ruleid, sourceFormat || subformat.name);
}
}
for (const [subRule, source, limit, bans] of subRuleTable.complexBans) {
@ -643,20 +752,20 @@ export class DexFormats {
}
ruleTable.timer = subRuleTable.timer;
}
// minSourceGen is automatically ignored if higher than current gen
// this helps the common situation where Standard has a minSourceGen in the
// latest gen but not in any past gens
if (subRuleTable.minSourceGen && subRuleTable.minSourceGen[0] <= this.dex.gen) {
if (ruleTable.minSourceGen) {
throw new Error(
`"${format.name}" has conflicting minSourceGen from "${ruleTable.minSourceGen[1]}" and "${subRuleTable.minSourceGen[1]}"`
);
}
ruleTable.minSourceGen = subRuleTable.minSourceGen;
}
}
ruleTable.getTagRules();
ruleTable.resolveNumbers(format);
for (const rule of ruleTable.keys()) {
if ("+*-!".includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
if (subFormat.exists) {
const value = subFormat.onValidateRule?.call({format, ruleTable, dex: this.dex}, ruleTable.valueRules.get(rule as ID)!);
if (typeof value === 'string') ruleTable.valueRules.set(subFormat.id, value);
}
}
if (!repeals) format.ruleTable = ruleTable;
return ruleTable;
}
@ -692,10 +801,14 @@ export class DexFormats {
}
return rule.charAt(0) + this.validateBanRule(rule.slice(1));
default:
const id = toID(rule);
if (!this.dex.data.Rulesets.hasOwnProperty(id)) {
const [ruleName, value] = rule.split('=');
let id: string = toID(ruleName);
const ruleset = this.dex.formats.get(id);
if (!ruleset.exists) {
throw new Error(`Unrecognized rule "${rule}"`);
}
if (typeof value === 'string') id = `${id}=${value.trim()}`;
if (rule.startsWith('!!')) return `!!${id}`;
if (rule.startsWith('!')) return `!${id}`;
return id;
}

View File

@ -295,7 +295,7 @@ export class Pokemon {
this.name = set.name.substr(0, 20);
this.fullname = this.side.id + ': ' + this.name;
set.level = this.battle.clampIntRange(set.forcedLevel || set.level || 100, 1, 9999);
set.level = this.battle.clampIntRange(set.adjustLevel || set.level || 100, 1, 9999);
this.level = set.level;
const genders: {[key: string]: GenderName} = {M: 'M', F: 'F', N: 'N'};
this.gender = genders[set.gender] || this.species.gender || (this.battle.random() * 2 < 1 ? 'M' : 'F');

View File

@ -388,7 +388,7 @@ export class Side {
if (this.choice.forcedSwitchesLeft) return false;
if (this.requestState === 'teampreview') {
return this.choice.actions.length >= this.chosenTeamSize();
return this.choice.actions.length >= this.pickedTeamSize();
}
// current request is move/switch
@ -735,8 +735,8 @@ export class Side {
* since that's nearly always a mistake, we haven't gotten around to
* supporting it.
*/
chosenTeamSize() {
return Math.min(this.pokemon.length, this.battle.format.teamLength?.battle || Infinity);
pickedTeamSize() {
return Math.min(this.pokemon.length, this.battle.ruleTable.pickedTeamSize || Infinity);
}
chooseTeam(data = '') {
@ -744,22 +744,22 @@ export class Side {
return this.emitChoiceError(`Can't choose for Team Preview: You're not in a Team Preview phase`);
}
const ruleTable = this.battle.ruleTable;
let positions = data.split(data.includes(',') ? ',' : '')
.map(datum => parseInt(datum) - 1);
const format = this.battle.format;
const chosenTeamSize = Math.min(this.pokemon.length, this.battle.format.teamLength?.battle || Infinity);
const pickedTeamSize = this.pickedTeamSize();
// make sure positions is exactly of length chosenTeamSize
// make sure positions is exactly of length pickedTeamSize
// - If too big: the client automatically sends a full list, so we just trim it down to size
positions.splice(chosenTeamSize);
positions.splice(pickedTeamSize);
// - If too small: we intentionally support only sending leads and having the sim fill in the rest
if (positions.length === 0) {
for (let i = 0; i < chosenTeamSize; i++) positions.push(i);
} else if (positions.length < chosenTeamSize) {
for (let i = 0; i < chosenTeamSize; i++) {
for (let i = 0; i < pickedTeamSize; i++) positions.push(i);
} else if (positions.length < pickedTeamSize) {
for (let i = 0; i < pickedTeamSize; i++) {
if (!positions.includes(i)) positions.push(i);
// duplicate in input, let the rest of the code handle the error message
if (positions.length >= chosenTeamSize) break;
if (positions.length >= pickedTeamSize) break;
}
}
@ -771,17 +771,17 @@ export class Side {
return this.emitChoiceError(`Can't choose for Team Preview: The Pokémon in slot ${pos + 1} can only switch in once`);
}
}
if (format.cupLevelLimit) {
if (ruleTable.maxTotalLevel) {
let totalLevel = 0;
for (const pos of positions) totalLevel += this.pokemon[pos].level;
if (totalLevel > format.cupLevelLimit.total) {
if (totalLevel > ruleTable.maxTotalLevel) {
if (!data) {
// autoChoose
positions = [...this.pokemon.keys()].sort((a, b) => (this.pokemon[a].level - this.pokemon[b].level))
.slice(0, chosenTeamSize);
.slice(0, pickedTeamSize);
} else {
return this.emitChoiceError(`Your selected team has a total level of ${totalLevel}, but it can't be above ${format.cupLevelLimit.total}; please select a valid team of ${chosenTeamSize} Pokémon`);
return this.emitChoiceError(`Your selected team has a total level of ${totalLevel}, but it can't be above ${ruleTable.maxTotalLevel}; please select a valid team of ${pickedTeamSize} Pokémon`);
}
}
}

View File

@ -199,8 +199,7 @@ export class TeamValidator {
this.gen = this.dex.gen;
this.ruleTable = this.dex.formats.getRuleTable(this.format);
this.minSourceGen = this.ruleTable.minSourceGen ?
this.ruleTable.minSourceGen[0] : 1;
this.minSourceGen = this.ruleTable.minSourceGen;
this.toID = toID;
}
@ -249,12 +248,12 @@ export class TeamValidator {
throw new Error(`Invalid team data`);
}
let [minSize, maxSize] = format.teamLength && format.teamLength.validate || [1, 6];
if (format.gameType === 'doubles' && minSize < 2) minSize = 2;
if (['triples', 'rotation'].includes(format.gameType as 'triples') && minSize < 3) minSize = 3;
if (team.length < minSize) problems.push(`You must bring at least ${minSize} Pok\u00E9mon.`);
if (team.length > maxSize) return [`You may only bring up to ${maxSize} Pok\u00E9mon.`];
if (team.length < ruleTable.minTeamSize) {
problems.push(`You must bring at least ${ruleTable.minTeamSize} Pok\u00E9mon.`);
}
if (team.length > ruleTable.maxTeamSize) {
return [`You may only bring up to ${ruleTable.maxTeamSize} Pok\u00E9mon.`];
}
// A limit is imposed here to prevent too much engine strain or
// too much layout deformation - to be exact, this is the limit
@ -380,34 +379,38 @@ export class TeamValidator {
set.nature = nature.name;
if (!Array.isArray(set.moves)) set.moves = [];
const maxLevel = format.maxLevel || 100;
const maxForcedLevel = format.maxForcedLevel || maxLevel;
let forcedLevel: number | null = null;
if (!set.level) {
set.level = (format.defaultLevel || maxLevel);
}
if (format.forcedLevel) {
forcedLevel = format.forcedLevel;
} else if (set.level >= maxForcedLevel) {
forcedLevel = maxForcedLevel;
}
if (set.level > maxLevel || set.level === forcedLevel || set.level === maxForcedLevel) {
// Note that we're temporarily setting level 50 pokemon in VGC to level 100
// This allows e.g. level 50 Hydreigon even though it doesn't evolve until level 64.
// Leveling up can't make an obtainable pokemon unobtainable, so this is safe.
// Just remember to set the level back to 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.');
}
set.name = set.name || species.baseSpecies;
let name = set.species;
if (set.species !== set.name && species.baseSpecies !== set.name) {
name = `${set.name} (${set.species})`;
}
const maxLevel = ruleTable.maxLevel;
const adjustLevelDown = ruleTable.adjustLevelDown || maxLevel;
let adjustLevel = ruleTable.adjustLevel;
if (!set.level) {
set.level = ruleTable.defaultLevel;
}
if (adjustLevelDown && set.level >= adjustLevelDown) {
adjustLevel = adjustLevelDown;
}
if (set.level === adjustLevel || set.level === adjustLevelDown || (set.level === 100 && maxLevel < 100)) {
// Note that we're temporarily setting level 50 pokemon in VGC to level 100
// This allows e.g. level 50 Hydreigon even though it doesn't evolve until level 64.
// Leveling up can't make an obtainable pokemon unobtainable, so this is safe.
// Just remember to set the level back to adjustLevel at the end of validation.
set.level = maxLevel;
}
if (set.level < ruleTable.minLevel) {
problems.push(`${name} (level ${set.level}) is below the minimum level of ${ruleTable.minLevel}${ruleTable.blame('minlevel')}`);
}
if (set.level > ruleTable.maxLevel) {
problems.push(`${name} (level ${set.level}) is above the maximum level of ${ruleTable.maxLevel}${ruleTable.blame('maxlevel')}`);
}
if ((set.level > 100 || set.level < 1) && ruleTable.isBanned('nonexistent')) {
problems.push(`${name} (level ${set.level}) is higher than level 100.`);
}
const setHas: {[k: string]: true} = {};
const allowEVs = dex.currentMod !== 'letsgo';
@ -756,7 +759,7 @@ export class TeamValidator {
}
if (!problems.length) {
if (forcedLevel) set.level = forcedLevel;
if (adjustLevel) set.level = adjustLevel;
return null;
}
@ -929,7 +932,7 @@ export class TeamValidator {
let totalEV = 0;
for (const stat in set.evs) totalEV += set.evs[stat as 'hp'];
// Not having this affect Nintendo Cup formats because it is annoying to deal with having to lower a base stat by 1 for every Pokemon.
if (!this.format.debug && !this.format.cupLevelLimit) {
if (!this.format.debug && !ruleTable.has('maxtotallevel')) {
if (set.level > 1 && (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)) {
@ -937,7 +940,7 @@ export class TeamValidator {
}
// Check for level import errors from user in VGC -> DOU, etc.
// Note that in VGC etc (maxForcedLevel: 50), `set.level` will be 100 here for validation purposes
if (set.level === 50 && this.format.maxLevel !== 50 && allowEVs && totalEV % 4 === 0) {
if (set.level === 50 && ruleTable.maxLevel !== 50 && allowEVs && totalEV % 4 === 0) {
problems.push(`${name} is level 50, but this format allows level 100 Pokémon. (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`);
}
}

View File

@ -148,7 +148,12 @@ describe('Dex data', function () {
it('should have valid Formats', function () {
for (const format of Dex.formats.all()) {
Dex.formats.getRuleTable(format);
try {
Dex.formats.getRuleTable(format);
} catch (e) {
e.message = `${format.name}: ${e.message}`;
throw e;
}
}
});

View File

@ -791,6 +791,21 @@ describe('Team Validator', function () {
assert(illegal);
});
it('should support restrictions', function () {
let team = [
{species: 'Yveltal', ability: 'No Ability', moves: ['protect'], evs: {hp: 1}},
];
let illegal = TeamValidator.get('gen7customgame@@@limitonerestricted,*restrictedlegendary').validateTeam(team);
assert.equal(illegal, null);
team = [
{species: 'Yveltal', ability: 'No Ability', moves: ['protect'], evs: {hp: 1}},
{species: 'Xerneas', ability: 'No Ability', moves: ['protect'], evs: {hp: 1}},
];
illegal = TeamValidator.get('gen7customgame@@@limitonerestricted,*restrictedlegendary').validateTeam(team);
assert(illegal);
});
it('should allow moves to be banned', function () {
const team = [
{species: 'pikachu', ability: 'static', moves: ['agility', 'protect', 'thunder', 'thunderbolt'], evs: {hp: 1}},

View File

@ -433,11 +433,12 @@ async function getAnalysesByFormat(pokemon: string, gen: GenerationNum) {
}
function getLevel(format: Format, level = 0) {
if (format.forcedLevel) return format.forcedLevel;
const maxLevel = format.maxLevel || 100;
const maxForcedLevel = format.maxForcedLevel || maxLevel;
if (!level) level = format.defaultLevel || maxLevel;
return level > maxForcedLevel ? maxForcedLevel : level;
const ruleTable = Dex.formats.getRuleTable(format);
if (ruleTable.adjustLevel) return ruleTable.adjustLevel;
const maxLevel = ruleTable.maxLevel;
const adjustLevelDown = ruleTable.adjustLevelDown || maxLevel;
if (!level) level = ruleTable.defaultLevel;
return level > adjustLevelDown ? adjustLevelDown : level;
}
export async function getStatisticsURL(