pokemon-showdown/sim/dex-formats.ts
Guangcong Luo f9fdc73133
Support per-pokemon Residual handlers in Side/Field conditions (#8222)
For side conditions, `onStart`/`onRestart`/`onResidual`/`onEnd`
have been renamed `onSideStart`/`onSideRestart`/`onSideResidual`/`onSideEnd`,
with the `onResidualOrder` properties renamed `onSideResidualOrder`.

For field conditions, `onStart`/`onRestart`/`onResidual`/`onEnd`
have been renamed `onFieldStart`/`onFieldRestart`/`onFieldResidual`/`onFieldEnd`,
with the `onResidualOrder` properties renamed `onFieldResidualOrder`.

(The `onField` and `onSide` part helps make it clear to the type system
that the first argument is a Field or Side, not a Pokemon.)

Side and field conditions can now use `onResidual` to tick separately
on each pokemon in Speed order. `onResidualOrder` (the per-pokemon
tick) can be timed separate from `onSideResidualOrder` (the
per-condition tick), allowing conditions to end at a different priority
than they tick per-pokemon.

Relatedly, `onTeamPreview` and `onStart` in formats now need to be
`onFieldTeamPreview` and `onFieldStart`.

Unrelatedly, `effectData` has been renamed `effectState`, and the
corresponding state containers (`pokemon.statusData`,
`pokemon.speciesData`, `pokemon.itemData`, `pokemon.abilityData`,
`field.weatherData`, `field.terrainData`) have been similarly renamed. I
renamed the types a while ago, but I was holding off renaming the fields
because it would be a breaking change. But this is a breaking change
anyway, so we might as well do it now.

Note: `onResidual` will tick even on `onSideEnd` turns, although
`onSideResidual` won't. When refactoring weather, remember to
check `this.state.duration` so you don't deal weather damage on the
ending turn.

Intended as a better fix for #8216
2021-04-25 10:55:54 -07:00

717 lines
25 KiB
TypeScript

import {Utils} from '../lib';
import {toID, BasicEffect} from './dex-data';
import {EventMethods} from './dex-conditions';
const DEFAULT_MOD = 'gen8';
const MAIN_FORMATS = `${__dirname}/../.config-dist/formats`;
const CUSTOM_FORMATS = `${__dirname}/../.config-dist/custom-formats`;
export interface FormatData extends Partial<Format>, EventMethods {
name: string;
}
export type FormatList = (FormatData | {section: string, column?: number})[];
export type ModdedFormatData = FormatData | Omit<FormatData, 'name'> & {inherit: true};
type FormatEffectType = 'Format' | 'Ruleset' | 'Rule' | 'ValidatorRule';
/** rule, source, limit, bans */
export type ComplexBan = [string, string, number, string[]];
export type ComplexTeamBan = ComplexBan;
/**
* A RuleTable keeps track of the rules that a format has. The key can be:
* - '[ruleid]' the ID of a rule in effect
* - '-[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<string, string> {
complexBans: ComplexBan[];
complexTeamBans: ComplexTeamBan[];
checkCanLearn: [TeamValidator['checkCanLearn'], string] | null;
timer: [Partial<GameTimerSettings>, string] | null;
minSourceGen: [number, string] | null;
constructor() {
super();
this.complexBans = [];
this.complexTeamBans = [];
this.checkCanLearn = null;
this.timer = null;
this.minSourceGen = null;
}
isBanned(thing: string) {
if (this.has(`+${thing}`)) return false;
return this.has(`-${thing}`);
}
isBannedSpecies(species: Species) {
if (this.has(`+pokemon:${species.id}`)) return false;
if (this.has(`-pokemon:${species.id}`)) return true;
if (this.has(`+basepokemon:${toID(species.baseSpecies)}`)) return false;
if (this.has(`-basepokemon:${toID(species.baseSpecies)}`)) return true;
const tier = species.tier === '(PU)' ? 'ZU' : species.tier === '(NU)' ? 'PU' : species.tier;
if (this.has(`+pokemontag:${toID(tier)}`)) return false;
if (this.has(`-pokemontag:${toID(tier)}`)) return true;
const doublesTier = species.doublesTier === '(DUU)' ? 'DNU' : species.doublesTier;
if (this.has(`+pokemontag:${toID(doublesTier)}`)) return false;
if (this.has(`-pokemontag:${toID(doublesTier)}`)) return true;
return this.has(`-pokemontag:allpokemon`);
}
isRestricted(thing: string) {
if (this.has(`+${thing}`)) return false;
return this.has(`*${thing}`);
}
isRestrictedSpecies(species: Species) {
if (this.has(`+pokemon:${species.id}`)) return false;
if (this.has(`*pokemon:${species.id}`)) return true;
if (this.has(`+basepokemon:${toID(species.baseSpecies)}`)) return false;
if (this.has(`*basepokemon:${toID(species.baseSpecies)}`)) return true;
const tier = species.tier === '(PU)' ? 'ZU' : species.tier === '(NU)' ? 'PU' : species.tier;
if (this.has(`+pokemontag:${toID(tier)}`)) return false;
if (this.has(`*pokemontag:${toID(tier)}`)) return true;
const doublesTier = species.doublesTier === '(DUU)' ? 'DNU' : species.doublesTier;
if (this.has(`+pokemontag:${toID(doublesTier)}`)) return false;
if (this.has(`*pokemontag:${toID(doublesTier)}`)) return true;
return this.has(`*pokemontag:allpokemon`);
}
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 | null {
const source = this.get(key);
if (source === undefined) return null;
if (key === '-nonexistent' || key.startsWith('obtainable')) {
return 'not obtainable';
}
return source ? `banned by ${source}` : `banned`;
}
getComplexBanIndex(complexBans: ComplexBan[], rule: string): number {
const ruleId = toID(rule);
let complexBanIndex = -1;
for (let i = 0; i < complexBans.length; i++) {
if (toID(complexBans[i][0]) === ruleId) {
complexBanIndex = i;
break;
}
}
return complexBanIndex;
}
addComplexBan(rule: string, source: string, limit: number, bans: string[]) {
const complexBanIndex = this.getComplexBanIndex(this.complexBans, rule);
if (complexBanIndex !== -1) {
if (this.complexBans[complexBanIndex][2] === Infinity) return;
this.complexBans[complexBanIndex] = [rule, source, limit, bans];
} else {
this.complexBans.push([rule, source, limit, bans]);
}
}
addComplexTeamBan(rule: string, source: string, limit: number, bans: string[]) {
const complexBanTeamIndex = this.getComplexBanIndex(this.complexTeamBans, rule);
if (complexBanTeamIndex !== -1) {
if (this.complexTeamBans[complexBanTeamIndex][2] === Infinity) return;
this.complexTeamBans[complexBanTeamIndex] = [rule, source, limit, bans];
} else {
this.complexTeamBans.push([rule, source, limit, bans]);
}
}
}
export class Format extends BasicEffect implements Readonly<BasicEffect> {
readonly mod: string;
/**
* Name of the team generator algorithm, if this format uses
* random/fixed teams. null if players can bring teams.
*/
readonly team?: string;
readonly effectType: FormatEffectType;
readonly debug: boolean;
/**
* Whether or not a format will update ladder points if searched
* for using the "Battle!" button.
* (Challenge and tournament games will never update ladder points.)
* (Defaults to `true`.)
*/
readonly rated: boolean | string;
/** Game type. */
readonly gameType: GameType;
/** List of rule names. */
readonly ruleset: string[];
/**
* Base list of rule names as specified in "./config/formats.ts".
* Used in a custom format to correctly display the altered ruleset.
*/
readonly baseRuleset: string[];
/** List of banned effects. */
readonly banlist: string[];
/** List of effects that aren't completely banned. */
readonly restricted: string[];
/** List of inherited banned effects to override. */
readonly unbanlist: string[];
/** List of ruleset and banlist changes in a custom format. */
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?: {range: [number, number], total: number};
/**
* The number of Pokemon players can bring to battle and
* the number that can actually be used.
*/
readonly teamLength?: {battle?: number, 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;
readonly battle?: ModdedBattleScriptsData;
readonly pokemon?: ModdedBattlePokemon;
readonly queue?: ModdedBattleQueue;
readonly field?: ModdedField;
readonly actions?: ModdedBattleActions;
readonly cannotMega?: string[];
readonly challengeShow?: boolean;
readonly searchShow?: boolean;
readonly threads?: string[];
readonly timer?: Partial<GameTimerSettings>;
readonly tournamentShow?: boolean;
readonly checkCanLearn?: (
this: TeamValidator, move: Move, species: Species, setSources: PokemonSources, set: PokemonSet
) => string | null;
readonly getEvoFamily?: (this: Format, speciesid: string) => ID;
readonly getSharedPower?: (this: Format, pokemon: Pokemon) => Set<string>;
readonly onChangeSet?: (
this: TeamValidator, set: PokemonSet, format: Format, setHas?: AnyObject, teamHas?: AnyObject
) => string[] | void;
readonly onModifySpeciesPriority?: number;
readonly onModifySpecies?: (
this: Battle, species: Species, target?: Pokemon, source?: Pokemon, effect?: Effect
) => Species | void;
readonly onFieldStart?: (this: Battle) => void;
readonly onFieldTeamPreview?: (this: Battle) => void;
readonly onValidateSet?: (
this: TeamValidator, set: PokemonSet, format: Format, setHas: AnyObject, teamHas: AnyObject
) => string[] | void;
readonly onValidateTeam?: (
this: TeamValidator, team: PokemonSet[], format: Format, teamHas: AnyObject
) => string[] | void;
readonly validateSet?: (this: TeamValidator, set: PokemonSet, teamHas: AnyObject) => string[] | null;
readonly validateTeam?: (this: TeamValidator, team: PokemonSet[], options?: {
removeNicknames?: boolean,
skipSets?: {[name: string]: {[key: string]: boolean}},
}) => string[] | void;
readonly section?: string;
readonly column?: number;
constructor(data: AnyObject) {
super(data);
data = this;
this.mod = Utils.getString(data.mod) || 'gen8';
this.effectType = Utils.getString(data.effectType) as FormatEffectType || 'Format';
this.debug = !!data.debug;
this.rated = (typeof data.rated === 'string' ? data.rated : data.rated !== false);
this.gameType = data.gameType || 'singles';
this.ruleset = data.ruleset || [];
this.baseRuleset = data.baseRuleset || [];
this.banlist = data.banlist || [];
this.restricted = data.restricted || [];
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;
}
}
/** merges format lists from config/formats and config/custom-formats */
function mergeFormatLists(main: FormatList, custom: FormatList | undefined): FormatList {
// interface for the builder.
interface FormatSection {
section: string;
column?: number;
formats: FormatData[];
}
// result that is return and makes the actual list for formats.
const result: FormatList = [];
// used as a intermediary to build the final list.
const build: FormatSection[] = [];
// used to track current section to keep formats under their sections.
let current: FormatSection | undefined = {section: "", formats: []};
// populates the original sections and formats easily
// there should be no repeat sections at this point.
for (const element of main) {
if (element.section) {
current = {section: element.section, column: element.column, formats: []};
build.push(current);
} else if ((element as FormatData).name) {
current.formats.push((element as FormatData));
}
}
// merges the second list the hard way. Accounts for repeats.
if (custom !== undefined) {
for (const element of custom) {
// finds the section and makes it if it doesn't exist.
if (element.section) {
current = build.find(e => e.section === element.section);
// if it's new it makes a new entry.
if (current === undefined) {
current = {section: element.section, column: element.column, formats: []};
build.push(current);
}
} else if ((element as FormatData).name) { // otherwise, adds the element to its section.
current.formats.push(element as FormatData);
}
}
}
// builds the final result.
for (const element of build) {
// adds the section to the list.
result.push({section: element.section, column: element.column}, ...element.formats);
}
return result;
}
export class DexFormats {
readonly dex: ModdedDex;
rulesetCache = new Map<ID, Format>();
formatsListCache: readonly Format[] | null;
constructor(dex: ModdedDex) {
this.dex = dex;
this.formatsListCache = null;
}
load(): this {
if (!this.dex.isBase) throw new Error(`This should only be run on the base mod`);
this.dex.includeMods();
if (this.formatsListCache) return this;
const formatsList = [];
// Load formats
let customFormats;
try {
customFormats = require(CUSTOM_FORMATS).Formats;
if (!Array.isArray(customFormats)) {
throw new TypeError(`Exported property 'Formats' from "./config/custom-formats.ts" must be an array`);
}
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND' && e.code !== 'ENOENT') {
throw e;
}
}
let Formats: AnyObject[] = require(MAIN_FORMATS).Formats;
if (!Array.isArray(Formats)) {
throw new TypeError(`Exported property 'Formats' from "./config/formats.ts" must be an array`);
}
if (customFormats) Formats = mergeFormatLists(Formats as any, customFormats);
let section = '';
let column = 1;
for (const [i, format] of Formats.entries()) {
const id = toID(format.name);
if (format.section) section = format.section;
if (format.column) column = format.column;
if (!format.name && format.section) continue;
if (!id) {
throw new RangeError(`Format #${i + 1} must have a name with alphanumeric characters, not '${format.name}'`);
}
if (!format.section) format.section = section;
if (!format.column) format.column = column;
if (this.rulesetCache.has(id)) throw new Error(`Format #${i + 1} has a duplicate ID: '${id}'`);
format.effectType = 'Format';
format.baseRuleset = format.ruleset ? format.ruleset.slice() : [];
if (format.challengeShow === undefined) format.challengeShow = true;
if (format.searchShow === undefined) format.searchShow = true;
if (format.tournamentShow === undefined) format.tournamentShow = true;
if (format.mod === undefined) format.mod = 'gen8';
if (!this.dex.dexes[format.mod]) throw new Error(`Format "${format.name}" requires nonexistent mod: '${format.mod}'`);
const ruleset = new Format(format);
this.rulesetCache.set(id, ruleset);
formatsList.push(ruleset);
}
this.formatsListCache = formatsList;
return this;
}
/**
* Returns a sanitized format ID if valid, or throws if invalid.
*/
validate(name: string) {
const [formatName, customRulesString] = name.split('@@@', 2);
const format = this.get(formatName);
if (!format.exists) throw new Error(`Unrecognized format "${formatName}"`);
if (!customRulesString) return format.id;
const ruleTable = this.getRuleTable(format);
const customRules = customRulesString.split(',').map(rule => {
rule = rule.replace(/[\r\n|]*/g, '').trim();
const ruleSpec = this.validateRule(rule);
if (typeof ruleSpec === 'string' && ruleTable.has(ruleSpec)) return null;
return rule;
}).filter(Boolean);
if (!customRules.length) throw new Error(`The format already has your custom rules`);
const validatedFormatid = format.id + '@@@' + customRules.join(',');
const moddedFormat = this.get(validatedFormatid, true);
this.getRuleTable(moddedFormat);
return validatedFormatid;
}
get(name?: string | Format, isTrusted = false): Format {
if (name && typeof name !== 'string') return name;
name = (name || '').trim();
let id = toID(name);
if (!name.includes('@@@')) {
const ruleset = this.rulesetCache.get(id);
if (ruleset) return ruleset;
}
if (this.dex.data.Aliases.hasOwnProperty(id)) {
name = this.dex.data.Aliases[id];
id = toID(name);
}
if (this.dex.data.Rulesets.hasOwnProperty(DEFAULT_MOD + id)) {
id = (DEFAULT_MOD + id) as ID;
}
let supplementaryAttributes: AnyObject | null = null;
if (name.includes('@@@')) {
if (!isTrusted) {
try {
name = this.validate(name);
isTrusted = true;
} catch (e) {}
}
const [newName, customRulesString] = name.split('@@@', 2);
name = newName.trim();
id = toID(name);
if (isTrusted && customRulesString) {
supplementaryAttributes = {
customRules: customRulesString.split(','),
searchShow: false,
};
}
}
let effect;
if (this.dex.data.Rulesets.hasOwnProperty(id)) {
effect = new Format({name, ...this.dex.data.Rulesets[id] as any, ...supplementaryAttributes});
} else {
effect = new Format({id, name, exists: false});
}
return effect;
}
all() {
this.load();
return this.formatsListCache!;
}
getRuleTable(format: Format, depth = 1, repeals?: Map<string, number>): RuleTable {
if (format.ruleTable && !repeals) return format.ruleTable;
if (depth === 1) {
const dex = this.dex.mod(format.mod);
if (dex !== this.dex) {
return dex.formats.getRuleTable(format, 2, repeals);
}
}
const ruleTable = new RuleTable();
const ruleset = format.ruleset.slice();
for (const ban of format.banlist) {
ruleset.push('-' + ban);
}
for (const ban of format.restricted) {
ruleset.push('*' + ban);
}
for (const ban of format.unbanlist) {
ruleset.push('+' + ban);
}
if (format.customRules) {
ruleset.push(...format.customRules);
}
if (format.checkCanLearn) {
ruleTable.checkCanLearn = [format.checkCanLearn, format.name];
}
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
for (const rule of ruleset) {
if (rule.startsWith('!')) {
const ruleSpec = this.validateRule(rule, format) as string;
if (!repeals) repeals = new Map();
repeals.set(ruleSpec.slice(1), depth);
}
}
for (const rule of ruleset) {
const ruleSpec = this.validateRule(rule, format);
if (typeof ruleSpec !== 'string') {
if (ruleSpec[0] === 'complexTeamBan') {
const complexTeamBan: ComplexTeamBan = ruleSpec.slice(1) as ComplexTeamBan;
ruleTable.addComplexTeamBan(complexTeamBan[0], complexTeamBan[1], complexTeamBan[2], complexTeamBan[3]);
} else if (ruleSpec[0] === 'complexBan') {
const complexBan: ComplexBan = ruleSpec.slice(1) as ComplexBan;
ruleTable.addComplexBan(complexBan[0], complexBan[1], complexBan[2], complexBan[3]);
} else {
throw new Error(`Unrecognized rule spec ${ruleSpec}`);
}
continue;
}
if (rule.startsWith('!')) {
const repealDepth = repeals!.get(ruleSpec.slice(1));
if (repealDepth === undefined) throw new Error(`Multiple "${rule}" rules in ${format.name}`);
if (repealDepth === depth) {
throw new Error(`Rule "${rule}" did nothing because "${rule.slice(1)}" is not in effect`);
}
if (repealDepth === -depth) repeals!.delete(ruleSpec.slice(1));
continue;
}
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}"`);
}
for (const prefix of "-*+") ruleTable.delete(prefix + ruleSpec.slice(1));
ruleTable.set(ruleSpec, '');
continue;
}
const subformat = this.get(ruleSpec);
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}"`);
}
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) {
// don't check for "already exists" here; multiple inheritance is allowed
if (!repeals?.has(k)) {
ruleTable.set(k, v || subformat.name);
}
}
for (const [subRule, source, limit, bans] of subRuleTable.complexBans) {
ruleTable.addComplexBan(subRule, source || subformat.name, limit, bans);
}
for (const [subRule, source, limit, bans] of subRuleTable.complexTeamBans) {
ruleTable.addComplexTeamBan(subRule, source || subformat.name, limit, bans);
}
if (subRuleTable.checkCanLearn) {
if (ruleTable.checkCanLearn) {
throw new Error(
`"${format.name}" has conflicting move validation rules from ` +
`"${ruleTable.checkCanLearn[1]}" and "${subRuleTable.checkCanLearn[1]}"`
);
}
ruleTable.checkCanLearn = subRuleTable.checkCanLearn;
}
if (subRuleTable.timer) {
if (ruleTable.timer) {
throw new Error(
`"${format.name}" has conflicting timer validation rules from "${ruleTable.timer[1]}" and "${subRuleTable.timer[1]}"`
);
}
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;
}
}
if (!repeals) format.ruleTable = ruleTable;
return ruleTable;
}
validateRule(rule: string, format: Format | null = null) {
if (rule !== rule.trim()) throw new Error(`Rule "${rule}" should be trimmed`);
switch (rule.charAt(0)) {
case '-':
case '*':
case '+':
if (format?.team) throw new Error(`We don't currently support bans in generated teams`);
if (rule.slice(1).includes('>') || rule.slice(1).includes('+')) {
let buf = rule.slice(1);
const gtIndex = buf.lastIndexOf('>');
let limit = rule.startsWith('+') ? Infinity : 0;
if (gtIndex >= 0 && /^[0-9]+$/.test(buf.slice(gtIndex + 1).trim())) {
if (limit === 0) limit = parseInt(buf.slice(gtIndex + 1));
buf = buf.slice(0, gtIndex);
}
let checkTeam = buf.includes('++');
const banNames = buf.split(checkTeam ? '++' : '+').map(v => v.trim());
if (banNames.length === 1 && limit > 0) checkTeam = true;
const innerRule = banNames.join(checkTeam ? ' ++ ' : ' + ');
const bans = banNames.map(v => this.validateBanRule(v));
if (checkTeam) {
return ['complexTeamBan', innerRule, '', limit, bans];
}
if (bans.length > 1 || limit > 0) {
return ['complexBan', innerRule, '', limit, bans];
}
throw new Error(`Confusing rule ${rule}`);
}
return rule.charAt(0) + this.validateBanRule(rule.slice(1));
default:
const id = toID(rule);
if (!this.dex.data.Rulesets.hasOwnProperty(id)) {
throw new Error(`Unrecognized rule "${rule}"`);
}
if (rule.startsWith('!')) return `!${id}`;
return id;
}
}
validateBanRule(rule: string) {
let id = toID(rule);
if (id === 'unreleased') return 'unreleased';
if (id === 'nonexistent') return 'nonexistent';
const matches = [];
let matchTypes = ['pokemon', 'move', 'ability', 'item', 'nature', 'pokemontag'];
for (const matchType of matchTypes) {
if (rule.startsWith(`${matchType}:`)) {
matchTypes = [matchType];
id = id.slice(matchType.length) as ID;
break;
}
}
const ruleid = id;
if (this.dex.data.Aliases.hasOwnProperty(id)) id = toID(this.dex.data.Aliases[id]);
for (const matchType of matchTypes) {
let table;
switch (matchType) {
case 'pokemon': table = this.dex.data.Pokedex; break;
case 'move': table = this.dex.data.Moves; break;
case 'item': table = this.dex.data.Items; break;
case 'ability': table = this.dex.data.Abilities; break;
case 'nature': table = this.dex.data.Natures; break;
case 'pokemontag':
// valid pokemontags
const validTags = [
// singles tiers
'uber', 'ou', 'uubl', 'uu', 'rubl', 'ru', 'nubl', 'nu', 'publ', 'pu', 'zu', 'nfe', 'lc', 'cap', 'caplc', 'capnfe', 'ag',
// doubles tiers
'duber', 'dou', 'dbl', 'duu', 'dnu',
// custom tags -- nduubl is used for national dex teambuilder formatting
'mega', 'nduubl',
// illegal/nonstandard reasons
'past', 'future', 'unobtainable', 'lgpe', 'custom',
// all
'allpokemon', 'allitems', 'allmoves', 'allabilities', 'allnatures',
];
if (validTags.includes(ruleid)) matches.push('pokemontag:' + ruleid);
continue;
default:
throw new Error(`Unrecognized match type.`);
}
if (table.hasOwnProperty(id)) {
if (matchType === 'pokemon') {
const species: Species = table[id] as Species;
if (species.otherFormes && ruleid !== species.id + toID(species.baseForme)) {
matches.push('basepokemon:' + id);
continue;
}
}
matches.push(matchType + ':' + id);
} else if (matchType === 'pokemon' && id.endsWith('base')) {
id = id.slice(0, -4) as ID;
if (table.hasOwnProperty(id)) {
matches.push('pokemon:' + id);
}
}
}
if (matches.length > 1) {
throw new Error(`More than one thing matches "${rule}"; please specify one of: ` + matches.join(', '));
}
if (matches.length < 1) {
throw new Error(`Nothing matches "${rule}"`);
}
return matches[0];
}
}