#!/usr/bin/env node
'use strict';

const fs = require("fs");
const path = require('path');
const child_process = require("child_process");

const rootDir = path.resolve(__dirname, '..');
process.chdir(rootDir);

if (!fs.existsSync('caches/pokemon-showdown')) {
	child_process.execSync('git clone https://github.com/smogon/pokemon-showdown.git', {
		cwd: 'caches',
	});
}

process.stdout.write("Syncing data from Git repository... ");
child_process.execSync('git pull', { cwd: 'caches/pokemon-showdown' });
child_process.execSync('npm run build', { cwd: 'caches/pokemon-showdown' });
console.log("DONE");

const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex;
const CompoundWordNames = require('../caches/pokemon-showdown/dist/data/aliases').CompoundWordNames;
const toID = Dex.toID;
process.stdout.write("Loading gen 6 data... ");
Dex.includeData();
console.log("DONE");

function es3stringify(obj) {
	const buf = JSON.stringify(obj);
	return buf.replace(/"([A-Za-z][A-Za-z0-9]*)":/g, (fullMatch, key) => (
		['return', 'new', 'delete'].includes(key) ? fullMatch : `${key}:`
	));
}

function requireNoCache(pathSpec) {
	delete require.cache[require.resolve(pathSpec)];
	return require(pathSpec);
}

/*********************************************************
 * Build search-index.js
 *********************************************************/

{
	process.stdout.write("Building `data/search-index.js`... ");

	let index = [];

	index = index.concat(Object.keys(Dex.data.Pokedex).map(x => x + ' pokemon'));
	index = index.concat(Object.keys(Dex.data.Moves).map(x => x + ' move'));
	index = index.concat(Object.keys(Dex.data.Items).map(x => x + ' item'));
	index = index.concat(Object.keys(Dex.data.Abilities).map(x => x + ' ability'));
	index = index.concat(Object.keys(Dex.data.TypeChart).map(x => toID(x) + ' type'));
	index = index.concat(['physical', 'special', 'status'].map(x => toID(x) + ' category'));
	index = index.concat([
		'monster', 'water1', 'bug', 'flying', 'field', 'fairy', 'grass', 'humanlike', 'water3', 'mineral', 'amorphous', 'water2', 'ditto', 'dragon', 'undiscovered',
	].map(x => toID(x) + ' egggroup'));
	index = index.concat([
		'ou', 'uu', 'ru', 'nu', 'pu', 'zu', 'lc', 'nfe', 'uber', 'uubl', 'rubl', 'nubl', 'publ', 'zubl', 'cap', 'caplc', 'capnfe',
	].map(x => toID(x) + ' tier'));

	const BattleArticleTitles = {};

	try {
		for (const file of fs.readdirSync('../dex.pokemonshowdown.com/articles/')) {
			if (file.endsWith('.md')) {
				const id = file.slice(0, -3);
				const contents = '' + fs.readFileSync('../dex.pokemonshowdown.com/articles/' + file);
				if (contents.startsWith('# ')) {
					const title = contents.slice(2, contents.indexOf('\n'));
					if (title !== id.charAt(0).toUpperCase() + id.slice(1)) {
						BattleArticleTitles[id] = title;
					}
				}
				index.push('' + id + ' article');
			}
		}
	} catch {
		console.log('\n(WARNING: NO ARTICLES)');
	}
	index.push('pokemon article');
	index.push('moves article');

	const compoundTable = new Map(CompoundWordNames.map(word => [toID(word), word]));
	// generate aliases
	function generateAlias(id, name, type) {
		name = compoundTable.get(id) || name;
		if (type === 'pokemon' && !compoundTable.has(id)) {
			const species = Dex.species.get(id);
			const baseid = toID(species.baseSpecies);
			if (baseid !== id) {
				name = (compoundTable.get(baseid) || species.baseSpecies) + ' ' + species.forme;
			}
		}
		if (name.endsWith(' Mega-X') || name.endsWith(' Mega-Y')) {
			index.push('mega' + toID(name.slice(0, -7) + name.slice(-1)) + ' ' + type + ' ' + id + ' 0');
			index.push('m' + toID(name.slice(0, -7) + name.slice(-1)) + ' ' + type + ' ' + id + ' 0');
			index.push('mega' + toID(name.slice(-1)) + ' ' + type + ' ' + id + ' ' + toID(name.slice(0, -7)).length);
		} else if (name.endsWith(' Mega')) {
			index.push('mega' + toID(name.slice(0, -5)) + ' ' + type + ' ' + id + ' 0');
			index.push('m' + toID(name.slice(0, -5)) + ' ' + type + ' ' + id + ' 0');
		} else if (name.endsWith(' Alola')) {
			index.push('alolan' + toID(name.slice(0, -6)) + ' ' + type + ' ' + id + ' 0');
		} else if (name.endsWith(' Galar')) {
			index.push('galarian' + toID(name.slice(0, -6)) + ' ' + type + ' ' + id + ' 0');
		} else if (name.endsWith(' Hisui')) {
			index.push('hisuian' + toID(name.slice(0, -6)) + ' ' + type + ' ' + id + ' 0');
		}
		const fullSplit = name.split(/ |-/).map(toID);
		if (fullSplit.length < 2) return;
		const fullAcronym = fullSplit.map(x => x.charAt(0)).join('') + fullSplit.at(-1).slice(1);
		index.push('' + fullAcronym + ' ' + type + ' ' + id + ' 0');
		for (let i = 1; i < fullSplit.length; i++) {
			index.push('' + fullSplit.slice(i).join('') + ' ' + type + ' ' + id + ' ' + fullSplit.slice(0, i).join('').length);
		}

		const spaceSplit = name.split(' ').map(toID);
		if (spaceSplit.length !== fullSplit.length) {
			const spaceAcronym = spaceSplit.map(x => x.charAt(0)).join('') + spaceSplit.at(-1).slice(1);
			if (spaceAcronym !== fullAcronym) {
				index.push('' + spaceAcronym + ' ' + type + ' ' + id + ' 0');
			}
		}
	}
	for (const id in Dex.data.Pokedex) {
		const name = Dex.data.Pokedex[id].name;
		if (Dex.data.Pokedex[id].isCosmeticForme) continue;
		generateAlias(id, name, 'pokemon');
	}
	for (const id in Dex.data.Moves) {
		const name = Dex.data.Moves[id].name;
		generateAlias(id, name, 'move');
	}
	for (const id in Dex.data.Items) {
		const name = Dex.data.Items[id].name;
		generateAlias(id, name, 'item');
	}
	for (const id in Dex.data.Abilities) {
		const name = Dex.data.Abilities[id].name;
		generateAlias(id, name, 'ability');
	}

	// hardcode ultra beasts
	const ultraBeasts = {
		ub01symbiont: "nihilego",
		ub02absorption: "buzzwole",
		ub02beauty: "pheromosa",
		ub03lightning: "xurkitree",
		ub04blade: "kartana",
		ub04blaster: "celesteela",
		ub05glutton: "guzzlord",
		ubburst: "blacephalon",
		ubassembly: "stakataka",
		ubadhesive: "poipole",
		ubstinger: "naganadel",
	};
	for (const [ubCode, id] of Object.entries(ultraBeasts)) {
		index.push(`${ubCode} pokemon ${id} 0`);
	}

	index.sort();

	// manually rearrange
	index[index.indexOf('grass type')] = 'grass egggroup';
	index[index.indexOf('grass egggroup')] = 'grass type';

	index[index.indexOf('fairy type')] = 'fairy egggroup';
	index[index.indexOf('fairy egggroup')] = 'fairy type';

	index[index.indexOf('flying type')] = 'flying egggroup';
	index[index.indexOf('flying egggroup')] = 'flying type';

	index[index.indexOf('dragon type')] = 'dragon egggroup';
	index[index.indexOf('dragon egggroup')] = 'dragon type';

	index[index.indexOf('bug type')] = 'bug egggroup';
	index[index.indexOf('bug egggroup')] = 'bug type';

	index[index.indexOf('psychic type')] = 'psychic move';
	index[index.indexOf('psychic move')] = 'psychic type';

	index[index.indexOf('ditto pokemon')] = 'ditto egggroup';
	index[index.indexOf('ditto egggroup')] = 'ditto pokemon';

	const BattleSearchIndex = index.map(x => {
		x = x.split(' ');
		if (x.length > 3) {
			x[3] = Number(x[3]);
			x[2] = index.indexOf(x[2] + ' ' + x[1]);
		}
		return x;
	});

	const BattleSearchIndexOffset = BattleSearchIndex.map(entry => {
		const id = entry[0];
		let name = '';
		switch (entry[1]) {
		case 'pokemon': name = Dex.species.get(id).name; break;
		case 'move': name = Dex.moves.get(id).name; break;
		case 'item': name = Dex.items.get(id).name; break;
		case 'ability': name = Dex.abilities.get(id).name; break;
		case 'article': name = BattleArticleTitles[id] || ''; break;
		}
		let res = '';
		let nonAlnum = 0;
		for (let i = 0, j = 0; i < id.length; i++, j++) {
			while (!/[a-zA-Z0-9]/.test(name[j])) {
				j++;
				nonAlnum++;
			}
			res += nonAlnum;
		}
		if (nonAlnum) return res;
		return '';
	});

	const BattleSearchCountIndex = {};
	for (const type in Dex.data.TypeChart) {
		BattleSearchCountIndex[type + ' move'] = Object.keys(Dex.data.Moves)
			.filter(id => (Dex.data.Moves[id].type === type)).length;
	}

	for (const type in Dex.data.TypeChart) {
		BattleSearchCountIndex[type + ' pokemon'] = Object.keys(Dex.data.Pokedex)
			.filter(id => (
				!Dex.data.Pokedex[id].isCosmeticForme &&
				Dex.data.Pokedex[id].types.indexOf(type) >= 0
			)).length;
	}

	let buf = '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n';

	buf += 'exports.BattleSearchIndex = ' + JSON.stringify(BattleSearchIndex) + ';\n\n';

	buf += 'exports.BattleSearchIndexOffset = ' + JSON.stringify(BattleSearchIndexOffset) + ';\n\n';

	buf += 'exports.BattleSearchCountIndex = ' + JSON.stringify(BattleSearchCountIndex) + ';\n\n';

	buf += 'exports.BattleArticleTitles = ' + JSON.stringify(BattleArticleTitles) + ';\n\n';

	fs.writeFileSync('play.pokemonshowdown.com/data/search-index.js', buf);
}

console.log("DONE");

/*********************************************************
 * Build teambuilder-tables.js
 *********************************************************/

process.stdout.write("Building `data/teambuilder-tables.js`... ");

{
	const BattleTeambuilderTable = {};

	let buf = '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n';
	const GENS = [9, 8, 7, 6, 5, 4, 3, 2, 1];
	const DOUBLES = GENS.filter(x => x > 2).map(num => -num);
	const VGC = GENS.filter(x => x > 3).map(num => -num - 0.5);
	const NFE = GENS.map(num => num + 0.3);
	const LC = GENS.map(num => num + 0.7);
	const STADIUM = [2.04, 1.04];
	const NATDEX = [9.1, 8.1];
	const OTHER = [
		9.9, 9.6, 9.411, 9.41, 9.401, 9.4, 9.33, 9.2, -9.4, -9.401,
		8.6, 8.4, 8.2, 8.1, -8.4, -8.6,
		7.1,
		5.1,
		3.1,
	];

	// process.stdout.write("\n  ");
	for (const genIdent of [...GENS, ...DOUBLES, ...VGC, ...NFE, ...STADIUM, ...OTHER, ...NATDEX, ...LC]) {
		const isLetsGo = (genIdent === 7.1);
		const isBDSP = (genIdent === 8.6 || genIdent === -8.6);
		const isMetBattle = ('' + genIdent).endsWith('.2');
		const isNFE = ('' + genIdent).endsWith('.3');
		const isLC = ('' + genIdent).endsWith('.7');
		const isSSDLC1 = (genIdent === 8.4 || genIdent === -8.4);
		const isPreDLC = (genIdent === 9.4 || genIdent === 9.41 || genIdent === -9.4);
		const isSVDLC1 = (genIdent === 9.401 || genIdent === 9.411 || genIdent === -9.401);
		const isLegendsZA = genIdent === 9.33;
		const isNatDex = ('' + genIdent).endsWith('.1') && genIdent > 8;
		const isStadium = ('' + genIdent).endsWith('.04');
		const isDoubles = (genIdent < 0);
		const isVGC = ('' + genIdent).endsWith('.5');
		const isGen9BH = genIdent === 9.9;
		const isSSB = genIdent === 9.6;
		const genNum = Math.floor(isDoubles ? -genIdent : genIdent);
		const isBW1 = genIdent === 5.1;
		const isRS = genIdent === 3.1;
		const gen = (() => {
			let genStr = 'gen' + genNum;
			if (isSSDLC1) genStr += 'dlc1';
			if (isLetsGo) genStr += 'letsgo';
			if (isBDSP) genStr += 'bdsp';
			if (isPreDLC) genStr += 'predlc';
			if (isSVDLC1) genStr += 'dlc1';
			if (isLegendsZA) genStr += 'legendsou';
			if (isStadium) genStr += 'stadium' + (genNum > 1 ? genNum : '');
			if (isSSB) genStr += 'ssb';
			if (isBW1) genStr += 'bw1';
			if (isRS) genStr += 'rs';
			return genStr;
		})();
		// process.stdout.write("" + gen + (isDoubles ? " doubles" : "") + "... ");
		const pokemon = Object.keys(Dex.data.Pokedex);
		pokemon.sort();
		const tierTable = {};
		const overrideTier = {};
		const ubersUUBans = {};
		const ndDoublesBans = {};
		const thirtyfivePokes = {};
		const monotypeBans = {};
		const nonstandardMoves = [];
		const gen5zuBans = {};
		const gen4puBans = {};
		for (const id of pokemon) {
			const species = Dex.mod(gen).species.get(id);
			const baseSpecies = Dex.mod(gen).species.get(species.baseSpecies);
			if (species.gen > genNum) continue;
			const speciesTier = (() => {
				if (isMetBattle) {
					let tier = species.tier;
					if (species.isNonstandard) {
						if (species.isNonstandard === 'Past') {
							tier = Dex.mod('gen7').species.get(species.name).tier;
						} else {
							tier = 'OU';
						}
					}
					if (species.isNonstandard === 'Gigantamax') tier = '(Uber)';
					if (species.tier === 'CAP LC') tier = 'LC';
					if (species.tier === 'CAP NFE') tier = 'NFE';
					if (species.tier === 'CAP') tier = 'OU';
					const format = Dex.formats.get(gen + 'metronomebattle');
					let bst = 0;
					for (const stat of Object.values(species.baseStats)) {
						bst += stat;
					}
					if (bst > 625) tier = 'Illegal';
					if (Dex.formats.getRuleTable(format).isBannedSpecies(species)) tier = 'Illegal';
					if (species.types.includes('Steel')) tier = 'Illegal';
					return tier;
				}
				if (isNFE) {
					let tier = species.tier;
					if (!species.nfe) tier = 'Illegal';
					const format = Dex.formats.get(gen + 'nfe');
					const banlist = Dex.formats.getRuleTable(format);
					if (banlist.isBannedSpecies(species)) {
						tier = 'Uber';
					}
					return tier;
				}
				if (isLC) {
					let tier = species.tier;
					const lc = Dex.formats.get(gen + 'lc');
					const lcBanlist = Dex.formats.getRuleTable(lc);
					const prevoSpecies = Dex.mod(gen).species.get(species.prevo);
					if (!species.nfe || (prevoSpecies.exists && prevoSpecies.gen <= genNum) || lcBanlist.isBannedSpecies(species)) {
						tier = 'Illegal';
					}
					if (/^([OURNPZ]U(BL)?|Uber|AG)$/g.test(tier) && tier !== 'Illegal') {
						tier = 'LC';
					}
					return tier;
				}
				if (isLetsGo) {
					const validNum = (baseSpecies.num <= 151 && species.num >= 1) || [808, 809].includes(baseSpecies.num);
					if (!validNum) return 'Illegal';
					if (species.forme && !['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme)) return 'Illegal';
					if (species.name === 'Pikachu-Alola') return 'Illegal';
					return species.tier;
				}
				if (isVGC) {
					if (species.isNonstandard && species.isNonstandard !== 'Gigantamax') return 'Illegal';
					if (baseSpecies.tags.includes('Mythical')) return 'Mythical';
					if (baseSpecies.tags.includes('Restricted Legendary')) return 'Restricted Legendary';
					if (species.tier === 'NFE') return 'NFE';
					if (species.tier === 'LC') return 'LC';
					return 'Regular';
				}
				if (isRS) {
					if (species.isNonstandard) {
						if (species.isNonstandard === 'Unobtainable') return 'Unreleased';
						return 'Illegal';
					}
					if (species.tier === 'Uber') return 'Uber';
					if (species.nfe) {
						if (species.prevo.length === 0) return "LC";
						return "NFE";
					}
					return "Regular";
				}
				if (isGen9BH) {
					if ((species.natDexTier === 'Illegal' || species.forme.includes('Totem')) &&
						!['Floette-Eternal', 'Greninja-Ash', 'Xerneas-Neutral'].includes(species.name)) {
						return 'Illegal';
					}
					if ((species.name === 'Xerneas' || species.battleOnly || species.forme === 'Eternamax') &&
						!(species.isMega || species.isPrimal || ['Greninja-Ash', 'Necrozma-Ultra'].includes(species.name))) {
						return 'Illegal';
					}
					if (species.isNonstandard && ['LGPE', 'CAP', 'Future'].includes(species.isNonstandard)) return 'Illegal';
					return species.tags.includes('Mythical') ? 'Mythical' :
						species.tags.includes('Restricted Legendary') ? 'Restricted Legendary' :
						species.nfe ? (species.prevo ? 'NFE' : 'LC') : 'Regular';
				}
				if (species.tier === 'CAP' || species.tier === 'CAP NFE' || species.tier === 'CAP LC') {
					return species.tier;
				}
				if (isDoubles && genNum > 4) {
					return species.doublesTier;
				}
				if (isNatDex || (isPreDLC && genNum === 9.41) || (isSVDLC1 && genNum === 9.411)) {
					return species.natDexTier;
				}
				return species.tier;
			})();
			overrideTier[species.id] = speciesTier;
			if (species.forme) {
				if (
					[
						'Aegislash', 'Castform', 'Cherrim', 'Cramorant', 'Eiscue', 'Meloetta', 'Mimikyu', 'Minior', 'Morpeko', 'Ramnarok', 'Wishiwashi',
					].includes(species.baseSpecies) || species.forme.includes('Totem') || species.forme.includes('Zen') ||
					(species.baseSpecies === 'Ogerpon' && species.forme.includes('Tera')) || species.isCosmeticForme
				) {
					continue;
				}
			}

			if (!tierTable[speciesTier]) tierTable[speciesTier] = [];
			tierTable[speciesTier].push(id);

			if (genNum === 9) {
				const ubersUU = Dex.formats.get(gen + 'ubersuu');
				if (ubersUU.exists && Dex.formats.getRuleTable(ubersUU).isBannedSpecies(species)) {
					ubersUUBans[species.id] = 1;
				}
				const nd35Pokes = Dex.formats.get(gen + 'nationaldex35pokes');
				if (nd35Pokes.exists && !Dex.formats.getRuleTable(nd35Pokes).isBannedSpecies(species)) {
					thirtyfivePokes[species.id] = 1;
				}
			}
			if (genNum >= 8) {
				const ndDoubles = Dex.formats.get(gen + 'nationaldexdoubles');
				if (ndDoubles.exists && Dex.formats.getRuleTable(ndDoubles).isBannedSpecies(species)) {
					ndDoublesBans[species.id] = 1;
				}
			}
			if (genNum >= 5) {
				if (genNum === 5) {
					const gen5zu = Dex.formats.get(gen + 'zu');
					if (gen5zu.exists && Dex.formats.getRuleTable(gen5zu).isBannedSpecies(species)) {
						gen5zuBans[species.id] = 1;
					}
				}
				const mono = Dex.formats.get(gen + (isNatDex ? 'nationaldex' : '') + 'monotype');
				if (mono.exists && Dex.formats.getRuleTable(mono).isBannedSpecies(species)) {
					monotypeBans[species.id] = 1;
				}
			}
			if (genNum === 4) {
				const gen4pu = Dex.formats.get(gen + 'pu');
				if (gen4pu.exists && Dex.formats.getRuleTable(gen4pu).isBannedSpecies(species)) {
					gen4puBans[species.id] = 1;
				}
			}
		}

		nonstandardMoves.push(...Object.keys(Dex.data.Moves).filter(id => {
			const move = Dex.mod(isSSDLC1 ? 'gen8dlc1' : isPreDLC ? 'gen9predlc' : 'gen9dlc1').moves.get(id);
			const bMove = Dex.mod(isSSDLC1 ? 'gen8' : 'gen9').moves.get(id);
			return bMove.isNonstandard !== move.isNonstandard;
		}));

		const tiers = [];
		const items = [];
		const formatSlices = {};

		if (isNatDex || (isPreDLC && genNum === 9.41) || (isSVDLC1 && genNum === 9.411)) {
			BattleTeambuilderTable['gen' + genNum + 'natdex'] = {};
			BattleTeambuilderTable['gen' + genNum + 'natdex'].tiers = tiers;
			BattleTeambuilderTable['gen' + genNum + 'natdex'].overrideTier = overrideTier;
			BattleTeambuilderTable['gen' + genNum + 'natdex'].items = items;
			BattleTeambuilderTable['gen' + genNum + 'natdex'].ndDoublesBans = ndDoublesBans;
			BattleTeambuilderTable['gen' + genNum + 'natdex'].monotypeBans = monotypeBans;
			BattleTeambuilderTable['gen' + genNum + 'natdex'].formatSlices = formatSlices;
			if (isNatDex && genNum === 9) {
				BattleTeambuilderTable['gen' + genNum + 'natdex'].thirtyfivePokes = thirtyfivePokes;
			}
		} else if (isMetBattle) {
			BattleTeambuilderTable[gen + 'metronome'] = {};
			BattleTeambuilderTable[gen + 'metronome'].tiers = tiers;
			BattleTeambuilderTable[gen + 'metronome'].items = items;
			BattleTeambuilderTable[gen + 'metronome'].formatSlices = formatSlices;
		} else if (isNFE) {
			BattleTeambuilderTable[gen + 'nfe'] = {};
			BattleTeambuilderTable[gen + 'nfe'].tiers = tiers;
			BattleTeambuilderTable[gen + 'nfe'].overrideTier = overrideTier;
			BattleTeambuilderTable[gen + 'nfe'].formatSlices = formatSlices;
		} else if (isLC) {
			BattleTeambuilderTable[gen + 'lc'] = {};
			BattleTeambuilderTable[gen + 'lc'].tiers = tiers;
			BattleTeambuilderTable[gen + 'lc'].overrideTier = overrideTier;
			BattleTeambuilderTable[gen + 'lc'].formatSlices = formatSlices;
		} else if (isLetsGo) {
			BattleTeambuilderTable['gen7letsgo'] = {};
			BattleTeambuilderTable['gen7letsgo'].learnsets = {};
			BattleTeambuilderTable['gen7letsgo'].tiers = tiers;
			BattleTeambuilderTable['gen7letsgo'].overrideTier = overrideTier;
			BattleTeambuilderTable['gen7letsgo'].formatSlices = formatSlices;
		} else if (isBDSP && !isDoubles) {
			BattleTeambuilderTable['gen8bdsp'] = {};
			BattleTeambuilderTable['gen8bdsp'].learnsets = {};
			BattleTeambuilderTable['gen8bdsp'].tiers = tiers;
			BattleTeambuilderTable['gen8bdsp'].items = items;
			BattleTeambuilderTable['gen8bdsp'].overrideTier = overrideTier;
			BattleTeambuilderTable['gen8bdsp'].monotypeBans = monotypeBans;
			BattleTeambuilderTable['gen8bdsp'].formatSlices = formatSlices;
		} else if (isVGC) {
			BattleTeambuilderTable[gen + 'vgc'] = {};
			BattleTeambuilderTable[gen + 'vgc'].tiers = tiers;
			BattleTeambuilderTable[gen + 'vgc'].formatSlices = formatSlices;
		} else if (isDoubles) {
			BattleTeambuilderTable[gen + 'doubles'] = {};
			BattleTeambuilderTable[gen + 'doubles'].tiers = tiers;
			BattleTeambuilderTable[gen + 'doubles'].items = items;
			BattleTeambuilderTable[gen + 'doubles'].overrideTier = overrideTier;
			BattleTeambuilderTable[gen + 'doubles'].formatSlices = formatSlices;
		} else if (isGen9BH) {
			BattleTeambuilderTable['bh'] = {};
			BattleTeambuilderTable['bh'].tiers = tiers;
			BattleTeambuilderTable['bh'].overrideTier = overrideTier;
			BattleTeambuilderTable['bh'].formatSlices = formatSlices;
		} else if (isSSB) {
			BattleTeambuilderTable['gen9ssb'] = {};
			BattleTeambuilderTable['gen9ssb'].tiers = tiers;
			BattleTeambuilderTable['gen9ssb'].overrideTier = overrideTier;
			BattleTeambuilderTable['gen9ssb'].formatSlices = formatSlices;
		} else if (isLegendsZA) {
			BattleTeambuilderTable['gen9legendsou'] = {};
			BattleTeambuilderTable['gen9legendsou'].tiers = tiers;
			BattleTeambuilderTable['gen9legendsou'].overrideTier = overrideTier;
			BattleTeambuilderTable['gen9legendsou'].formatSlices = formatSlices;
			BattleTeambuilderTable['gen9legendsou'].items = items;
			BattleTeambuilderTable['gen9legendsou'].learnsets = {};
		} else if (gen === 'gen9') {
			BattleTeambuilderTable.tiers = tiers;
			BattleTeambuilderTable.items = items;
			BattleTeambuilderTable.overrideTier = overrideTier;
			BattleTeambuilderTable.ubersUUBans = ubersUUBans;
			BattleTeambuilderTable.monotypeBans = monotypeBans;
			BattleTeambuilderTable.formatSlices = formatSlices;
		} else if (isBW1 || isRS) {
			BattleTeambuilderTable[gen] = {};
			BattleTeambuilderTable[gen].overrideTier = overrideTier;
			BattleTeambuilderTable[gen].tiers = tiers;
			BattleTeambuilderTable[gen].items = items;
			BattleTeambuilderTable[gen].formatSlices = formatSlices;
			BattleTeambuilderTable[gen].nonstandardMoves = nonstandardMoves;
			BattleTeambuilderTable[gen].learnsets = {};
		} else {
			BattleTeambuilderTable[gen] = {};
			if (genNum === 5) {
				BattleTeambuilderTable[gen].gen5zuBans = gen5zuBans;
			}
			if (genNum === 4) {
				BattleTeambuilderTable[gen].gen4puBans = gen4puBans;
			}
			BattleTeambuilderTable[gen].overrideTier = overrideTier;
			BattleTeambuilderTable[gen].tiers = tiers;
			BattleTeambuilderTable[gen].items = items;
			BattleTeambuilderTable[gen].formatSlices = formatSlices;
			if (genNum >= 5) {
				BattleTeambuilderTable[gen].monotypeBans = monotypeBans;
			}
			if (isSSDLC1 || isPreDLC || isSVDLC1) {
				BattleTeambuilderTable[gen].nonstandardMoves = nonstandardMoves;
				BattleTeambuilderTable[gen].learnsets = {};
			}
		}

		const tierOrder = (() => {
			if (isVGC || isGen9BH) {
				return ["Mythical", "Restricted Legendary", "Regular", "NFE", "LC"];
			}
			if (isRS) {
				return ["Regular", "NFE", "LC", "Uber"];
			}
			if (isDoubles && genNum > 4) {
				return ["DUber", "(DUber)", "DOU", "DBL", "(DOU)", "DUU", "(DUU)", "New", "NFE", "LC"];
			}
			if (gen === 'gen4') {
				return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "OU", "(OU)", "UUBL", "UU", "NUBL", "NU", "NFE", "LC"];
			}
			return [
				"CAP", "CAP NFE", "CAP LC", "AG", "Uber", "(Uber)", "OU", "(OU)", "UUBL", "UU", "RUBL", "RU", "NUBL", "NU", "PUBL", "PU", "ZUBL", "ZU", "New", "NFE", "LC", "Unreleased",
			];
		})();

		for (const tier of tierOrder) {
			if (tier in {
				OU: 1, UUBL: 1, AG: 1, Uber: 1, UU: 1, RU: 1, NU: 1, PU: 1, ZU: 1, NFE: 1, LC: 1, DOU: 1, DUU: 1,
				"(DUU)": 1, New: 1, Legal: 1, Regular: 1, "Restricted Legendary": 1, "CAP LC": 1,
			}) {
				let usedTier = tier;
				if (usedTier === "(DUU)") usedTier = "DNU";
				formatSlices[usedTier] = tiers.length;
			}
			if (!tierTable[tier]) continue;
			if (tier === "(PU)") {
				tiers.push(['header', "Below PU"]);
			} else if (tier === "(DUU)") {
				tiers.push(['header', "Below DUU"]);
			} else if (tier.charAt(0) === '(') {
				tiers.push(['header', tier.slice(1, -1) + " by technicality"]);
			} else if (tier === "NFE") {
				tiers.push(['header', "NFEs not in a higher tier"]);
			} else {
				tiers.push(['header', tier]);
			}
			tiers.push(...tierTable[tier]);
		}
		if (!isDoubles) {
			if (!formatSlices['OU']) formatSlices['OU'] = formatSlices['New'];
			if (!formatSlices['UU']) formatSlices['UU'] = formatSlices['New'];
			if (!formatSlices['RU']) formatSlices['RU'] = formatSlices['New'];
		} else {
			if (!formatSlices['DOU']) formatSlices['DOU'] = formatSlices['New'];
			if (!formatSlices['DUU']) formatSlices['DUU'] = formatSlices['New'];
		}

		const itemList = Object.keys(Dex.data.Items);
		itemList.sort();

		const greatItems = [['header', "Popular items"]];
		const goodItems = [['header', "Items"]];
		const specificItems = [['header', "Pok\u00e9mon-specific items"]];
		const poorItems = [['header', "Usually useless items"]];
		const badItems = [['header', "Useless items"]];
		const unreleasedItems = [];
		if (genNum === 6) unreleasedItems.push(['header', "Unreleased"]);
		for (const id of itemList) {
			const item = Dex.mod(gen).items.get(id);
			if (item.gen > genNum) {
				continue;
			}
			if (item.isNonstandard && !isMetBattle) {
				if (isNatDex) {
					let curItem = item;
					let curGen = genNum;
					while (curItem.isNonstandard && curGen >= 7) {
						curItem = Dex.forGen(curGen).items.get(item.id);
						curGen--;
					}
					if (curItem.isNonstandard) continue;
				} else if (genNum !== 2) {
					continue;
				}
			}
			if (isMetBattle) {
				const banlist = Dex.formats.getRuleTable(Dex.formats.get(gen + 'metronomebattle'));
				if (banlist.isBanned('item:' + item.id)) continue;
			}
			switch (id) {
			// mainstays
			case 'choiceband':
			case 'choicespecs':
			case 'eviolite':
				greatItems.push(id);
				break;
			// great everywhere but metronome
			case 'leftovers':
			case 'lifeorb':
			case 'choicescarf':
			case 'assaultvest':
			case 'focussash':
			case 'powerherb':
			case 'rockyhelmet':
				if (!isMetBattle) greatItems.push(id);
				else goodItems.push(id);
				break;
			// just singles
			case 'airballoon':
			case 'loadeddice':
			case 'heavydutyboots':
			case 'expertbelt':
			case 'salacberry':
				if (isDoubles || isMetBattle) goodItems.push(id);
				else greatItems.push(id);
				break;
			// just doubles
			case 'safetygoggles':
			case 'ejectbutton':
			case 'ejectpack':
				if (isDoubles) greatItems.push(id);
				else goodItems.push(id);
				break;
			// doubles + metronome
			case 'covertcloak':
			case 'clearamulet':
			case 'weaknesspolicy':
				if (isDoubles || isMetBattle) greatItems.push(id);
				else goodItems.push(id);
				break;
			// metronome only
			case 'mirrorherb':
				if (isMetBattle) greatItems.push(id);
				else goodItems.push(id);
				break;
			// generation specific
			case 'mentalherb':
				if (isMetBattle) goodItems.push(id);
				else if (genNum > 4) greatItems.push(id);
				else poorItems.push(id);
				break;
			case 'lumberry':
				if (genNum === 2 || genNum > 6) goodItems.push(id);
				else greatItems.push(id);
				break;
			case 'sitrusberry':
				if (genNum > 3 && (genNum < 7 || isDoubles)) greatItems.push(id);
				else if (genNum > 6) goodItems.push(id);
				else poorItems.push(id);
				break;
			case 'aguavberry':
			case 'figyberry':
			case 'iapapaberry':
			case 'magoberry':
			case 'wikiberry':
				if (genNum === 7) greatItems.push(id);
				else if (genNum >= 8) goodItems.push(id);
				else poorItems.push(id);
				break;
			case 'berryjuice':
				if (genNum === 2) poorItems.push(id);
				else goodItems.push(id);
				break;
			case 'dragonfang':
				if (genNum === 2) badItems.push(id);
				else goodItems.push(id);
				break;
			case 'dragonscale':
				if (genNum === 2) goodItems.push(id);
				else badItems.push(id);
				break;
			case 'mail':
				if (genNum >= 6) unreleasedItems.push(id);
				else goodItems.push(id);
				break;
			// Legendaries
			case 'adamantorb':
			case 'griseousorb':
			case 'lustrousorb':
			case 'blueorb':
			case 'redorb':
			case 'souldew':
			// falls through
			// Other
			case 'leek':
			case 'stick':
			case 'thickclub':
			case 'lightball':
			case 'luckypunch':
			case 'quickpowder':
			case 'metalpowder':
			case 'deepseascale':
			case 'deepseatooth':
				specificItems.push(id);
				break;
			// Fling-only
			case 'rarebone':
			case 'belueberry':
			case 'blukberry':
			case 'cornnberry':
			case 'durinberry':
			case 'hondewberry':
			case 'magostberry':
			case 'nanabberry':
			case 'nomelberry':
			case 'pamtreberry':
			case 'pinapberry':
			case 'pomegberry':
			case 'qualotberry':
			case 'rabutaberry':
			case 'razzberry':
			case 'spelonberry':
			case 'tamatoberry':
			case 'watmelberry':
			case 'wepearberry':
			case 'energypowder':
			case 'electirizer':
			case 'oldamber':
			case 'dawnstone':
			case 'dubiousdisc':
			case 'duskstone':
			case 'firestone':
			case 'icestone':
			case 'leafstone':
			case 'magmarizer':
			case 'moonstone':
			case 'ovalstone':
			case 'prismscale':
			case 'protector':
			case 'reapercloth':
			case 'sachet':
			case 'shinystone':
			case 'sunstone':
			case 'thunderstone':
			case 'upgrade':
			case 'waterstone':
			case 'whippeddream':
			case 'bottlecap':
			case 'goldbottlecap':
			case 'galaricacuff':
			case 'chippedpot':
			case 'crackedpot':
			case 'galaricawreath':
			case 'auspiciousarmor':
			case 'maliciousarmor':
			case 'masterpieceteacup':
			case 'metalalloy':
			case 'unremarkableteacup':
			case 'bignugget':
				badItems.push(id);
				break;
			// outclassed items
			case 'aspearberry':
			case 'bindingband':
			case 'cheriberry':
			case 'destinyknot':
			case 'enigmaberry':
			case 'floatstone':
			case 'ironball':
			case 'jabocaberry':
			case 'oranberry':
			case 'machobrace':
			case 'pechaberry':
			case 'persimberry':
			case 'poweranklet':
			case 'powerband':
			case 'powerbelt':
			case 'powerbracer':
			case 'powerlens':
			case 'powerweight':
			case 'rawstberry':
			case 'ringtarget':
			case 'rowapberry':
			case 'bigroot':
			case 'focusband':
			// gen 2
			case 'psncureberry':
			case 'przcureberry':
			case 'burntberry':
			case 'bitterberry':
			case 'iceberry':
			case 'berry':
				poorItems.push(id);
				break;
			default:
				if (
					item.name.endsWith(" Ball") || item.name.endsWith(" Fossil") || item.name.startsWith("Fossilized ") ||
					item.name.endsWith(" Sweet") || item.name.endsWith(" Apple")
				) {
					badItems.push(id);
				} else if (item.name.startsWith("TR")) {
					badItems.push(id);
				} else if (item.name.endsWith(" Gem") && item.name !== "Normal Gem") {
					if (genNum >= 6) {
						unreleasedItems.push(id);
					} else {
						goodItems.push(id);
					}
				} else if (item.name.endsWith(" Drive")) {
					specificItems.push(id);
				} else if (item.name.endsWith(" Memory")) {
					specificItems.push(id);
				} else if (item.name.startsWith("Rusted")) {
					specificItems.push(id);
				} else if (item.itemUser) {
					specificItems.push(id);
				} else if (item.megaStone) {
					specificItems.push(id);
				} else {
					goodItems.push(id);
				}
			}
		}
		items.push(...greatItems);
		items.push(...goodItems);
		items.push(...specificItems);
		items.push(...poorItems);
		items.push(...badItems);
		items.push(...unreleasedItems);
	}

	//
	// Learnset table
	//

	const gen3HMs = new Set(['cut', 'fly', 'surf', 'strength', 'flash', 'rocksmash', 'waterfall', 'dive']);
	const gen4HMs = new Set(['cut', 'fly', 'surf', 'strength', 'rocksmash', 'waterfall', 'rockclimb']);

	const learnsets = {};
	BattleTeambuilderTable.learnsets = learnsets;
	for (const id in Dex.data.Learnsets) {
		const learnset = Dex.data.Learnsets[id].learnset;
		if (!learnset) continue;
		learnsets[id] = {};
		for (const moveid in learnset) {
			const gens = learnset[moveid].map(x => Number(x[0]));
			const minGen = Math.min(...gens);

			if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) {
				let legalGens = '';
				let available = false;

				if (minGen === 3) {
					legalGens += '3';
					available = true;
				}
				if (available) available = !gen3HMs.has(moveid);

				if (available || gens.includes(4)) {
					legalGens += '4';
					available = true;
				}
				if (available) available = !gen4HMs.has(moveid);

				const minUpperGen = available ? 5 : Math.min(
					...gens.filter(gen => gen > 4)
				);
				legalGens += '0123456789'.slice(minUpperGen);
				learnsets[id][moveid] = legalGens;
			} else {
				learnsets[id][moveid] = '0123456789'.slice(minGen);
			}

			if (gens.indexOf(6) >= 0) learnsets[id][moveid] += 'p';
			if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) {
				learnsets[id][moveid] += 'q';
			}
			if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) {
				learnsets[id][moveid] += 'g';
			}
			if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) {
				learnsets[id][moveid] += 'a';
			}
			if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x === '9E')) learnsets[id][moveid] += 'e';
		}
	}
	const G2Learnsets = Dex.mod('gen2').data.Learnsets;
	for (const id in G2Learnsets) {
		const learnset = G2Learnsets[id].learnset;
		for (const moveid in learnset) {
			const gens = learnset[moveid].map(x => Number(x[0]));
			const minGen = Math.min(...gens);
			if (!learnsets[id]) {
				throw new Error(`${id} has a Gen 2 learnset but not a modern learnset`);
			}
			if (!learnsets[id][moveid]) learnsets[id][moveid] = '';
			if (minGen === 2) learnsets[id][moveid] = '2' + learnsets[id][moveid];
			if (minGen === 1) learnsets[id][moveid] = '12' + learnsets[id][moveid];
		}
	}
	const G3RSLearnsets = Dex.mod('gen3rs').data.Learnsets;
	for (const id in G3RSLearnsets) {
		const species = Dex.mod('gen3rs').species.get(id);
		if (species.isNonstandard && !['Unobtainable', 'CAP'].includes(species.isNonstandard)) continue;
		const learnset = G3RSLearnsets[id].learnset;
		BattleTeambuilderTable['gen3rs'].learnsets[id] = {};
		for (const moveid in learnset) {
			BattleTeambuilderTable['gen3rs'].learnsets[id][moveid] = '3';
		}
	}
	const G5BW1Learnsets = Dex.mod('gen5bw1').data.Learnsets;
	for (const id in G5BW1Learnsets) {
		const species = Dex.mod('gen5bw1').species.get(id);
		if (species.isNonstandard && !['Unobtainable', 'CAP'].includes(species.isNonstandard)) continue;
		const learnset = G5BW1Learnsets[id].learnset;
		BattleTeambuilderTable['gen5bw1'].learnsets[id] = {};
		for (const moveid in learnset) {
			BattleTeambuilderTable['gen5bw1'].learnsets[id][moveid] = '5';
		}
	}
	const LGLearnsets = Dex.mod('gen7letsgo').data.Learnsets;
	for (const id in LGLearnsets) {
		const species = Dex.mod('gen7letsgo').species.get(id);
		const baseSpecies = Dex.mod('gen7letsgo').species.get(species.baseSpecies);
		const validNum = (baseSpecies.num <= 151 && baseSpecies.num >= 1) || [808, 809].includes(baseSpecies.num);
		if (!validNum) continue;
		if (species.forme && !['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme)) continue;
		const learnset = LGLearnsets[id].learnset;
		BattleTeambuilderTable['gen7letsgo'].learnsets[id] = {};
		for (const moveid in learnset) {
			BattleTeambuilderTable['gen7letsgo'].learnsets[id][moveid] = '7';
		}
	}
	const BDSPLearnsets = Dex.mod('gen8bdsp').data.Learnsets;
	for (const id in BDSPLearnsets) {
		const species = Dex.mod('gen8bdsp').species.get(id);
		if (species.isNonstandard && !['Unobtainable', 'CAP'].includes(species.isNonstandard)) continue;
		const learnset = BDSPLearnsets[id].learnset;
		BattleTeambuilderTable['gen8bdsp'].learnsets[id] = {};
		for (const moveid in learnset) {
			BattleTeambuilderTable['gen8bdsp'].learnsets[id][moveid] = '8g';
		}
	}
	const SSDLC1Learnsets = Dex.mod('gen8dlc1').data.Learnsets;
	for (const id in SSDLC1Learnsets) {
		const learnset = SSDLC1Learnsets[id].learnset;
		if (!learnset) continue;
		BattleTeambuilderTable['gen8dlc1'].learnsets[id] = {};
		for (const moveid in learnset) {
			const gens = learnset[moveid].map(x => Number(x[0]));
			const minGen = Math.min(...gens);

			if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) {
				let legalGens = '';
				let available = false;

				if (minGen === 3) {
					legalGens += '3';
					available = true;
				}
				if (available) available = !gen3HMs.has(moveid);

				if (available || gens.includes(4)) {
					legalGens += '4';
					available = true;
				}
				if (available) available = !gen4HMs.has(moveid);

				const minUpperGen = available ? 5 : Math.min(
					...gens.filter(gen => gen > 4)
				);
				legalGens += '012345678'.slice(minUpperGen);
				BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] = legalGens;
			} else {
				BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] = '012345678'.slice(minGen);
			}

			if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'p';
			if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) {
				BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'q';
			}
			if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) {
				BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'g';
			}
		}
	}
	const PreDLCLearnsets = Dex.mod('gen9predlc').data.Learnsets;
	for (const id in PreDLCLearnsets) {
		const learnset = PreDLCLearnsets[id].learnset;
		if (!learnset) continue;
		BattleTeambuilderTable['gen9predlc'].learnsets[id] = {};
		for (const moveid in learnset) {
			const gens = learnset[moveid].map(x => Number(x[0]));
			const minGen = Math.min(...gens);

			if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) {
				let legalGens = '';
				let available = false;

				if (minGen === 3) {
					legalGens += '3';
					available = true;
				}
				if (available) available = !gen3HMs.has(moveid);

				if (available || gens.includes(4)) {
					legalGens += '4';
					available = true;
				}
				if (available) available = !gen4HMs.has(moveid);

				const minUpperGen = available ? 5 : Math.min(
					...gens.filter(gen => gen > 4)
				);
				legalGens += '0123456789'.slice(minUpperGen);
				BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] = legalGens;
			} else {
				BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] = '0123456789'.slice(minGen);
			}

			if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'p';
			if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) {
				BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'q';
			}
			if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) {
				BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'g';
			}
			if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) {
				BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'a';
			}
		}
	}
	const SVDLC1Learnsets = Dex.mod('gen9dlc1').data.Learnsets;
	for (const id in SVDLC1Learnsets) {
		const learnset = SVDLC1Learnsets[id].learnset;
		if (!learnset) continue;
		BattleTeambuilderTable['gen9dlc1'].learnsets[id] = {};
		for (const moveid in learnset) {
			const gens = learnset[moveid].map(x => Number(x[0]));
			const minGen = Math.min(...gens);

			if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) {
				let legalGens = '';
				let available = false;

				if (minGen === 3) {
					legalGens += '3';
					available = true;
				}
				if (available) available = !gen3HMs.has(moveid);

				if (available || gens.includes(4)) {
					legalGens += '4';
					available = true;
				}
				if (available) available = !gen4HMs.has(moveid);

				const minUpperGen = available ? 5 : Math.min(
					...gens.filter(gen => gen > 4)
				);
				legalGens += '0123456789'.slice(minUpperGen);
				BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] = legalGens;
			} else {
				BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] = '0123456789'.slice(minGen);
			}

			if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'p';
			if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) {
				BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'q';
			}
			if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) {
				BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'g';
			}
			if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) {
				BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'a';
			}
		}
	}
	const LegendsZALearnsets = Dex.mod('gen9legendsou').data.Learnsets;
	for (const id in LegendsZALearnsets) {
		const learnset = LegendsZALearnsets[id].learnset;
		if (!learnset) continue;
		BattleTeambuilderTable['gen9legendsou'].learnsets[id] = {};
		for (const moveid in learnset) {
			const gens = learnset[moveid].map(x => Number(x[0]));
			const minGen = Math.min(...gens);

			if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) {
				let legalGens = '';
				let available = false;

				if (minGen === 3) {
					legalGens += '3';
					available = true;
				}
				if (available) available = !gen3HMs.has(moveid);

				if (available || gens.includes(4)) {
					legalGens += '4';
					available = true;
				}
				if (available) available = !gen4HMs.has(moveid);

				const minUpperGen = available ? 5 : Math.min(
					...gens.filter(gen => gen > 4)
				);
				legalGens += '0123456789'.slice(minUpperGen);
				BattleTeambuilderTable['gen9legendsou'].learnsets[id][moveid] = legalGens;
			} else {
				BattleTeambuilderTable['gen9legendsou'].learnsets[id][moveid] = '0123456789'.slice(minGen);
			}

			if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen9legendsou'].learnsets[id][moveid] += 'p';
			if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) {
				BattleTeambuilderTable['gen9legendsou'].learnsets[id][moveid] += 'q';
			}
			if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) {
				BattleTeambuilderTable['gen9legendsou'].learnsets[id][moveid] += 'g';
			}
			if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) {
				BattleTeambuilderTable['gen9legendsou'].learnsets[id][moveid] += 'a';
			}
		}
	}

	// Client relevant data that should be overridden by past gens and mods
	const overrideSpeciesKeys = [
		'abilities', 'baseStats', 'cosmeticFormes', 'isNonstandard', 'requiredItems', 'types', 'unreleasedHidden',
	];
	const overrideMoveKeys = [
		'accuracy', 'basePower', 'category', 'desc', 'flags', 'isNonstandard', 'pp', 'priority', 'shortDesc', 'target', 'type',
	];
	const overrideAbilityKeys = ['desc', 'flags', 'isNonstandard', 'rating', 'shortDesc'];
	const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'naturalGift', 'shortDesc'];

	//
	// Past gen table
	//

	for (const genNum of [8, 7, 6, 5, 4, 3, 2, 1]) {
		const gen = 'gen' + genNum;
		const nextGen = 'gen' + (genNum + 1);
		const genDex = Dex.mod(gen);
		const genData = genDex.data;
		const nextGenDex = Dex.mod(nextGen);
		const nextGenData = nextGenDex.data;

		const overrideSpeciesData = {};
		BattleTeambuilderTable[gen].overrideSpeciesData = overrideSpeciesData;
		for (const id in genData.Pokedex) {
			const curEntry = genDex.species.get(id);
			if (curEntry.isCosmeticForme) continue;
			const nextEntry = nextGenDex.species.get(id);
			for (const key of overrideSpeciesKeys) {
				if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) {
					if (!overrideSpeciesData[id]) overrideSpeciesData[id] = {};
					overrideSpeciesData[id][key] = curEntry[key];
				}
			}
		}

		const overrideMoveData = {};
		BattleTeambuilderTable[gen].overrideMoveData = overrideMoveData;
		for (const id in genData.Moves) {
			const curEntry = genDex.moves.get(id);
			const nextEntry = nextGenDex.moves.get(id);
			for (const key of overrideMoveKeys) {
				if (key === 'category' && genNum <= 3) continue;
				if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) {
					if (!overrideMoveData[id]) overrideMoveData[id] = {};
					overrideMoveData[id][key] = curEntry[key];
				}
			}
		}

		const overrideAbilityData = {};
		BattleTeambuilderTable[gen].overrideAbilityData = overrideAbilityData;
		for (const id in genData.Abilities) {
			const curEntry = genDex.abilities.get(id);
			const nextEntry = nextGenDex.abilities.get(id);
			for (const key of overrideAbilityKeys) {
				if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) {
					if (!overrideAbilityData[id]) overrideAbilityData[id] = {};
					overrideAbilityData[id][key] = curEntry[key];
				}
			}
		}

		const overrideItemData = {};
		BattleTeambuilderTable[gen].overrideItemData = overrideItemData;
		for (const id in genData.Items) {
			const curEntry = genDex.items.get(id);
			const nextEntry = nextGenDex.items.get(id);
			for (const key of overrideItemKeys) {
				if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) {
					if (!overrideItemData[id]) overrideItemData[id] = {};
					overrideItemData[id][key] = curEntry[key];
				}
			}
		}

		const overrideTypeChart = {};
		BattleTeambuilderTable[gen].overrideTypeChart = overrideTypeChart;
		const removeType = {};
		BattleTeambuilderTable[gen].removeType = removeType;
		for (const id in nextGenData.TypeChart) {
			const curEntry = genData.TypeChart[id];
			const nextEntry = nextGenData.TypeChart[id];
			if (curEntry.isNonstandard) {
				removeType[id] = true;
				continue;
			}
			if (JSON.stringify(nextEntry) !== JSON.stringify(curEntry)) {
				overrideTypeChart[id] = curEntry;
			}
		}
	}

	//
	// Mods
	//

	for (const mod of ['gen3rs', 'gen5bw1', 'gen7letsgo', 'gen8bdsp', 'gen9ssb', 'gen9legendsou']) {
		const modDex = Dex.mod(mod);
		const modData = modDex.data;
		const parentDex = Dex.forGen(modDex.gen);

		const overrideSpeciesData = {};
		BattleTeambuilderTable[mod].overrideSpeciesData = overrideSpeciesData;
		for (const id in modData.Pokedex) {
			const modEntry = modDex.species.get(id);
			const parentEntry = parentDex.species.get(id);
			for (const key of overrideSpeciesKeys) {
				if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) {
					if (!overrideSpeciesData[id]) overrideSpeciesData[id] = {};
					overrideSpeciesData[id][key] = modEntry[key];
				}
			}
		}

		const overrideMoveData = {};
		BattleTeambuilderTable[mod].overrideMoveData = overrideMoveData;
		for (const id in modData.Moves) {
			const modEntry = modDex.moves.get(id);
			const parentEntry = parentDex.moves.get(id);
			for (const key of overrideMoveKeys) {
				if (key === 'category' && modDex.gen <= 3) continue;
				if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) {
					if (!overrideMoveData[id]) overrideMoveData[id] = {};
					overrideMoveData[id][key] = modEntry[key];
				}
			}
		}

		const overrideAbilityData = {};
		BattleTeambuilderTable[mod].overrideAbilityData = overrideAbilityData;
		for (const id in modData.Abilities) {
			const modEntry = modDex.abilities.get(id);
			const parentEntry = parentDex.abilities.get(id);
			for (const key of overrideAbilityKeys) {
				if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) {
					if (!overrideAbilityData[id]) overrideAbilityData[id] = {};
					overrideAbilityData[id][key] = modEntry[key];
				}
			}
		}

		const overrideItemData = {};
		BattleTeambuilderTable[mod].overrideItemData = overrideItemData;
		for (const id in modData.Items) {
			const modEntry = modDex.items.get(id);
			const parentEntry = parentDex.items.get(id);
			for (const key of overrideItemKeys) {
				if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) {
					if (!overrideItemData[id]) overrideItemData[id] = {};
					overrideItemData[id][key] = modEntry[key];
				}
			}
		}
	}

	buf += `exports.BattleTeambuilderTable = JSON.parse('${JSON.stringify(BattleTeambuilderTable).replace(/['\\]/g, "\\$&")}');\n\n`;

	fs.writeFileSync('play.pokemonshowdown.com/data/teambuilder-tables.js', buf);
}

console.log("DONE");

/*********************************************************
 * Build pokedex.js
 *********************************************************/

process.stdout.write("Building `data/pokedex.js`... ");

{
	const Pokedex = requireNoCache('../caches/pokemon-showdown/dist/data/pokedex.js').Pokedex;
	for (const id in Pokedex) {
		const entry = Pokedex[id];
		if (Dex.data.FormatsData[id]) {
			// console.log('formatsentry:' + id);
			const formatsEntry = Dex.data.FormatsData[id];
			if (formatsEntry.tier) entry.tier = formatsEntry.tier;
			if (formatsEntry.isNonstandard) entry.isNonstandard = formatsEntry.isNonstandard;
			if (formatsEntry.unreleasedHidden) entry.unreleasedHidden = formatsEntry.unreleasedHidden;
		}
	}
	const buf = 'exports.BattlePokedex = ' + es3stringify(Pokedex) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/pokedex.js', buf);
	fs.writeFileSync('play.pokemonshowdown.com/data/pokedex.json', JSON.stringify(Pokedex));
}

console.log("DONE");

/*********************************************************
 * Build moves.js
 *********************************************************/

process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.js`...");

{
	const Moves = requireNoCache('../caches/pokemon-showdown/dist/data/moves.js').Moves;
	for (const id in Moves) {
		const move = Dex.moves.get(Moves[id].name);
		if (move.desc) Moves[id].desc = move.desc;
		if (move.shortDesc) Moves[id].shortDesc = move.shortDesc;
		if (move.basePowerCallback) Moves[id].basePowerCallback = true;
	}
	const buf = 'exports.BattleMovedex = ' + es3stringify(Moves) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/moves.js', buf);
	fs.writeFileSync('play.pokemonshowdown.com/data/moves.json', JSON.stringify(Moves));
}

/*********************************************************
 * Build items.js
 *********************************************************/

{
	const Items = requireNoCache('../caches/pokemon-showdown/dist/data/items.js').Items;
	for (const id in Items) {
		const item = Dex.items.get(Items[id].name);
		if (item.desc) Items[id].desc = item.desc;
		if (item.shortDesc) Items[id].shortDesc = item.shortDesc;
	}
	const buf = 'exports.BattleItems = ' + es3stringify(Items) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/items.js', buf);
}

/*********************************************************
 * Build abilities.js
 *********************************************************/

{
	const Abilities = requireNoCache('../caches/pokemon-showdown/dist/data/abilities.js').Abilities;
	for (const id in Abilities) {
		const ability = Dex.abilities.get(Abilities[id].name);
		if (ability.desc) Abilities[id].desc = ability.desc;
		if (ability.shortDesc) Abilities[id].shortDesc = ability.shortDesc;
	}
	const buf = 'exports.BattleAbilities = ' + es3stringify(Abilities) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/abilities.js', buf);
}

/*********************************************************
 * Build typechart.js
 *********************************************************/

{
	const TypeChart = requireNoCache('../caches/pokemon-showdown/dist/data/typechart.js').TypeChart;
	const buf = 'exports.BattleTypeChart = ' + es3stringify(TypeChart) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/typechart.js', buf);
}

/*********************************************************
 * Build aliases.js
 *********************************************************/

{
	const Aliases = requireNoCache('../caches/pokemon-showdown/dist/data/aliases.js').Aliases;
	const buf = 'exports.BattleAliases = ' + es3stringify(Aliases) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/aliases.js', buf);
}

/*********************************************************
 * Build formats-data.js
 *********************************************************/

{
	const FormatsData = requireNoCache('../caches/pokemon-showdown/dist/data/formats-data.js').FormatsData;
	const buf = 'exports.BattleFormatsData = ' + es3stringify(FormatsData) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/formats-data.js', buf);
}

/*********************************************************
 * Build formats.js
 *********************************************************/

{
	const Formats = requireNoCache('../caches/pokemon-showdown/dist/config/formats.js').Formats;
	const buf = 'exports.Formats = ' + es3stringify(Formats) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/formats.js', buf);
}

/*********************************************************
 * Build learnsets.js
 *********************************************************/

{
	const Learnsets = requireNoCache('../caches/pokemon-showdown/dist/data/learnsets.js').Learnsets;
	const buf = 'exports.BattleLearnsets = ' + es3stringify(Learnsets) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/learnsets.js', buf);
	fs.writeFileSync('play.pokemonshowdown.com/data/learnsets.json', JSON.stringify(Learnsets));
}

/*********************************************************
 * Build text.js
 *********************************************************/

{
	const textData = Dex.loadTextData();
	const Text = textData.Default;

	function assignData(id, entry) {
		for (const key in entry) {
			if (['name', 'desc', 'shortDesc'].includes(key)) continue;
			const textEntry = Text[id] || (Text[id] = {});
			if (key.startsWith('gen')) {
				for (const modKey in entry[key]) {
					if (['desc', 'shortDesc'].includes(key)) continue;
					textEntry[modKey + 'Gen' + key.charAt(3)] = entry[key][modKey];
				}
			} else {
				textEntry[key] = entry[key];
			}
		}
	}

	for (const id in textData.Moves) assignData(id, textData.Moves[id]);
	for (const id in textData.Abilities) assignData(id, textData.Abilities[id]);
	for (const id in textData.Items) assignData(id, textData.Items[id]);

	const buf = 'exports.BattleText = ' + es3stringify(Text) + ';';
	fs.writeFileSync('play.pokemonshowdown.com/data/text.js', buf);
}

console.log("DONE");
