diff --git a/.eslintrc.js b/.eslintrc.js
index f3aa66b17..dd4833c54 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -28,6 +28,7 @@ module.exports = {
 
 		// Generic global variables
 		"Config": false, "BattleSearch": false, "Storage": false, "Dex": false, "DexSearch": false,
+		"ModConfig": false, "ModSprites": false,
 		"app": false, "toID": false, "toRoomid": false, "toUserid": false, "toName": false, "PSUtils": false, "MD5": false,
 		"ChatHistory": false, "Topbar": false, "UserList": false,
 
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b1ea8233f..fea7815ce 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,9 +5,9 @@ name: Node.js CI
 
 on:
   push:
-    branches: [ master ]
+    branches: [ main ]
   pull_request:
-    branches: [ master ]
+    branches: [ main ]
 
 jobs:
   build:
@@ -16,15 +16,24 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [14.x]
+        node-version: [18.x]
 
     steps:
     - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: Zarel/setup-node@patch-1
+      uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}
     - run: npm install
-    - run: npm run test
+    - run: npm run build --if-present
+    - run: node build full
+    - name: Build DH2
+      run: npm run build --if-present
+    - name: start DH2 server instance
+      run: |
+        node caches/DH2/pokemon-showdown &
+        while ! curl -s http://localhost:8000 -o /dev/null; do
+        sleep 60
+        done
       env:
         CI: true
diff --git a/.gitignore b/.gitignore
index 5a210bb73..258c56f94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,16 @@ npm-debug.log
 /play.pokemonshowdown.com/js/miniedit.js
 /play.pokemonshowdown.com/ads.txt
 
+/website/.well-known/
+/website/.pages-cached/
+/website/files/
+/website/images/
+/website/ads.txt
+/DH2/*
+/DH2/
+/website/replays/index.html
+/website/replays/js/
+node_modules
 /pokemonshowdown.com/.well-known/
 /pokemonshowdown.com/.pages-cached/
 /pokemonshowdown.com/files/
diff --git a/build-tools/build-indexes b/build-tools/build-indexes
old mode 100755
new mode 100644
index aa13c390f..3d7719068
--- a/build-tools/build-indexes
+++ b/build-tools/build-indexes
@@ -4,32 +4,57 @@
 const fs = require("fs");
 const path = require('path');
 const child_process = require("child_process");
+const server_repo = require("./server-repo");
 
 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',
+const debug = true;
+
+process.stdout.write("Syncing data from Git repository... ");
+if (!fs.existsSync('./caches/DH2')) {
+	child_process.execSync('git clone ' + server_repo + ' caches/DH2', {
 	});
+	child_process.execSync("git pull " + server_repo, {cwd: 'caches/DH2'});
 }
 
-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'});
+child_process.execSync('npm run build', {cwd: 'caches/DH2'});
+
 console.log("DONE");
 
-const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex;
+const Dex = require('../caches/DH2/dist/sim/dex').Dex;
 const toID = Dex.toID;
+const ModConfigData = require('../config/mod-config').ModConfigData;
+const ModConfig = ModConfigData.ClientMods;
+var Formats = require('../caches/DH2/dist/config/formats.js').Formats;
+
+for (const modid in Dex.dexes) {
+	try {
+		if ((/gen\d/.test(modid) && modid.length === 4) || modid === 'base') continue;
+		let teambuilderConfig = Dex.dexes[modid].data.Scripts.teambuilderConfig;
+		teambuilderConfig = teambuilderConfig ? teambuilderConfig : {};
+		if(teambuilderConfig.moveIsNotUseless) teambuilderConfig.moveIsNotUseless = JSON.stringify(teambuilderConfig.moveIsNotUseless.toString());
+		ModConfig[modid] = ModConfig[modid] ? ModConfig[modid] : {};
+		ModConfig[modid] = Object.assign(ModConfig[modid], teambuilderConfig);
+	} catch(err) {
+		// delete ModConfig[modid];
+		// delete BattleTeambuilderTable[modid];
+		if (debug) {
+			console.log("WARNING: Failed to load config data for " + modid);
+			console.log("This was the error:");
+			console.log(err);
+		}
+	}
+}
 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}:`
-	));
+	let buf = JSON.stringify(obj);
+	buf = buf.replace(/\"([A-Za-z][A-Za-z0-9]*)\"\:/g, '$1:');
+	buf = buf.replace(/return\:/g, '"return":').replace(/new\:/g, '"new":').replace(/delete\:/g, '"delete":');
+	return buf;
 }
 
 function requireNoCache(pathSpec) {
@@ -37,12 +62,42 @@ function requireNoCache(pathSpec) {
 	return require(pathSpec);
 }
 
+process.stdout.write("Building `data/search-index.js`... ");
+buildSearchIndex();
+console.log("DONE");
+
+process.stdout.write("Building `data/formats.js`... ");
+buildFormats();
+console.log("DONE");
+
+process.stdout.write("Building `data/teambuilder-tables.js`... ");
+buildTeambuilderTables();
+console.log("DONE");
+
+process.stdout.write("Building `data/pokedex.js`... ");
+buildPokedex();
+console.log("DONE");
+
+process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.js`...");
+buildMoves();
+buildItems();
+buildAbilities();
+buildTypechart();
+buildLearnsets();
+console.log("DONE");
+
+process.stdout.write("Building aliases, formats-data, mod-sprites, text.js...");
+buildAliases();
+buildFormatsData();
+buildModSprites();
+buildText();
+console.log("DONE");
+
 /*********************************************************
  * Build search-index.js
  *********************************************************/
 
-{
-	process.stdout.write("Building `data/search-index.js`... ");
+function buildSearchIndex() {
 
 	let index = [];
 
@@ -78,7 +133,9 @@ function requireNoCache(pathSpec) {
 	index.push('moves article');
 
 	// generate aliases
+	const usedIDs = [];
 	function generateAlias(id, name, type) {
+		usedIDs.push(id);
 		let i = name.lastIndexOf(' ');
 		if (i < 0) i = name.lastIndexOf('-');
 		if (name.endsWith('-Mega-X') || name.endsWith('-Mega-Y')) {
@@ -224,6 +281,31 @@ function requireNoCache(pathSpec) {
 		generateAlias(id, name, 'ability');
 	}
 
+	const infoTable = {Pokedex: 'pokemon', Moves: 'move', Items: 'item', Abilities: 'ability', TypeChart: 'type'};
+	for (const modid in ModConfig) {
+		for (const key in infoTable) {
+			try {
+				const modDex = Dex.mod(modid);
+				if (!modDex || !modDex.data || !modDex.data[key]) continue;
+				for (let id in modDex.data[key]) {
+					if (!modDex.data[key][id]) continue;
+					if (!usedIDs.includes(toID(id)) && id in Dex.data.TypeChart === false) {
+						index.push(toID(id) + ' ' + infoTable[key]);
+						usedIDs.push(toID(id));
+						const name = modDex.data[key][id].name;
+						if (name) generateAlias(id, name, infoTable[key]);
+					}
+				}
+			} catch(err) {
+				if (debug) {
+					console.log("WARNING: Failed to load search data for " + modid);
+					console.log("This was the error:");
+					console.log(err);
+				}
+			}
+		}
+	}
+
 	index.sort();
 
 	// manually rearrange
@@ -303,15 +385,11 @@ function requireNoCache(pathSpec) {
 	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`... ");
-
-{
+function buildTeambuilderTables() {
 	const BattleTeambuilderTable = {};
 
 	let buf = '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n';
@@ -424,6 +502,8 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 				}
 				if (isVGC) {
 					if (species.isNonstandard && species.isNonstandard !== 'Gigantamax') return 'Illegal';
+					// these are breaking certain mods, disable them for now.
+
 					if (baseSpecies.tags.includes('Mythical')) return 'Mythical';
 					if (baseSpecies.tags.includes('Restricted Legendary')) return 'Restricted Legendary';
 					if (species.tier === 'NFE') return 'NFE';
@@ -650,8 +730,6 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 		const specificItems = [['header', "Pok&eacute;mon-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) {
@@ -838,10 +916,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 			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")) {
+					item.name.endsWith(" Sweet") || item.name.endsWith(" Apple") || item.name.startsWith("TR")) {
 					badItems.push(id);
 				} else if (item.name.endsWith(" Gem") && item.name !== "Normal Gem") {
 					if (genNum >= 6) {
@@ -851,18 +926,29 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 					} 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) {
+				} else if (item.name.endsWith(" Drive") || item.name.endsWith(" Memory")
+					|| item.name.startsWith("Rusted") || item.itemUser|| item.megaStone) {
 					specificItems.push(id);
 				} else {
-					goodItems.push(id);
+					switch (item.rating) {
+					case 3:
+						greatItems.push(id);
+						break;
+					case 2:
+						goodItems.push(id);
+						break;
+					// outclassed items
+					case 1:
+						poorItems.push(id);
+						break;
+					// Fling-only
+					case 0:
+						badItems.push(id);
+						break;
+					default:
+						goodItems.push(id);
+						break;
+					}
 				}
 			}
 		}
@@ -871,7 +957,6 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 		items.push(...specificItems);
 		items.push(...poorItems);
 		items.push(...badItems);
-		items.push(...unreleasedItems);
 	}
 
 	//
@@ -881,6 +966,49 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 	const gen3HMs = new Set(['cut', 'fly', 'surf', 'strength', 'flash', 'rocksmash', 'waterfall', 'dive']);
 	const gen4HMs = new Set(['cut', 'fly', 'surf', 'strength', 'rocksmash', 'waterfall', 'rockclimb']);
 
+	function getLearnsetStr(moveid, learnMoveArr, pokemonid = "", modid = "") {
+		let learnsetStr = '';
+		if (!learnMoveArr || !learnMoveArr.map) {
+			return "";
+		}
+		const gens = learnMoveArr.map(x => Number(x[0]));
+		const minGen = Math.min(...gens);
+		const vcOnly = (minGen === 7 && learnMoveArr.every(x => x[0] !== '7' || x === '7V') ||
+			minGen === 8 && learnMoveArr.every(x => x[0] !== '8' || x === '8V') ||
+			minGen === 9 && learnMoveArr.every(x => x[0] !== '9' || x === '9V'));
+
+      if (minGen <= 4 && minGen > 2 && (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);
+
+			let minUpperGen = available ? 5 : Math.min(
+				...gens.filter(gen => gen > 4)
+			);
+			legalGens += '0123456789'.slice(minUpperGen);
+			learnsetStr = legalGens;
+		} else {
+			learnsetStr = '0123456789'.slice(minGen);
+		}
+
+		if (gens.indexOf(6) >= 0) learnsetStr += 'p';
+		if (gens.indexOf(7) >= 0 && !vcOnly) learnsetStr += 'q';
+		if (gens.indexOf(8) >= 0 && !vcOnly) learnsetStr += 'g';
+		if (gens.indexOf(9) >= 0 && !vcOnly) learnsetStr += 'a';
+		return learnsetStr;
+	}
+
 	const learnsets = {};
 	BattleTeambuilderTable.learnsets = learnsets;
 	for (const id in Dex.data.Learnsets) {
@@ -1114,9 +1242,9 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 
 	// Client relevant data that should be overriden 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 overrideMoveKeys = ['accuracy', 'basePower', 'category', 'desc', 'flags', 'isNonstandard', 'noSketch', 'pp', 'priority', 'shortDesc', 'target', 'type', 'viable'];
 	const overrideAbilityKeys = ['desc', 'flags', 'isNonstandard', 'rating', 'shortDesc'];
-	const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'naturalGift', 'shortDesc'];
+	const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'rating', 'shortDesc'];
 
 	//
 	// Past gen table
@@ -1143,16 +1271,53 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 			}
 		}
 
-		const overrideMoveData = {};
-		BattleTeambuilderTable[gen].overrideMoveData = overrideMoveData;
+		const overrideDexInfo = {};
+		BattleTeambuilderTable[gen].overrideDexInfo = overrideDexInfo;
+		for (const id in genData.Pokedex) {
+			const modEntry = genData.Pokedex[id];
+			const baseEntry = Dex.data.Pokedex[id];
+			if (typeof baseEntry === 'undefined') {
+				overrideDexInfo[id] = {};
+				overrideDexInfo[id] = modEntry;
+				overrideDexInfo[id].exists = true;
+				continue;
+			}
+			for (const key in modEntry) {
+				const modString = JSON.stringify(modEntry[key]);
+				const baseString = JSON.stringify(baseEntry[key]);
+				if (modString !== baseString) {
+					if (!overrideDexInfo[id]) overrideDexInfo[id] = {};
+					if (modString === undefined) overrideDexInfo[id][key] = null;
+					else try {
+						overrideDexInfo[id][key] = JSON.parse(modString);
+					} catch (e) {
+						// Vivillon-Fancy coded with intentional undefined fields in the source, so we'll escape it
+						if (id === 'vivillonfancy') continue;
+						console.log(gen + " " + id + " " + key + " parsed an invalid value: " + modString);
+						continue;
+					}
+				}
+			}
+		}
+		const overrideMoveInfo = {};
+		BattleTeambuilderTable[gen].overrideMoveInfo = overrideMoveInfo;
 		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 modEntry = genData.Moves[id];
+			const baseEntry = Dex.data.Moves[id];
+			if (typeof baseEntry === 'undefined') {
+				overrideMoveInfo[id] = {};
+				overrideMoveInfo[id] = modEntry;
+				overrideMoveInfo[id].exists = true;
+				continue;
+			}
+			for (const key in modEntry) {
+				if (!overrideMoveKeys.includes(key)) continue;
+				const modString = JSON.stringify(modEntry[key]);
+				const baseString = JSON.stringify(baseEntry[key]);
+				if (modString !== baseString) {
+					if (!overrideMoveInfo[id]) overrideMoveInfo[id] = {};
+					//if (modString === undefined) overrideMoveInfo[id][key] = null;
+					overrideMoveInfo[id][key] = JSON.parse(modString);
 				}
 			}
 		}
@@ -1176,9 +1341,13 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 			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])) {
+				const curString = JSON.stringify(curEntry[key]);
+				const nextString = JSON.stringify(nextEntry[key]);
+				if (curString !== nextString) {
 					if (!overrideItemData[id]) overrideItemData[id] = {};
-					overrideItemData[id][key] = curEntry[key];
+					if (curString === undefined) overrideItemData[id][key] = null;
+					else overrideItemData[id][key] = JSON.parse(curString);
+					if(key === 'desc' && !curEntry['shortDesc']) overrideItemData[id]['shortDesc'] = JSON.parse(curString);
 				}
 			}
 		}
@@ -1190,7 +1359,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 		for (const id in nextGenData.TypeChart) {
 			const curEntry = genData.TypeChart[id];
 			const nextEntry = nextGenData.TypeChart[id];
-			if (curEntry.isNonstandard) {
+			if (!curEntry) {
 				removeType[id] = true;
 				continue;
 			}
@@ -1199,9 +1368,384 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 			}
 		}
 	}
+	const sliceTiers = ["OU", "AG", "Uber", "UU", "(UU)", "RU", "NU", "(NU)", "PU", "(PU)", "ZUBL", "ZU", "NFE", "LC", "DOU", "DUU", "(DUU)", "New", "Legal", "Regular", "Restricted Legendary", "CAP LC"];
+	function buildTiers(modid, tierTable, tiers, customTiers, formatSlices, tierOrder) {
+		for (const tier of tierOrder) {
+			if (ModConfig[modid].excludeStandardTiers) break;
+			if (sliceTiers.includes(tier)) {
+				let usedTier = tier;
+				if (usedTier === "(UU)") usedTier = "RU";
+				if (usedTier === "(NU)") usedTier = "PU";
+				if (usedTier === "(PU)") usedTier = "ZU";
+				if (usedTier === "(DUU)") usedTier = "DNU";
+				formatSlices[usedTier] = tiers.length;
+			}
+			if (!tierTable[tier]) continue;
+			tiers.push(['header', tier]);
+			tiers.push(...tierTable[tier]);
+		}
+		for (const tier of ModConfig[modid].customTiers) {
+			if (!tierTable[tier]) continue;
+			customTiers.push(['header', tier]);
+			customTiers.push(...tierTable[tier]);
+		}
+	}
+	for (const modConfigId of Object.keys(ModConfig)) {
+		if (BattleTeambuilderTable[modConfigId]) continue;
+		try { // catch loading errors so the whole thing doesn't break if one mod does
+			// Gather data about formats
+			const modDex = Dex.mod(modConfigId);
+			const modData = modDex.data;
+			if (modData.Scripts.teambuilderConfig) ModConfig[modConfigId] = modData.Scripts.teambuilderConfig;
+			let modGen = modDex.gen;
+			const petModFormats = {};
+			ModConfig[modConfigId].formats = petModFormats;
+			// Find formats using this mod
+			let hasSinglesFormats = false;
+			let hasDoublesFormats = false;
+			for (const i in Dex.formats.formatsListCache) {
+				// const format = Dex.formats.get(i);
+				// const formatMod = toID(format.mod);
+				const formatCacheMod = toID(Dex.formats.formatsListCache[i].mod);
+				const formatCacheName = toID(Dex.formats.formatsListCache[i].name);
+				if (formatCacheMod !== modConfigId) continue;
+				// petModFormats[formatCacheMod] = Formats.find(m => m.mod === modConfigId);
+				petModFormats[formatCacheName] = Formats.find(m => toID(m.name) === formatCacheName);
+				const format = petModFormats[formatCacheName];
+				let maxGen = 
+				formatCacheName.substr(0, 3) === 'gen' && parseInt(formatCacheMod.substr(3, 1)) < 10 ? parseInt(formatCacheMod.substr(3, 1)) : 9;
+				if(format.gameType === 'doubles') hasDoublesFormats = true;
+				else hasSinglesFormats = true;
+				// format.name = formatCacheMod;
+				if (!format.banlist) format.banlist = [];
+				if (!format.unbanlist) format.unbanlist = [];
+				if (!format.teambuilderFormat) format.teambuilderFormat = '';
+				format.bans = [];
+				format.unbans = [];
+				if (!!format.banlist) {
+					for (const name of format.banlist) {
+						let id = toID(name);
+						if(name.endsWith('-Base')) id = id.substr(0, id.length - 4);
+						if (id in Dex.data.Pokedex || id in modData.Pokedex) format.bans.push(id);
+						const formes = modData.Pokedex[id]?.otherFormes;
+						if(formes && toID(name) === id){ //id wasn't modified for base forme ban, therefore all formes are banned
+							for(const forme of formes){
+								const formeid = toID(forme);
+								if (formeid in Dex.data.Pokedex || formeid in modData.Pokedex) format.bans.push(formeid);
+							}
+						}
+					}
+					if (!!format.banlist && format.banlist.includes("All Pokemon")) {
+						format.bans.push('All Pokemon');
+						for (const name of format.unbanlist) {
+							let id = toID(name);
+							if(name.endsWith('-Base')) id = id.substr(0, id.length - 4);
+							if (id in Dex.data.Pokedex || id in modData.Pokedex) format.unbans.push(id);
+							const formes = modData.Pokedex[id]?.otherFormes;
+							if(formes && toID(name) === id){ //id wasn't modified for base forme ban, therefore all formes are banned
+								for(const forme of formes){
+									const formeid = toID(forme);
+									if (formeid in Dex.data.Pokedex || formeid in modData.Pokedex) format.unbans.push(formeid);
+								}
+							}
+						}
+					}
+				}
+				if (formatCacheMod.startsWith('gen') && maxGen < Number(formatCacheMod.charAt(3))) maxGen = Number(formatCacheMod.charAt(3));
+				if (maxGen !== 9) modGen = maxGen;
+			}
+			// Find any nonstandard tiers used by this mod
+			const standardTiers = ['uber', 'ou', 'uubl', 'uu', 'rubl', 'ru', 'nubl', 'nu', 'publ',
+				'pu', 'zu', 'zubl', 'nfe', 'lcuber', 'lc', 'cap', 'caplc', 'capnfe', 'ag', 'duber',
+				'dou', 'dbl', 'duu', 'dnu', 'illegal', 'unreleased'];
+			if (!ModConfig[modConfigId].customTiers) ModConfig[modConfigId].customTiers = [];
+			if (!ModConfig[modConfigId].customDoublesTiers) ModConfig[modConfigId].customDoublesTiers = [];
+			for (const speciesid in modData.FormatsData) {
+				if (!modData.FormatsData[speciesid]) {
+					if(debug && !(speciesid in Dex.data.Pokedex)) console.log('Warning: ' + speciesid + ' does not have tiering data in ' + modConfigId);
+					continue;
+				}
+				const tier = modData.FormatsData[speciesid].tier;
+				const doublesTier = modData.FormatsData[speciesid].doublesTier;
+				if (tier === undefined && doublesTier === undefined) continue;
+				if (!standardTiers.includes(toID(tier)) &&
+					!ModConfig[modConfigId].customTiers.includes(tier)) {
+					ModConfig[modConfigId].customTiers.push(tier);
+				}
+				if (!standardTiers.includes(toID(doublesTier)) &&
+					!ModConfig[modConfigId].customDoublesTiers.includes(doublesTier)) {
+					ModConfig[modConfigId].customDoublesTiers.push(doublesTier);
+				}
+			}
+			BattleTeambuilderTable[modConfigId] = {};
+			//tiers
+			const tierTable = {};
+			const pokemon = Object.keys(modData.Pokedex);
+			pokemon.sort();
+			const overrideTier = {};
+			if(hasSinglesFormats) {
+				BattleTeambuilderTable[modConfigId].overrideTier = overrideTier;
+			}
+			const doublesOverrideTier = {};
+			if (hasDoublesFormats) {
+				BattleTeambuilderTable[modConfigId].doubles = {};
+				BattleTeambuilderTable[modConfigId].doubles.overrideTier = doublesOverrideTier;
+			}
+			for (const id of pokemon) {
+				const species = modDex.species.get(id);
+				const tier = species.tier;
+				overrideTier[species.id] = tier;
+				if (species.forme && JSON.stringify(modData.Pokedex[species.id]) === JSON.stringify(Dex.data.Pokedex[species.id]) && !ModConfig.showAllFormes) { // hardcode from main
+				if (
+						[
+							'Aegislash', 'Castform', 'Cherrim', 'Cramorant', 'Eiscue', 'Meloetta', 'Mimikyu', 'Minior', 'Morpeko', 'Wishiwashi',
+						].includes(species.baseSpecies) || species.forme.includes('Totem') || species.forme.includes('Zen')
+					) {
+						continue;
+					}
+				}
+				if (!tierTable[tier]) tierTable[tier] = [];
+				tierTable[tier].push(id);
+				if (hasDoublesFormats) {
+					const doublesTier = species.doublesTier;
+					doublesOverrideTier[species.id] = species.doublesTier;
+					if (!tierTable[doublesTier]) tierTable[doublesTier] = [];
+					if (tier !== doublesTier) tierTable[doublesTier].push(id);
+				}
+			}
+			for (const tier in tierTable) {
+				for (const formatid in petModFormats) {
+					const banlist = petModFormats[formatid].banlist;
+					if (!!banlist.includes(tier) || !!banlist.includes(toID(tier))) {
+						for (const i in tierTable[tier]) {
+							const speciesid = tierTable[tier][i];
+							if (!petModFormats[formatid].banlist.includes(speciesid)) petModFormats[formatid].bans.push(speciesid);
+						}
+					}
+				}
+			}
+			const tiers = [];
+			BattleTeambuilderTable[modConfigId].tiers = tiers;
+			const doublesTiers = [];
+			if (hasDoublesFormats) BattleTeambuilderTable[modConfigId].doubles.tiers = doublesTiers;
+			const customTiers = [];
+			BattleTeambuilderTable[modConfigId].customTiers = customTiers;
+			const customDoublesTiers = [];
+			if (hasDoublesFormats) BattleTeambuilderTable[modConfigId].doubles.customTiers = customDoublesTiers;
+			const formatSlices = {};
+			BattleTeambuilderTable[modConfigId].formatSlices = formatSlices;
+			const doublesFormatSlices = {};
+			if (hasDoublesFormats) BattleTeambuilderTable[modConfigId].doubles.formatSlices = doublesFormatSlices;
+			const tierOrder = ["OU", "AG", "Uber", "(Uber)", "OU", "(OU)", "UUBL", "UU", "RUBL", "RU", "NUBL", "NU", "PUBL", "PU", "(PU)", "ZUBL", "ZU", "New", "NFE", "LC Uber", "LC", "Unreleased"];
+			const tierOrderDoubles = ["DUber", "(DUber)", "DOU", "DBL", "(DOU)", "DUU", "(DUU)", "New", "NFE", "LC Uber", "LC"];
+			if (hasSinglesFormats) buildTiers(modConfigId, tierTable, tiers, customTiers, formatSlices, tierOrder);
+			if (hasDoublesFormats) buildTiers(modConfigId, tierTable, doublesTiers, customDoublesTiers, doublesFormatSlices, tierOrderDoubles);
+			//pokemon stats
+			const overrideDexInfo = {};
+			BattleTeambuilderTable[modConfigId].overrideDexInfo = overrideDexInfo;
+			for (const id in modData.Pokedex) {
+				const modEntry = modData.Pokedex[id];
+				const baseEntry = Dex.data.Pokedex[id];
+				if (typeof baseEntry === 'undefined') {
+					overrideDexInfo[id] = {};
+					overrideDexInfo[id] = modEntry;
+					overrideDexInfo[id].exists = true;
+					continue;
+				}
+				for (const key in modEntry) {
+					const modString = JSON.stringify(modEntry[key]);
+					const baseString = JSON.stringify(baseEntry[key]);
+					if (modString !== baseString) {
+						if (!overrideDexInfo[id]) overrideDexInfo[id] = {};
+						if (modString === undefined) overrideDexInfo[id][key] = undefined;
+						else overrideDexInfo[id][key] = JSON.parse(modString);
+					}
+				}
+			}
+			//learnsets
+			const overrideLearnsets = {};
+			BattleTeambuilderTable[modConfigId].overrideLearnsets = overrideLearnsets;
+			for (const id in modDex.data.Learnsets) {
+				const learnset = modDex.data.Learnsets[id].learnset;
+				const baseLearnset = Dex.data.Learnsets[id] ? Dex.data.Learnsets[id].learnset : {};
+				if (!learnset) continue;
+				if (!learnsets[id]) learnsets[id] = {};
+				for (const moveid in learnset) {
+					const newLearnsetEntry = getLearnsetStr(moveid, learnset[moveid], id, modConfigId);
+					let baseLearnsetEntry = learnsets[id][moveid];
+					if (modGen <= 2 && G2Learnsets[id]) baseLearnsetEntry = G2Learnsets[id][moveid];
+					const baseLsetNoEarlyGen = baseLearnsetEntry ? baseLearnsetEntry.replace(1, '').replace(2, '') : '';
+					if (newLearnsetEntry !== baseLearnsetEntry && newLearnsetEntry !== baseLsetNoEarlyGen) {
+						if (!overrideLearnsets[id]) overrideLearnsets[id] = {};
+						overrideLearnsets[id][moveid] = newLearnsetEntry;
+					}
+				}
+				for (const moveid in baseLearnset) {
+					if (baseLearnset[moveid] && !learnset[moveid]) {
+						if (!overrideLearnsets[id]) overrideLearnsets[id] = {};
+						overrideLearnsets[id][moveid] = 'r';
+					}
+				}
+			}
+			//items			
+			let items = [];
+			const fullItemName = {};
+			BattleTeambuilderTable[modConfigId].fullItemName = fullItemName;
+			BattleTeambuilderTable[modConfigId].items = items;
+			const overrideItemInfo = {};
+			BattleTeambuilderTable[modConfigId].overrideItemData = overrideItemInfo;
+
+			const greatItems = [];
+			const goodItems = [];
+			const specificItems = [];
+			const poorItems = [];
+			const badItems = [];
+			for (const id in modData.Items) {
+				const modEntry = modData.Items[id];
+				const baseEntry = Dex.data.Items[id];
+				if (typeof baseEntry === 'undefined') {
+					overrideItemInfo[id] = {};
+					overrideItemInfo[id] = modEntry;
+					overrideItemInfo[id].exists = true;
+					fullItemName[id] = modData.Items[id].name;
+				} else {
+					if(baseEntry.gen > modGen) continue;
+					for (const key in modEntry) {
+						if (!overrideItemKeys.includes(key)) continue;
+						const modString = JSON.stringify(modEntry[key]);
+						const baseString = JSON.stringify(baseEntry[key]);
+						if (modString !== baseString) {
+							if (!overrideItemInfo[id]) overrideItemInfo[id] = {};
+							overrideItemInfo[id][key] = JSON.parse(modString);
+							if(key === 'desc' && !modEntry['shortDesc']) overrideItemInfo[id]['shortDesc'] = JSON.parse(modString);
+						}
+					}
+				}
+				
+				if (modEntry.itemUser || modEntry.megaStone || id === 'boosterenergy') {
+					specificItems.push(id);
+					continue;
+				}
+				if (modEntry.isPokeball || modEntry.name.startsWith("TR")) {
+					badItems.push(id);
+					continue;
+				}
+				switch (modEntry.rating) {
+				case 3:
+					greatItems.push(id);
+					break;
+				case 2:
+					goodItems.push(id);
+					break;
+				// outclassed items
+				case 1:
+					poorItems.push(id);
+					break;
+				// Fling-only
+				case 0:
+					badItems.push(id);
+					break;
+				// Allows mods to manually set Pokemon-specific items
+				case -1:
+					specificItems.push(id);
+					break;
+				default:
+					goodItems.push(id);
+				}
+			}
+			greatItems.sort();
+			greatItems.unshift(['header', "Popular items"]);
+			items.push(...greatItems);
+			goodItems.sort();
+			goodItems.unshift(['header', "Items"]);
+			items.push(...goodItems);
+			specificItems.sort();
+			specificItems.unshift(['header', "Pok&eacute;mon-specific items"]);
+			items.push(...specificItems);
+			poorItems.sort();
+			poorItems.unshift(['header', "Usually useless items"]);
+			items.push(...poorItems);
+			badItems.sort();
+			badItems.unshift(['header', "Useless items"]);
+			items.push(...badItems);
+			//moves
+			const overrideMoveInfo = {};
+			BattleTeambuilderTable[modConfigId].overrideMoveInfo = overrideMoveInfo;
+			for (const id in modData.Moves) {
+				const modEntry = modData.Moves[id];
+				const baseEntry = Dex.data.Moves[id];
+				const genEntry = modGen !== 9 ? Dex.mod('gen'+modGen).data.Moves[id] : null;
+				if (typeof baseEntry === 'undefined') {
+					overrideMoveInfo[id] = {};
+					overrideMoveInfo[id] = modEntry;
+					overrideMoveInfo[id].exists = true;
+					continue;
+				}
+				let moddedMoveIsOldGen = true;
+				for (const key in modEntry) {
+					if (!overrideMoveKeys.includes(key)) continue;
+					const modString = JSON.stringify(modEntry[key]);
+					const baseString = JSON.stringify(baseEntry[key]);
+					if (modString !== baseString) {
+						if (genEntry && moddedMoveIsOldGen) {
+							const genString = JSON.stringify(genEntry[key]);
+							if (genString !== modString) {
+								moddedMoveIsOldGen = false;
+							}
+						}
+						if (!overrideMoveInfo[id]) overrideMoveInfo[id] = {};
+						if (modString === undefined) overrideMoveInfo[id][key] = undefined;
+						else overrideMoveInfo[id][key] = JSON.parse(modString);
+					}
+				}
+				if (overrideMoveInfo[id] && moddedMoveIsOldGen) overrideMoveInfo[id].modMoveFromOldGen = true;
+			}
+			for(const id in Dex.data.Moves){ //Weed out nulled moves
+				const modEntry = modData.Moves[id];
+				if(!modEntry){
+					overrideMoveInfo[id] = {};
+					overrideMoveInfo[id].isNonstandard = "Unobtainable";
+					overrideMoveInfo[id].exists = false;
+					continue;
+				}
+			}
+			//abilities
+			const fullAbilityName = {};
+			BattleTeambuilderTable[modConfigId].fullAbilityName = fullAbilityName;
+			const overrideAbilityDesc = {};
+			BattleTeambuilderTable[modConfigId].overrideAbilityDesc = overrideAbilityDesc;
+			for (const id in modData.Abilities) {
+				const modEntry = modData.Abilities[id];
+				const baseEntry = Dex.data.Abilities[id];
+				const fakeAbility = (typeof baseEntry === 'undefined');
+				if (fakeAbility) fullAbilityName[id] = modData.Abilities[id].name;
+				if (fakeAbility || (modEntry.shortDesc || modEntry.desc) !== (baseEntry.shortDesc || baseEntry.desc)) {
+					overrideAbilityDesc[id] = (modEntry.shortDesc || modEntry.desc);
+				}
+			}
+			//type chart
+			const overrideTypeChart = {};
+			BattleTeambuilderTable[modConfigId].overrideTypeChart = overrideTypeChart;
+			for (const id in modData.TypeChart) {
+				const modEntry = modData.TypeChart[id];
+				const baseEntry = Dex.data.TypeChart[id];
+				if (JSON.stringify(modEntry) !== JSON.stringify(baseEntry)) {
+					overrideTypeChart[id] = modEntry;
+				}
+			}
+		} catch (err) {
+			delete ModConfig[modConfigId];
+			delete BattleTeambuilderTable[modConfigId];
+			if (debug) {
+				console.log("WARNING: Failed to load " + modConfigId);
+				console.log("This was the error:");
+				console.log(err);
+			}
+		}
+	}
 
 	//
-	// Mods
+	// (not pet) Mods
 	//
 
 	for (const mod of ['gen5bw1', 'gen7letsgo', 'gen8bdsp', 'gen9ssb']) {
@@ -1222,7 +1766,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 			}
 		}
 
-		const overrideMoveData = {};
+		var overrideMoveData = {};
 		BattleTeambuilderTable[mod].overrideMoveData = overrideMoveData;
 		for (const id in modData.Moves) {
 			const modEntry = modDex.moves.get(id);
@@ -1236,7 +1780,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 			}
 		}
 
-		const overrideAbilityData = {};
+		var overrideAbilityData = {};
 		BattleTeambuilderTable[mod].overrideAbilityData = overrideAbilityData;
 		for (const id in modData.Abilities) {
 			const modEntry = modDex.abilities.get(id);
@@ -1250,7 +1794,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 		}
 
 		const overrideItemData = {};
-		BattleTeambuilderTable[mod].overrideItemData = overrideItemData;
+		BattleTeambuilderTable[mod].overrideItemInfo = overrideItemData;
 		for (const id in modData.Items) {
 			const modEntry = modDex.items.get(id);
 			const parentEntry = parentDex.items.get(id);
@@ -1263,21 +1807,65 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
 		}
 	}
 
-	buf += `exports.BattleTeambuilderTable = JSON.parse('${JSON.stringify(BattleTeambuilderTable).replace(/['\\]/g, "\\$&")}');\n\n`;
+	/*
+
+	This block of code is a bit wild, I might not be able to help with it too much.
+
+	Essentially it breaks down everything in BattleTeambuilderTable into blocks of 5,
+	casts them to a JSON object and then un-JSONs them by deleting the curly braces
+	that surround them. It =manually parses the edges of the entire JSON file so that the whole
+	thing is treated as a single JSON object with sub-objects to be loaded by the teambuilder.
+
+	*/
+
+	const tableKeys = Object.keys(BattleTeambuilderTable);
+	const blockSize = 5;
+	let blockIndex = 0;
+
+	console.log("writing BattleTeambuilderTable...");
+	const tableDir = 'play.pokemonshowdown.com/data/teambuilder-tables.js';
+	fs.writeFileSync(tableDir, '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n');
+	fs.appendFileSync(tableDir, 'exports.BattleTeambuilderTable = JSON.parse(\'{')
+
+	// ChatGPT suggested to put BattleTeambuilderTable into blocks to prevent memory overload.
+	while (blockIndex < tableKeys.length) {
+		const block = tableKeys.slice(blockIndex, blockIndex + blockSize);
+		const blockTable = {};
+		block.forEach(key => {
+			blockTable[key] = BattleTeambuilderTable[key];
+		});
+		var jsonString = JSON.stringify(blockTable).replace(/['\\]/g, "\\$&");
+		jsonString = jsonString.substring(1, jsonString.length - 1);
+		fs.appendFileSync(tableDir, jsonString);
+		blockIndex += blockSize;
+		if (blockIndex < tableKeys.length) fs.appendFileSync(tableDir, ',');
+	}
+	fs.appendFileSync(tableDir, '}\');\n\n');
+	console.log("DONE");
+
+	console.log("writing compressed data/teambuilder-tables.js.gz...");
+
+	var zlib = require('zlib');
+	var gzip = zlib.createGzip();
+	var tbtJs = fs.createReadStream(tableDir);
+	var tbtGz = fs.createWriteStream(tableDir + '.gz');
+	tbtJs.pipe(gzip).pipe(tbtGz);
+
+	console.log("DONE");
 
-	fs.writeFileSync('play.pokemonshowdown.com/data/teambuilder-tables.js', buf);
+	console.log("writing ModConfig...");
+	fs.writeFileSync('play.pokemonshowdown.com/data/mod-config.js', 'exports.ModConfig = ' + JSON.stringify(ModConfig) + ';\n\n');
+	console.log("DONE");
 }
 
-console.log("DONE");
+
 
 /*********************************************************
  * Build pokedex.js
  *********************************************************/
 
-process.stdout.write("Building `data/pokedex.js`... ");
-
-{
-	const Pokedex = requireNoCache('../caches/pokemon-showdown/dist/data/pokedex.js').Pokedex;
+function buildPokedex() {
+	const Pokedex = requireNoCache('../caches/DH2/dist/data/pokedex.js').Pokedex;
 	for (const id in Pokedex) {
 		const entry = Pokedex[id];
 		if (Dex.data.FormatsData[id]) {
@@ -1293,16 +1881,12 @@ process.stdout.write("Building `data/pokedex.js`... ");
 	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;
+function buildMoves() {
+	const Moves = requireNoCache('../caches/DH2/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;
@@ -1318,8 +1902,8 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build items.js
  *********************************************************/
 
-{
-	const Items = requireNoCache('../caches/pokemon-showdown/dist/data/items.js').Items;
+function buildItems() {
+	const Items = requireNoCache('../caches/DH2/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;
@@ -1333,8 +1917,8 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build abilities.js
  *********************************************************/
 
-{
-	const Abilities = requireNoCache('../caches/pokemon-showdown/dist/data/abilities.js').Abilities;
+function buildAbilities() {
+	const Abilities = requireNoCache('../caches/DH2/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;
@@ -1348,8 +1932,8 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build typechart.js
  *********************************************************/
 
-{
-	const TypeChart = requireNoCache('../caches/pokemon-showdown/dist/data/typechart.js').TypeChart;
+function buildTypechart() {
+	const TypeChart = requireNoCache('../caches/DH2/dist/data/typechart.js').TypeChart;
 	const buf = 'exports.BattleTypeChart = ' + es3stringify(TypeChart) + ';';
 	fs.writeFileSync('play.pokemonshowdown.com/data/typechart.js', buf);
 }
@@ -1358,8 +1942,8 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build aliases.js
  *********************************************************/
 
-{
-	const Aliases = requireNoCache('../caches/pokemon-showdown/dist/data/aliases.js').Aliases;
+function buildAliases() {
+	const Aliases = requireNoCache('../caches/DH2/dist/data/aliases.js').Aliases;
 	const buf = 'exports.BattleAliases = ' + es3stringify(Aliases) + ';';
 	fs.writeFileSync('play.pokemonshowdown.com/data/aliases.js', buf);
 }
@@ -1368,8 +1952,8 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build formats-data.js
  *********************************************************/
 
-{
-	const FormatsData = requireNoCache('../caches/pokemon-showdown/dist/data/formats-data.js').FormatsData;
+function buildFormatsData() {
+	const FormatsData = requireNoCache('../caches/DH2/dist/data/formats-data.js').FormatsData;
 	const buf = 'exports.BattleFormatsData = ' + es3stringify(FormatsData) + ';';
 	fs.writeFileSync('play.pokemonshowdown.com/data/formats-data.js', buf);
 }
@@ -1378,8 +1962,8 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build formats.js
  *********************************************************/
 
-{
-	const Formats = requireNoCache('../caches/pokemon-showdown/dist/config/formats.js').Formats;
+function buildFormats() {
+	Formats = requireNoCache('../caches/DH2/dist/config/formats.js').Formats;
 	const buf = 'exports.Formats = ' + es3stringify(Formats) + ';';
 	fs.writeFileSync('play.pokemonshowdown.com/data/formats.js', buf);
 }
@@ -1388,18 +1972,46 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
  * Build learnsets.js
  *********************************************************/
 
-{
-	const Learnsets = requireNoCache('../caches/pokemon-showdown/dist/data/learnsets.js').Learnsets;
+function buildLearnsets() {
+	const Learnsets = requireNoCache('../caches/DH2/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 mod-sprites.js
+ *********************************************************/
+
+function buildModSprites() {
+	const modSprites = {};
+	const modDir = fs.readdirSync('caches/DH2/data/mods/');
+	for (const i in modDir) {
+		const modName = modDir[i];
+		const subFolders = ['anifront', 'anifront-shiny','aniback','aniback-shiny','front', 'front-shiny', 'back', 'back-shiny', 'icons', 'types', 'items', 'cries'];
+		for (const j in subFolders) {
+			const subF = subFolders[j];
+			const spritePath = 'caches/DH2/data/mods/' + modName + (subF === 'cries' ? '/audio/cries' : ('/sprites/' + subF));
+			const spriteDir = fs.existsSync(spritePath) ? fs.readdirSync(spritePath) : '';
+			for (const sprI in spriteDir) {
+				let id = spriteDir[sprI];
+				const ext = id.split(".")[1];
+				id = toID(id.slice(0, id.length - 4));
+				modSprites[id] ||= {};
+				modSprites[id][modName] ||= [];
+				modSprites[id][modName].push(subF);
+			}
+		}
+	}
+	const buf = 'exports.ModSprites = ' + es3stringify(modSprites) + ';';
+	fs.writeFileSync('play.pokemonshowdown.com/data/mod-sprites.js', buf);
+}
+
 /*********************************************************
  * Build text.js
  *********************************************************/
 
-{
+function buildText() {
 	const textData = Dex.loadTextData();
 	const Text = textData.Default;
 
@@ -1425,5 +2037,3 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
 	const buf = 'exports.BattleText = ' + es3stringify(Text) + ';';
 	fs.writeFileSync('play.pokemonshowdown.com/data/text.js', buf);
 }
-
-console.log("DONE");
diff --git a/build-tools/build-learnsets b/build-tools/build-learnsets
index 1e149dbe8..652015232 100755
--- a/build-tools/build-learnsets
+++ b/build-tools/build-learnsets
@@ -18,7 +18,7 @@ const thisFile = __filename;
 const thisDir = __dirname;
 const rootDir = path.resolve(thisDir, '../play.pokemonshowdown.com');
 
-const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex;
+const Dex = require('../caches/DH2/dist/sim/dex').Dex;
 const toID = Dex.toID;
 
 function updateLearnsets(callback) {
diff --git a/build-tools/build-minidex b/build-tools/build-minidex
index dd4cfce04..7db1f0052 100755
--- a/build-tools/build-minidex
+++ b/build-tools/build-minidex
@@ -6,7 +6,7 @@ const path = require("path");
 process.chdir(path.resolve(__dirname, '../play.pokemonshowdown.com'));
 const imageSize = require('image-size');
 
-const Dex = require('./../caches/pokemon-showdown/dist/sim/dex').Dex;
+const Dex = require('./../caches/DH2/dist/sim/dex').Dex;
 const toID = Dex.toID;
 
 process.stdout.write("Updating animated sprite dimensions... ");
diff --git a/build-tools/build-sets b/build-tools/build-sets
new file mode 100644
index 000000000..a9f44e00c
--- /dev/null
+++ b/build-tools/build-sets
@@ -0,0 +1,25 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require("fs");
+const child_process = require('child_process');
+const path = require("path");
+
+process.stdout.write("Importing sets from @smogon/sets... ");
+
+const shell = cmd => child_process.execSync(cmd, {stdio: 'inherit', cwd: path.resolve(__dirname, '..')});
+shell(`npm install --no-audit --no-save @smogon/sets`);
+
+const src = path.resolve(__dirname, '../node_modules/@smogon/sets');
+const dest = path.resolve(__dirname, '../data/sets');
+
+try {
+	fs.mkdirSync(dest);
+} catch (err) {
+	if (err.code !== 'EEXIST') throw err;
+}
+
+for (const file of fs.readdirSync(src)) {
+	if (!file.endsWith('.json')) continue;
+	fs.copyFileSync(`${src}/${file}`, `${dest}/${file}`);
+}
\ No newline at end of file
diff --git a/build-tools/server-repo b/build-tools/server-repo
new file mode 100644
index 000000000..9d9d89a81
--- /dev/null
+++ b/build-tools/server-repo
@@ -0,0 +1,2 @@
+const server_repo = "https://github.com/scoopapa/DH2.git";
+module.exports = server_repo;
\ No newline at end of file
diff --git a/build-tools/update b/build-tools/update
old mode 100755
new mode 100644
index 23d52a894..fb02e0f77
--- a/build-tools/update
+++ b/build-tools/update
@@ -55,6 +55,7 @@ Config.routes = {
 	dex: '${routes.dex}',
 	replays: '${routes.replays}',
 	users: '${routes.users}',
+	psmain: '${routes.psmain}',
 };
 ${AUTOCONFIG_END}`;
 
@@ -178,32 +179,7 @@ crossprotocolContents = crossprotocolContents.replace(URL_REGEX, addCachebuster)
 let replayEmbedContents = fs.readFileSync('play.pokemonshowdown.com/js/replay-embed.template.js', {encoding: 'utf8'});
 replayEmbedContents = replayEmbedContents.replace(/play\.pokemonshowdown\.com/g, routes.client);
 
-// add news, only if it's actually likely to exist
-process.stdout.write("and news... ");
-let stdout = '';
-let newsid = 0;
-let news = '[failed to retrieve news]';
-try {
-	stdout = child_process.execSync('php ' + path.resolve(thisDir, 'news-embed.php'));
-} catch (e) {
-	console.log("git hook failed to retrieve news (exec command failed):\n" + (e.error + e.stderr + e.stdout));
-}
-try {
-	if (stdout) [newsid, news] = JSON.parse(stdout);
-} catch (e) {
-	console.log("git hook failed to retrieve news (parsing JSON failed):\n" + e.stack);
-}
-
-indexContents = indexContents.replace(/<!-- newsid -->/g, newsid);
-indexContents = indexContents.replace(/<!-- news -->/g, news);
-
-let indexContents2 = '';
-try {
-	let indexContentsOld = indexContents;
-	indexContents = indexContents.replace(/<!-- head custom -->/g, '' + fs.readFileSync('config/head-custom.html'));
-	indexContents2 = indexContentsOld.replace(/<!-- head custom -->/g, '' + fs.readFileSync('config/head-custom-test.html'));
-	indexContents2 = indexContents2.replace(/src="\/\/play.pokemonshowdown.com\/config\/config.js\?[a-z0-9]*"/, 'src="//play.pokemonshowdown.com/config/config-test.js?4"');
-} catch (e) {}
+// Nobody cares about the news. Cheers.
 
 fs.writeFileSync('play.pokemonshowdown.com/index.html', indexContents);
 if (indexContents2) {
diff --git a/config/config.js b/config/config.js
new file mode 100644
index 000000000..8d59db77c
--- /dev/null
+++ b/config/config.js
@@ -0,0 +1,46 @@
+var Config = Config || {};
+
+/* version */ Config.version = "0";
+
+Config.bannedHosts = ['cool.jit.su', 'pokeball-nixonserver.rhcloud.com'];
+
+Config.whitelist = [
+	'wikipedia.org',
+
+	// The full list is maintained outside of this repository so changes to it
+	// don't clutter the commit log. Feel free to copy our list for your own
+	// purposes; it's here: https://play.pokemonshowdown.com/config/config.js
+
+	// If you would like to change our list, simply message Zarel on Smogon or
+	// Discord.
+];
+
+// `defaultserver` specifies the server to use when the domain name in the
+// address bar is `Config.routes.client`.
+Config.defaultserver = {
+	id: 'dragonheaven',
+	host: '191.101.232.116',
+	port: 8000,
+	httpport: 80,
+	altport: 80,
+	registered: true
+};
+
+Config.roomsFirstOpenScript = function () {
+};
+
+Config.customcolors = {
+	'zarel': 'aeo'
+};
+/*** Begin automatically generated configuration ***/
+Config.version = "0.11.2";
+
+Config.routes = {
+	root: 'petmodsdh.com',
+	client: 'localhost',
+	dex: 'dex.pokemonshowdown.com',
+	replays: 'replay.pokemonshowdown.com',
+	users: 'pokemonshowdown.com/users',
+	psmain: 'pokemonshowdown.com',
+};
+/*** End automatically generated configuration ***/
diff --git a/config/mod-config.js b/config/mod-config.js
new file mode 100644
index 000000000..f519c4d7c
--- /dev/null
+++ b/config/mod-config.js
@@ -0,0 +1,39 @@
+/* 
+optional data:
+customTiers - these are auto-detected by the script, but you can set them here to ensure they show up in the right order
+excludeStandardTiers - set to true if you want only your custom tiers to show up for the format
+*/
+const ModConfigData = {
+	ClientMods: {
+		// cleanslate: {
+		// 	excludeStandardTiers: true,
+		// },
+		// cleanslatemicro: {
+		// 	excludeStandardTiers: true,
+		// },
+		// csts: {
+		// 	customTiers: ['CS1', 'CSM', 'CS2'],
+		// 	excludeStandardTiers: true,
+		// },
+		// roulettemons: {
+		// 	excludeStandardTiers: true,
+		// },
+		// ccapm2020: {
+		// 	excludeStandardTiers: true,
+		// },
+		// fealpha: {
+		// 	excludeStandardTiers: true,
+		// },
+		// feuu: {
+		// 	excludeStandardTiers: true,
+		// },
+		// prism: {
+		// 	ignoreEVLimits: true,
+		// 	spriteGen: 2,
+		// },
+		// smashmodsmelee: {
+		// 	excludeStandardTiers: true,
+		// },
+	},
+};
+exports.ModConfigData = ModConfigData;
diff --git a/config/routes.json b/config/routes.json
index 69553f29a..7376f4a27 100644
--- a/config/routes.json
+++ b/config/routes.json
@@ -1,7 +1,8 @@
 {
-    "root": "pokemonshowdown.com",
-    "client": "play.pokemonshowdown.com",
+    "root": "191.101.232.116",
+    "client": "localhost",
     "dex": "dex.pokemonshowdown.com",
     "replays": "replay.pokemonshowdown.com",
-    "users": "pokemonshowdown.com/users"
-}
+    "users": "pokemonshowdown.com/users",
+	"psmain": "pokemonshowdown.com"
+}
\ No newline at end of file
diff --git a/config/testclient-key.js b/config/testclient-key.js
new file mode 100644
index 000000000..cd6630d03
--- /dev/null
+++ b/config/testclient-key.js
@@ -0,0 +1 @@
+const POKEMON_SHOWDOWN_TESTCLIENT_KEY = 'DH%20Client%2C60930894%2C2adcb851619e8a7f6475b9e41609257005';
diff --git a/package.json b/package.json
index 05c95aaf2..6ecf9010b 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,12 @@
   "dependencies": {
     "@babel/core": "^7.21.3",
     "@babel/plugin-proposal-class-properties": "^7.18.6",
+    "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7",
+    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
+    "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
+    "@babel/plugin-proposal-optional-catch-binding": "^7.18.6",
+    "@babel/plugin-proposal-optional-chaining": "^7.21.0",
+    "@babel/plugin-transform-logical-assignment-operators": "^7.22.5",
     "@babel/plugin-transform-react-jsx": "^7.21.0",
     "@babel/preset-env": "^7.20.2",
     "@babel/preset-typescript": "^7.21.0",
diff --git a/play.pokemonshowdown.com/config/testclient-key.js b/play.pokemonshowdown.com/config/testclient-key.js
new file mode 100644
index 000000000..cd6630d03
--- /dev/null
+++ b/play.pokemonshowdown.com/config/testclient-key.js
@@ -0,0 +1 @@
+const POKEMON_SHOWDOWN_TESTCLIENT_KEY = 'DH%20Client%2C60930894%2C2adcb851619e8a7f6475b9e41609257005';
diff --git a/play.pokemonshowdown.com/index.template.html b/play.pokemonshowdown.com/index.template.html
index 5258ad886..d670c9af4 100644
--- a/play.pokemonshowdown.com/index.template.html
+++ b/play.pokemonshowdown.com/index.template.html
@@ -1,136 +1,134 @@
 <!DOCTYPE html>
-<!--
-           .............
-       ,...................
-     ,..................========
-    ....~=##############=======+
-   ...##################=======.
-  ..=######+...,    +##=======,..
-  ..=###### ., .....  +=====+,....
-     ###########~,..  ======
-  .....##############~=====+....., pokemonshowdown.com
-  ..........###########====......
-    ............#######,==......
-  =###.,........+#####+ .......
-  ####################~......,
-  #################+======,
-     ++++++++++++ =======+
-
-Viewing source? We're open source! Check us out on GitHub!
-https://github.com/Zarel/Pokemon-Showdown
-https://github.com/Zarel/Pokemon-Showdown-Client (you are here)
-
-Also visit us in the Dev chatroom:
-https://psim.us/dev
-
--->
-<meta charset="UTF-8" />
-<meta id="viewport" name="viewport" content="width=device-width" />
-<title>Showdown!</title>
-<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
-<link rel="shortcut icon" href="//play.pokemonshowdown.com/favicon.ico" id="dynamic-favicon" />
-<link rel="icon" sizes="256x256" href="//play.pokemonshowdown.com/favicon-256.png" />
-<link rel="stylesheet" href="//play.pokemonshowdown.com/style/battle.css?" />
-<link rel="stylesheet" href="//play.pokemonshowdown.com/style/client.css?" />
-<link rel="stylesheet" href="//play.pokemonshowdown.com/style/sim-types.css?" />
-<link rel="stylesheet" href="//play.pokemonshowdown.com/style/utilichart.css?" />
-<link rel="stylesheet" href="//play.pokemonshowdown.com/style/font-awesome.css?" />
-<meta name="apple-mobile-web-app-capable" content="yes" />
-<link rel="manifest" href="/manifest.json" />
-<!--[if lte IE 8]><script>document.location.replace('http://pokemonshowdown.com/autodownload/win');</script><![endif]-->
-
-<!-- head custom -->
-
-<div id="header" class="header">
-	<img class="logo" src="//play.pokemonshowdown.com/pokemonshowdownbeta.png" srcset="//play.pokemonshowdown.com/pokemonshowdownbeta@2x.png 2x" alt="Pok&eacute;mon Showdown! (beta)" width="146" height="44" /><div class="maintabbarbottom"></div>
-</div>
-<div class="ps-room scrollable" id="mainmenu"><div class="mainmenuwrapper">
-	<div class="leftmenu">
-		<div class="activitymenu">
-			<div class="pmbox">
-				<div class="pm-window news-embed" data-newsid="<!-- newsid -->">
-					<h3><button class="closebutton" tabindex="-1"><i class="fa fa-times-circle"></i></button><button class="minimizebutton" tabindex="-1"><i class="fa fa-minus-circle"></i></button>News</h3>
-					<div class="pm-log" style="max-height:none">
-						<!-- news -->
+<html>
+	<head>
+		<meta charset="UTF-8" />
+		<meta id="viewport" name="viewport" content="width=device-width" />
+		<title>Showdown Pet Mods</title>
+		<link rel="shortcut icon" href="favicon.ico" id="dynamic-favicon" />
+		<link rel="stylesheet" href="style/battle.css" />
+		<link rel="stylesheet" href="style/client.css" />
+		<link rel="stylesheet" href="style/sim-types.css" />
+		<link rel="stylesheet" href="style/utilichart.css" />
+		<link rel="stylesheet" href="style/font-awesome.css" />
+		<meta name="robots" content="noindex" />
+		<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+		<script src="config/config.js"></script>
+		<script>
+			function loadRemoteData(src) {
+				var scriptEl = document.createElement('script');
+				scriptEl.src = src.replace(/.*\/?data\//g, 'https://play.pokemonshowdown.com/data/');
+				document.head.appendChild(scriptEl);
+			}
+			Config.testclient = true;
+			(function() {
+				if (location.search !== '') {
+					var m = /\?~~(([^:\/]*)(:[0-9]*)?)/.exec(location.search);
+					if (m) {
+						Config.server = {
+							id: m[1],
+							host: m[2],
+							port: (m[3] && parseInt(m[3].substr(1))) || 8000
+						};
+					} else {
+						alert('Unrecognised query string syntax: ' + location.search);
+					}
+				}
+			})();
+		</script>
+		<!--[if lte IE 8]><script>
+			Config.oldie = true;
+		</script><![endif]-->
+	</head>
+	<body>
+		<div id="header" class="header">
+			<img class="logo" src="pokemonshowdownbeta.png" alt="Pok&eacute;mon Showdown! (beta)" width="146" height="44" /><div class="maintabbarbottom"></div>
+		</div>
+		<div class="ps-room scrollable" id="mainmenu"><div class="mainmenuwrapper">
+			<div class="leftmenu">
+				<div class="activitymenu">
+					<div class="pmbox">
+						<div class="pm-window news-embed">
+							<h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa-times-circle"></i></button><button class="minimizebutton" tabindex="-1" aria-label="Minimize"><i class="fa fa-minus-circle"></i></button>Latest News</h3>
+							<div class="pm-log" style="max-height:none">
+								<div class="newsentry"><h4>Pet Mods Client</h4><p>Welcome to the Pet Mods client! We have teambuilder support for mods here!</p><strong></strong></div>
+							</div>
+						</div>
 					</div>
 				</div>
+				<div class="mainmenu">
+					<div id="loading-message" class="mainmessage">Initializing... <noscript>FAILED<br /><br />Pok&eacute;mon Showdown requires JavaScript.</noscript></div>
+				</div>
 			</div>
-		</div>
-		<div class="mainmenu">
-			<div id="loading-message" class="mainmessage">Initializing... <noscript>FAILED<br /><br />Pok&eacute;mon Showdown requires JavaScript.</noscript></div>
-		</div>
-	</div>
-	<div class="rightmenu">
-	</div>
-	<div class="mainmenufooter">
-		<div class="bgcredit"></div>
-		<small><a href="//dex.pokemonshowdown.com/" target="_blank">Pok&eacute;dex</a> | <a href="//replay.pokemonshowdown.com/" target="_blank">Replays</a> | <a href="//pokemonshowdown.com/rules" target="_blank">Rules</a> | <a href="//pokemonshowdown.com/credits" target="_blank">Credits</a> | <a href="http://smogon.com/forums/" target="_blank">Forum</a> | <a href="//pokemonshowdown.com/privacy" target="_blank">Privacy policy</a></small>
-	</div>
-</div></div>
-<script>
-	var LM = document.getElementById('loading-message');
-	LM.innerHTML += ' DONE<br />Loading libraries...';
-</script>
-<script nomodule src="//play.pokemonshowdown.com/js/lib/ps-polyfill.js"></script>
-<script src="//play.pokemonshowdown.com/config/config.js?"></script>
-<script src="//play.pokemonshowdown.com/js/lib/jquery-2.2.4.min.js"></script>
-<script src="//play.pokemonshowdown.com/js/lib/jquery-cookie.js"></script>
-<script src="//play.pokemonshowdown.com/js/lib/autoresize.jquery.min.js?"></script>
-<script src="//play.pokemonshowdown.com/js/battle-sound.js?"></script>
-<script src="//play.pokemonshowdown.com/js/lib/html-css-sanitizer-minified.js?"></script>
-<script src="//play.pokemonshowdown.com/js/lib/lodash.core.js?"></script>
-<script src="//play.pokemonshowdown.com/js/lib/backbone.js?"></script>
-<script src="//play.pokemonshowdown.com/js/lib/d3.v3.min.js"></script>
+			<div class="rightmenu">
+			</div>
+			<div class="mainmenufooter">
+				<small><a href="//pokemonshowdown.com/" target="_blank"><strong>Pok&eacute;mon Showdown</strong></a> | <a href="http://smogon.com/" target="_blank"><strong>Smogon</strong></a><br><a href="//pokemonshowdown.com/dex/" target="_blank">Pokédex</a> | <a href="//pokemonshowdown.com/replay/" target="_blank">Replays</a> | <a href="//pokemonshowdown.com/rules" target="_blank">Rules</a></small> | <small><a href="//pokemonshowdown.com/forums/" target="_blank">Forum</a></small>
+			</div>
+		</div></div>
+		<script>
+			document.getElementById('loading-message').innerHTML += ' DONE<br />Loading libraries...';
+		</script>
+		<script nomodule src="/js/lib/ps-polyfill.js"></script>
+		<script src="js/lib/jquery-2.2.4.min.js"></script>
+		<script src="js/lib/jquery-cookie.js"></script>
+		<script src="js/lib/autoresize.jquery.min.js"></script>
+		<script src="js/battle-sound.js"></script>
+		<script src="../config/testclient-key.js"></script>
+		<script src="js/lib/html-css-sanitizer-minified.js"></script>
+		<script src="js/lib/lodash.core.js"></script>
+		<script src="js/lib/backbone.js"></script>
+		<script src="js/lib/d3.v3.min.js"></script>
+
+		<script>
+			document.getElementById('loading-message').innerHTML += ' DONE<br />Loading data...';
+			window.exports = window;
+		</script>
 
-<script>
-	LM.innerHTML += ' DONE<br />Loading data...';
-</script>
+		<script src="js/battledata.js" onerror="alert('You must build the client with `node build` before using testclient.html')"></script>
+		<script src="data/text.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/pokedex-mini.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/pokedex-mini-bw.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/typechart.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="js/battle.js"></script>
+		<script src="js/lib/sockjs-1.4.0-nwjsfix.min.js"></script>
+		<script src="js/lib/color-thief.min.js"></script>
 
-<script src="//play.pokemonshowdown.com/js/battledata.js?"></script>
-<script src="//play.pokemonshowdown.com/js/storage.js?"></script>
-<script src="//play.pokemonshowdown.com/data/pokedex-mini.js?"></script>
-<script src="//play.pokemonshowdown.com/data/typechart.js?"></script>
-<script src="//play.pokemonshowdown.com/js/battle.js?"></script>
-<script src="//play.pokemonshowdown.com/js/lib/sockjs-1.4.0-nwjsfix.min.js"></script>
-<script src="//play.pokemonshowdown.com/js/lib/color-thief.min.js"></script>
+		<script>
+			document.getElementById('loading-message').innerHTML += ' DONE<br />Loading client...';
+		</script>
 
-<script>
-	LM.innerHTML += ' DONE<br />Loading client...';
-</script>
+		<script src="js/client.js"></script>
+		<script src="js/client-topbar.js"></script>
+		<script src="js/client-mainmenu.js"></script>
+		<script src="js/client-teambuilder.js"></script>
+		<script src="js/client-ladder.js"></script>
+		<script src="js/client-chat.js"></script>
+		<script src="js/client-chat-tournament.js"></script>
+		<script src="js/battle-tooltips.js"></script>
+		<script src="js/client-battle.js"></script>
+		<script src="js/client-rooms.js"></script>
+		<script src="js/storage.js"></script>
+		<script src="data/graphics.js" onerror="loadRemoteData(this.src)"></script>
 
-<script src="//play.pokemonshowdown.com/js/client.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-topbar.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-mainmenu.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-teambuilder.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-ladder.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-chat.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-chat-tournament.js?"></script>
-<script src="//play.pokemonshowdown.com/js/battle-tooltips.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-battle.js?"></script>
-<script src="//play.pokemonshowdown.com/js/client-rooms.js?"></script>
-<script src="//play.pokemonshowdown.com/data/graphics.js?"></script>
+		<script src="data/pokedex.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/moves.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/items.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/abilities.js" onerror="loadRemoteData(this.src)"></script>
 
-<script>
-	// framebust - see https://owasp.org/www-pdf-archive/OWASP_AppSec_Research_2010_Busting_Frame_Busting_by_Rydstedt.pdf
-	// should be robust against reflective XSS filters and navigation interception
-	var app;
-	if (self === top) {
-		app = new App();
-	} else {
-		LM.innerHTML += ' IN FRAME<br />Please visit Showdown directly.';
-		top.location = self.location;
-	}
-</script>
+		<script src="data/search-index.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/teambuilder-tables.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/mod-sprites.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/mod-config.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="js/battle-dex-search.js"></script>
+		<script src="js/search.js"></script>
 
-<script src="//play.pokemonshowdown.com/data/pokedex.js?"></script>
-<script src="//play.pokemonshowdown.com/data/moves.js?"></script>
-<script src="//play.pokemonshowdown.com/data/items.js?"></script>
-<script src="//play.pokemonshowdown.com/data/abilities.js?"></script>
+		<script src="data/aliases.js" async="async" onerror="loadRemoteData(this.src)"></script>
 
-<script src="//play.pokemonshowdown.com/data/search-index.js?"></script>
-<script src="//play.pokemonshowdown.com/data/teambuilder-tables.js?"></script>
-<script src="//play.pokemonshowdown.com/js/battle-dex-search.js?"></script>
-<script src="//play.pokemonshowdown.com/js/search.js?"></script>
+		<script>
+			window.onload = () => {
+				window.app = new App();
+			}
+		</script>
 
-<script src="//play.pokemonshowdown.com/data/aliases.js?" async></script>
-<script src="//play.pokemonshowdown.com/js/clean-cookies.php" async></script>
+	</body>
+</html>
\ No newline at end of file
diff --git a/play.pokemonshowdown.com/js/client-battle.js b/play.pokemonshowdown.com/js/client-battle.js
index 431390403..eb68c87ad 100644
--- a/play.pokemonshowdown.com/js/client-battle.js
+++ b/play.pokemonshowdown.com/js/client-battle.js
@@ -617,7 +617,7 @@
 					var tooltipArgs = 'activepokemon|1|' + i;
 
 					var disabled = false;
-					if (moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
+					if (moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf' || moveTarget === 'anyAlly') {
 						disabled = true;
 					} else if (moveTarget === 'normal' || moveTarget === 'adjacentFoe') {
 						if (Math.abs(farSlot - i) > 1) disabled = true;
@@ -628,7 +628,7 @@
 					} else if (!pokemon || pokemon.fainted) {
 						targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '"><span class="picon" style="' + Dex.getPokemonIcon('missingno') + '"></span></button> ';
 					} else {
-						targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.speciesForme : BattleLog.escapeHTML(pokemon.name)) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
+						targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.speciesForme : BattleLog.escapeHTML(pokemon.name)) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
 					}
 				}
 				for (var i = 0; i < nearActive.length; i++) {
@@ -641,14 +641,14 @@
 					} else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
 						if (Math.abs(activePos - i) > 1) disabled = true;
 					}
-					if (moveTarget !== 'adjacentAllyOrSelf' && activePos == i) disabled = true;
+					if (moveTarget !== 'adjacentAllyOrSelf' && moveTarget !== 'anyAlly' && activePos == i) disabled = true;
 
 					if (disabled) {
 						targetMenus[1] += '<button disabled style="visibility:hidden"></button> ';
 					} else if (!pokemon || pokemon.fainted) {
 						targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '"><span class="picon" style="' + Dex.getPokemonIcon('missingno') + '"></span></button> ';
 					} else {
-						targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
+						targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
 					}
 				}
 
@@ -795,9 +795,9 @@
 				pokemon.name = pokemon.ident.substr(4);
 				var tooltipArgs = 'switchpokemon|' + i;
 				if (pokemon.fainted || i < this.battle.pokemonControlled || this.choice.switchFlags[i] || trapped) {
-					party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : trapped ? ',trapped' : i < this.battle.nearSide.active.length ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
+					party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : trapped ? ',trapped' : i < this.battle.nearSide.active.length ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
 				} else {
-					party += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
+					party += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
 				}
 			}
 			if (this.battle.mySide.ally) party += this.displayAllyParty();
@@ -811,7 +811,7 @@
 				var pokemon = allyParty[i];
 				pokemon.name = pokemon.ident.substr(4);
 				var tooltipArgs = 'allypokemon|' + i;
-				party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + ',notMine' + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
+				party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + ',notMine' + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
 			}
 			return party;
 		},
@@ -852,11 +852,11 @@
 					var pokemon = this.battle.myPokemon[i];
 					var tooltipArgs = 'switchpokemon|' + i;
 					if (pokemon && !pokemon.fainted || this.choice.switchOutFlags[i]) {
-						controls += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
+						controls += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
 					} else if (!pokemon) {
 						controls += '<button disabled></button> ';
 					} else {
-						controls += '<button name="chooseSwitchTarget" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
+						controls += '<button name="chooseSwitchTarget" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
 					}
 				}
 				controls += '</div>';
@@ -892,7 +892,7 @@
 							switchMenu += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
 						}
 					}
-					switchMenu += '<span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
+					switchMenu += '<span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
 				}
 
 				var controls = (
@@ -927,9 +927,9 @@
 				var pokemon = switchables[oIndex];
 				var tooltipArgs = 'switchpokemon|' + oIndex;
 				if (i < this.choice.done) {
-					switchMenu += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '</button> ';
+					switchMenu += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '</button> ';
 				} else {
-					switchMenu += '<button name="chooseTeamPreview" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '</button> ';
+					switchMenu += '<button name="chooseTeamPreview" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon, false, this.battle.mod || '') + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '</button> ';
 				}
 			}
 
@@ -1272,9 +1272,10 @@
 				var isTerastal = !!(this.$('input[name=terastallize]')[0] || '').checked;
 
 				var target = e.getAttribute('data-target');
-				var choosableTargets = {normal: 1, any: 1, adjacentAlly: 1, adjacentAllyOrSelf: 1, adjacentFoe: 1};
+				var choosableTargets = {normal: 1, any: 1, adjacentAlly: 1, adjacentAllyOrSelf: 1, anyAlly: 1, adjacentFoe: 1};
 				if (this.battle.gameType === 'freeforall') delete choosableTargets['adjacentAllyOrSelf'];
 
+
 				this.choice.choices.push('move ' + pos + (isMega ? ' mega' : '') + (isMegaX ? ' megax' : isMegaY ? ' megay' : '') + (isZMove ? ' zmove' : '') + (isUltraBurst ? ' ultra' : '') + (isDynamax ? ' dynamax' : '') + (isTerastal ? ' terastallize' : ''));
 				if (nearActive.length > 1 && target in choosableTargets) {
 					this.choice.type = 'movetarget';
diff --git a/play.pokemonshowdown.com/js/client-teambuilder.js b/play.pokemonshowdown.com/js/client-teambuilder.js
index fd2593e4f..853cd2fde 100644
--- a/play.pokemonshowdown.com/js/client-teambuilder.js
+++ b/play.pokemonshowdown.com/js/client-teambuilder.js
@@ -30,6 +30,9 @@
 				if (this.curTeam.format.includes('bdsp')) {
 					this.curTeam.dex = Dex.mod('gen8bdsp');
 				}
+				if (this.curTeam.mod) {
+					this.curTeam.dex = Dex.mod(this.curTeam.mod);
+				}
 				Storage.activeSetList = this.curSetList;
 			}
 		},
@@ -127,6 +130,7 @@
 		curTeamLoc: 0,
 		curSet: null,
 		curSetLoc: 0,
+		dex: Dex,
 
 		// curFolder will have '/' at the end if it's a folder, but
 		// it will be alphanumeric (so guaranteed no '/') if it's a
@@ -146,16 +150,20 @@
 		update: function () {
 			teams = Storage.teams;
 			if (this.curTeam) {
+				// console.log(JSON.stringify(this.curTeam, null, 4))
 				if (this.curTeam.format && !this.formatResources[this.curTeam.format]) {
 					this.tryLoadFormatResource(this.curTeam.format);
 				}
+				
 				if (this.curTeam.loaded === false || (this.curTeam.teamid && !this.curTeam.loaded)) {
 					this.loadTeam();
 					return this.updateTeamView();
 				}
 				this.ignoreEVLimits = (this.curTeam.gen < 3 ||
 					((this.curTeam.format.includes('hackmons') || this.curTeam.format.endsWith('bh')) && this.curTeam.gen !== 6) ||
-					this.curTeam.format.includes('metronomebattle'));
+					this.curTeam.format.includes('metronomebattle') || (this.curTeam.mod && ModConfig[this.curTeam.mod].ignoreEVLimits));
+					this.dex = this.curTeam.mod ? Dex.mod(this.curTeam.mod) : Dex; // keeping just in case for now
+					this.curTeam.dex = this.curTeam.mod ? Dex.mod(this.curTeam.mod) : Dex;
 				if (this.curSet) {
 					return this.updateSetView();
 				}
@@ -750,12 +758,21 @@
 			this.curTeam.iconCache = '!';
 			this.curTeam.gen = this.getGen(this.curTeam.format);
 			this.curTeam.dex = Dex.forGen(this.curTeam.gen);
+			var ClientMods = ModConfig;
+			for (var modid in (ClientMods)) {
+				for (var formatid in ClientMods[modid].formats) {
+					if (formatid === this.curTeam.format) this.curTeam.mod = modid;
+				}
+			}
 			if (this.curTeam.format.includes('letsgo')) {
 				this.curTeam.dex = Dex.mod('gen7letsgo');
 			}
 			if (this.curTeam.format.includes('bdsp')) {
 				this.curTeam.dex = Dex.mod('gen8bdsp');
 			}
+			if (this.curTeam.mod) {
+				this.curTeam.dex = Dex.mod(this.curTeam.mod);
+			}
 			Storage.activeSetList = this.curSetList = Storage.unpackTeam(this.curTeam.team);
 			this.curTeamIndex = i;
 			this.update();
@@ -1284,7 +1301,7 @@
 			var isBDSP = this.curTeam.format.includes('bdsp');
 			var isNatDex = this.curTeam.format.includes('nationaldex') || this.curTeam.format.includes('natdex');
 			var buf = '<li value="' + i + '">';
-			if (!set.species) {
+			if (!set.species || !species) {
 				if (this.deletedSet) {
 					buf += '<div class="setmenu setmenu-left"><button name="undeleteSet" class="button"><i class="fa fa-undo"></i> Undo Delete</button></div>';
 				}
@@ -1297,7 +1314,7 @@
 			buf += '<div class="setchart-nickname">';
 			buf += '<label>Nickname</label><input type="text" name="nickname" class="textbox" value="' + BattleLog.escapeHTML(set.name || '') + '" placeholder="' + BattleLog.escapeHTML(species.baseSpecies) + '" />';
 			buf += '</div>';
-			buf += '<div class="setchart" style="' + Dex.getTeambuilderSprite(set, this.curTeam.gen) + ';">';
+			buf += '<div class="setchart" style="' + Dex.getTeambuilderSprite(set, this.curTeam.gen, this.curTeam.mod) + ';">';
 
 			// icon
 			buf += '<div class="setcol setcol-icon">';
@@ -1356,14 +1373,19 @@
 			var itemicon = '<span class="itemicon"></span>';
 			if (set.item) {
 				var item = this.curTeam.dex.items.get(set.item);
-				itemicon = '<span class="itemicon" style="' + Dex.getItemIcon(item) + '"></span>';
+				itemicon = '<span class="itemicon" style="' + Dex.getItemIcon(item, this.curTeam.mod) + '"></span>';
 			}
 			buf += itemicon;
 			buf += '</div>';
 			buf += '<div class="setcell setcell-typeicons">';
 			var types = species.types;
+			var table = (this.curTeam.gen < 7 ? BattleTeambuilderTable['gen' + this.curTeam.gen] : null);
+			if (
+				table && table.overrideDexInfo && species.id in table.overrideDexInfo &&
+				table.overrideDexInfo[species.id].types
+			) types = table.overrideDexInfo[species.id].types; 
 			if (types) {
-				for (var i = 0; i < types.length; i++) buf += Dex.getTypeIcon(types[i]);
+				for (var i = 0; i < types.length; i++) buf += Dex.getTypeIcon(types[i], null, this.curTeam.mod);
 			}
 			buf += '</div></div>';
 
@@ -1385,7 +1407,7 @@
 			buf += '<div class="setcol setcol-stats"><div class="setrow"><label>Stats</label><button class="textbox setstats" name="stats">';
 			buf += '<span class="statrow statrow-head"><label></label> <span class="statgraph"></span> <em>' + (!isLetsGo ? 'EV' : 'AV') + '</em></span>';
 			var stats = {};
-			var defaultEV = (this.curTeam.gen > 2 ? 0 : 252);
+			var defaultEV = ((this.curTeam.gen > 2 && !this.ignoreEVLimits) ? 0 : 252);
 			for (var j in BattleStatNames) {
 				if (j === 'spd' && this.curTeam.gen === 1) continue;
 				stats[j] = this.getStat(j, set);
@@ -1471,6 +1493,7 @@
 			}
 		},
 		addPokemon: function () {
+			console.log("add pokemon");
 			if (!this.curTeam) return;
 			var team = this.curSetList;
 			if (!team.length || team[team.length - 1].species) {
@@ -1603,12 +1626,22 @@
 			this.curTeam.format = format;
 			this.curTeam.gen = this.getGen(this.curTeam.format);
 			this.curTeam.dex = Dex.forGen(this.curTeam.gen);
+			this.curTeam.mod = 0;
 			if (this.curTeam.format.includes('letsgo')) {
 				this.curTeam.dex = Dex.mod('gen7letsgo');
 			}
 			if (this.curTeam.format.includes('bdsp')) {
 				this.curTeam.dex = Dex.mod('gen8bdsp');
 			}
+			var ClientMods = ModConfig;
+			for (var modid in (ClientMods)) {
+				for (var formatid in ClientMods[modid].formats) {
+					if (formatid === this.curTeam.format) this.curTeam.mod = modid;
+				}
+			}
+			if (this.curTeam.mod) {
+				this.curTeam.dex = Dex.mod(this.curTeam.mod);
+			}
 			this.save();
 			if (this.curTeam.gen === 5 && !Dex.loadedSpriteData['bw']) Dex.loadSpriteData('bw');
 			this.update();
@@ -1653,10 +1686,10 @@
 			var buf = '';
 			for (var i = 0; i < this.clipboardCount(); i++) {
 				var res = this.clipboard[i];
-				var species = Dex.species.get(res.species);
+				var species = this.curTeam.dex.species.get(res.species);
 
 				buf += '<div class="result" data-id="' + i + '">';
-				buf += '<div class="section"><span class="icon" style="' + Dex.getPokemonIcon(species.name) + '"></span>';
+				buf += '<div class="section"><span class="icon" style="' + Dex.getPokemonIcon(species.name, false, this.curTeam.mod) + '"></span>';
 				buf += '<span class="species">' + (species.name === species.baseSpecies ? BattleLog.escapeHTML(species.name) : (BattleLog.escapeHTML(species.baseSpecies) + '-<small>' + BattleLog.escapeHTML(species.name.substr(species.baseSpecies.length + 1)) + '</small>')) + '</span></div>';
 				buf += '<div class="section"><span class="ability-item">' + (BattleLog.escapeHTML(res.ability) || '<i>No ability</i>') + '<br />' + (BattleLog.escapeHTML(res.item) || '<i>No item</i>') + '</span></div>';
 				buf += '<div class="section no-border">';
@@ -1766,7 +1799,7 @@
 				.focus()
 				.select();
 
-			this.getSmogonSets();
+			// this.getSmogonSets();
 		},
 		getSmogonSets: function () {
 			this.$('.teambuilder-pokemon-import .teambuilder-import-smogon-sets').empty();
@@ -2012,14 +2045,14 @@
 			}
 			for (var i = start; i < end; i++) {
 				var set = this.curSetList[i];
-				var pokemonicon = '<span class="picon pokemonicon-' + i + '" style="' + Dex.getPokemonIcon(set) + '"></span>';
+				var pokemonicon = '<span class="picon pokemonicon-' + i + '" style="' + Dex.getPokemonIcon(set, false, this.curTeam.mod) + '"></span>';
 				if (!set.species) {
 					buf += '<button disabled class="addpokemon" aria-label="Add Pok&eacute;mon"><i class="fa fa-plus"></i></button> ';
 					isAdd = true;
 				} else if (i == this.curSetLoc) {
 					buf += '<button disabled class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species).baseSpecies || '<i class="fa fa-plus"></i>') + '</button> ';
 				} else {
-					buf += '<button name="selectPokemon" value="' + i + '" class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species).baseSpecies) + '</button> ';
+					buf += '<button name="selectPokemon" value="' + i + '" class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species, undefined, "From Render Teambar 2").baseSpecies) + '</button> ';
 				}
 			}
 			if (this.curSetList.length < this.curTeam.capacity && !isAdd) {
@@ -2031,13 +2064,13 @@
 			var set = this.curSet;
 			if (!set) return;
 
-			this.$('.setchart').attr('style', Dex.getTeambuilderSprite(set, this.curTeam.gen));
+			this.$('.setchart').attr('style', Dex.getTeambuilderSprite(set, this.curTeam.gen, this.curTeam.mod));
 
-			this.$('.pokemonicon-' + this.curSetLoc).css('background', Dex.getPokemonIcon(set).substr(11));
+			this.$('.pokemonicon-' + this.curSetLoc).css('background', Dex.getPokemonIcon(set, false, this.curTeam.mod).substr(11));
 
 			var item = this.curTeam.dex.items.get(set.item);
 			if (item.id) {
-				this.$('.setcol-details .itemicon').css('background', Dex.getItemIcon(item).substr(11));
+				this.$('.setcol-details .itemicon').css('background', Dex.getItemIcon(set, false, this.curTeam.mod).substr(11));
 			} else {
 				this.$('.setcol-details .itemicon').css('background', 'none');
 			}
@@ -2271,12 +2304,12 @@
 			var buf = '';
 			var set = this.curSet;
 			var species = this.curTeam.dex.species.get(this.curSet.species);
-
+			if (this.curTeam.mod) species = this.curTeam.dex.species.get(this.curSet.species,undefined, "from updateStatForm 2"); 
 			var baseStats = species.baseStats;
 
 			buf += '<div class="resultheader"><h3>EVs</h3></div>';
 			buf += '<div class="statform">';
-			var guess = new BattleStatGuesser(this.curTeam.format).guess(set);
+			var guess = new BattleStatGuesser(this.curTeam.format, this.curTeam.mod).guess(set);
 			var role = guess.role;
 
 			var guessedEVs = guess.evs;
@@ -2319,10 +2352,11 @@
 
 			var supportsEVs = !this.curTeam.format.includes('letsgo');
 			// var supportsAVs = !supportsEVs && this.curTeam.format.endsWith('norestrictions');
-			var defaultEV = this.curTeam.gen <= 2 ? 252 : 0;
+			var defaultEV = (this.curTeam.gen > 2 && !this.ignoreEVLimits) ? 0 : 252;
 			var maxEV = supportsEVs ? 252 : 200;
 			var stepEV = supportsEVs ? 4 : 1;
 
+
 			// label column
 			buf += '<div class="col labelcol"><div></div>';
 			buf += '<div><label>HP</label></div><div><label>Attack</label></div><div><label>Defense</label></div><div>';
@@ -2838,7 +2872,7 @@
 			var isBDSP = this.curTeam.format.includes('bdsp');
 			var isNatDex = this.curTeam.format.includes('nationaldex') || this.curTeam.format.includes('natdex');
 			var isHackmons = this.curTeam.format.includes('hackmons') || this.curTeam.format.endsWith('bh');
-			var species = this.curTeam.dex.species.get(set.species);
+			var species = this.curTeam.dex.species.get(set.species, undefined, "from updateDetailsForm");
 			if (!set) return;
 			buf += '<div class="resultheader"><h3>Details</h3></div>';
 			buf += '<form class="detailsform">';
@@ -2939,7 +2973,7 @@
 			e.stopPropagation();
 			var set = this.curSet;
 			if (!set) return;
-			var species = this.curTeam.dex.species.get(set.species);
+			var species = this.curTeam.dex.species.get(set.species, undefined, "from detailsChange");
 			var isLetsGo = this.curTeam.format.includes('letsgo');
 			var isBDSP = this.curTeam.format.includes('bdsp');
 			var isNatDex = this.curTeam.format.includes('nationaldex') || this.curTeam.format.includes('natdex');
@@ -3216,7 +3250,7 @@
 				} else if (id in BattleMovedex && format && format.endsWith("trademarked")) {
 					val = BattleMovedex[id].name;
 				} else {
-					val = (id in BattleAbilities ? BattleAbilities[id].name : '');
+					val = (id in BattleAbilities ? this.curTeam.dex.abilities.get(e.currentTarget.value).name : '');
 				}
 				break;
 			case 'item':
@@ -3225,14 +3259,14 @@
 				} else if (id in BattleAbilities && format && format.endsWith("multibility")) {
 					val = BattleAbilities[id].name;
 				} else {
-					val = (id in BattleItems ? BattleItems[id].name : '');
+					val = (id in BattleItems ? this.curTeam.dex.items.get(e.currentTarget.value).name : '');
 				}
 				break;
 			case 'move1': case 'move2': case 'move3': case 'move4':
 				if (id in BattlePokedex && format && format.endsWith("pokemoves")) {
 					val = BattlePokedex[id].name;
 				} else {
-					val = (id in BattleMovedex ? BattleMovedex[id].name : '');
+					val = (id in BattleMovedex ? this.curTeam.dex.moves.get(e.currentTarget.value).name : '');
 				}
 				break;
 			}
@@ -3426,18 +3460,15 @@
 			if (resetSpeed) minSpe = false;
 			if (moveName.substr(0, 13) === 'Hidden Power ') {
 				if (!this.canHyperTrain(set)) {
-					var hpType = moveName.substr(13);
-
+					var hpType = moveName.substr(13).toLowerCase();
 					set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31};
 					if (this.curTeam.gen > 2) {
-						var HPivs = this.curTeam.dex.types.get(hpType).HPivs;
-						for (var i in HPivs) {
-							set.ivs[i] = HPivs[i];
+						for (var i in exports.BattleTypeChart[hpType].HPivs) {
+							set.ivs[i] = exports.BattleTypeChart[hpType].HPivs[i];
 						}
 					} else {
-						var HPdvs = this.curTeam.dex.types.get(hpType).HPdvs;
-						for (var i in HPdvs) {
-							set.ivs[i] = HPdvs[i] * 2;
+						for (var i in exports.BattleTypeChart[hpType].HPdvs) {
+							set.ivs[i] = exports.BattleTypeChart[hpType].HPdvs[i] * 2;
 						}
 						var atkDV = Math.floor(set.ivs.atk / 2);
 						var defDV = Math.floor(set.ivs.def / 2);
@@ -3470,7 +3501,10 @@
 			for (var i = 0; i < moves.length; ++i) {
 				if (!moves[i]) continue;
 				if (moves[i].substr(0, 13) === 'Hidden Power ') hasHiddenPower = true;
-				var move = this.curTeam.dex.moves.get(moves[i]);
+				//var move = this.curTeam.dex.moves.get(moves[i]);
+				var move = 0;
+				if (this.curTeam.mod) move = Dex.mod(this.curTeam.mod).moves.get(moves[i]);
+				else move = Dex.forGen(this.curTeam.gen).moves.get(moves[i]);
 				if (move.id === 'transform') {
 					hasHiddenPower = true; // A Pokemon with Transform can copy another Pokemon that knows Hidden Power
 
@@ -3525,8 +3559,10 @@
 		},
 		setPokemon: function (val, selectNext) {
 			var set = this.curSet;
-			var species = this.curTeam.dex.species.get(val);
-			if (!species.exists || set.species === species.name) {
+			var species;
+			if (this.curTeam.mod) species = Dex.mod(this.curTeam.mod).species.get(val, undefined, "from setPokemon 1"); 
+			else species = Dex.forGen(this.curTeam.gen).species.get(val,undefined, "from setPokemon 2"); 
+			if (!species || !species.exists || set.species === species.name) {
 				if (selectNext) this.$('input[name=item]').select();
 				return;
 			}
@@ -3562,12 +3598,11 @@
 			if (set.gigantamax) delete set.gigantamax;
 			if (set.teraType) delete set.teraType;
 			if (!(this.curTeam.format.includes('hackmons') || this.curTeam.format.endsWith('bh')) && species.requiredItems.length === 1) {
-				set.item = species.requiredItems[0];
+				set.item = species.requiredItems[0] || '';
 			} else {
 				set.item = '';
 			}
 			set.ability = species.abilities['0'];
-
 			set.moves = [];
 			set.evs = {};
 			set.ivs = {};
@@ -3600,8 +3635,10 @@
 
 			// do this after setting set.evs because it's assumed to exist
 			// after getStat is run
-			var species = this.curTeam.dex.species.get(set.species);
-			if (!species.exists) return 0;
+			var species;
+			if (this.curTeam.mod) species = Dex.mod(this.curTeam.mod).species.get(set.species,undefined, "from getStat1");
+			else species = Dex.forGen(this.curTeam.gen).species.get(set.species,undefined, "from getStat 2");
+			if (!species || !species.exists) return 0;
 
 			if (!set.level) set.level = 100;
 			if (typeof set.ivs[stat] === 'undefined') set.ivs[stat] = 31;
@@ -3704,47 +3741,65 @@
 			this.room = data.room;
 			this.curSet = data.curSet;
 			this.chartIndex = data.index;
+			const mod = (this.room.curTeam && this.room.curTeam.mod) || ""; 
 			var species = this.room.curTeam.dex.species.get(this.curSet.species);
 			var baseid = toID(species.baseSpecies);
 			var forms = [baseid].concat(species.cosmeticFormes.map(toID));
-			var spriteDir = Dex.resourcePrefix + 'sprites/';
+
+			let modSprite = Dex.getSpriteMod(mod, baseid, 'front', species.exists !== false)
+				|| Dex.getSpriteMod(mod, species.id, 'front', species.exists !== false);
+			let resourcePrefix;
+			let d;
+			if (modSprite) {
+				resourcePrefix = Dex.modResourcePrefix + modSprite + '/';
+				d = "";
+			}  else {
+				resourcePrefix = Dex.resourcePrefix;
+				d = "-";
+			}
+
+			var spriteDir = resourcePrefix + 'sprites/';
 			var spriteSize = 96;
 			var spriteDim = 'width: 96px; height: 96px;';
 
 			var gen = Math.max(this.room.curTeam.gen, species.gen);
-			var dir = gen > 5 ? 'dex' : 'gen' + gen;
-			if (Dex.prefs('nopastgens')) gen = 'dex';
-			if (Dex.prefs('bwgfx') && dir === 'dex') gen = 'gen5';
-			spriteDir += dir;
+			var dir;
+			if (modSprite) { 
+				dir = "front";
+			} else {
+				if (Dex.prefs('bwgfx')) dir = 'gen5';
+				else if (Dex.prefs('nopastgens') || gen > 5) dir = 'dex';
+				else dir = 'gen' + gen;
+			}
+			var spriteDir = resourcePrefix + 'sprites/' + dir;
 			if (dir === 'dex') {
 				spriteSize = 120;
 				spriteDim = 'width: 120px; height: 120px;';
 			}
 
-			var buf = '';
-			buf += '<p>Pick a variant or <button name="close" class="button">Cancel</button></p>';
-			buf += '<div class="formlist">';
+			var buf = '<p>Pick a variant or <button name="close" class="button">Cancel</button></p><div class="formlist">';
 
 			var formCount = forms.length;
 			for (var i = 0; i < formCount; i++) {
 				var formid = forms[i].substring(baseid.length);
 				var form = (formid ? formid[0].toUpperCase() + formid.slice(1) : '');
 				buf += '<button name="setForm" value="' + form + '" style="';
-				buf += 'background-image: url(' + spriteDir + '/' + baseid + (form ? '-' + formid : '') + '.png); ' + spriteDim + '" class="option';
-				buf += (form === (species.forme || '') ? ' cur' : '') + '"></button>';
+				buf += 'background-image: url(' + spriteDir + '/' + baseid + (form ? d + formid : '') + '.png); ' + spriteDim + '" class="option';
+				if (form === (species.forme || '')) buf += ' cur';
+				buf += '"></button>';
 			}
-			buf += '<div style="clear:both"></div>';
-			buf += '</div>';
+			buf += '<div style="clear:both"></div></div>';
 
 			this.$el.html(buf).css({'max-width': (4 + spriteSize) * 7});
 		},
 		setForm: function (form) {
-			var species = Dex.species.get(this.curSet.species);
-			if (form && form !== species.form) {
-				this.curSet.species = Dex.species.get(species.baseSpecies + form).name;
-			} else if (!form) {
+			var species = this.room.curTeam.dex.species.get(this.curSet.species);
+			if (!form) {
 				this.curSet.species = species.baseSpecies;
 			}
+			else if (form !== species.form) {
+				this.curSet.species = this.room.curTeam.dex.species.get(species.baseSpecies + form).name;
+			}
 			this.close();
 			if (this.room.curSet) {
 				this.room.updatePokemonSprite();
diff --git a/play.pokemonshowdown.com/js/client.js b/play.pokemonshowdown.com/js/client.js
index 8caa89362..24b6318c1 100644
--- a/play.pokemonshowdown.com/js/client.js
+++ b/play.pokemonshowdown.com/js/client.js
@@ -218,7 +218,7 @@ function toId() {
 		getActionPHP: function () {
 			var ret = '/~~' + Config.server.id + '/action.php';
 			if (Config.testclient) {
-				ret = 'https://' + Config.routes.client + ret;
+				ret = 'https://play.pokemonshowdown.com/action.php';
 			}
 			return (this.getActionPHP = function () {
 				return ret;
@@ -2081,7 +2081,7 @@ function toId() {
 
 		playNotificationSound: function () {
 			if (window.BattleSound && !Dex.prefs('mute')) {
-				BattleSound.playSound('audio/notification.wav', Dex.prefs('notifvolume'));
+				BattleSound.playSound('https://' + Config.routes.psmain + '/audio/notification.wav', Dex.prefs('notifvolume'));
 			}
 		},
 
diff --git a/play.pokemonshowdown.com/js/replay-embed.template.js b/play.pokemonshowdown.com/js/replay-embed.template.js
index 6b372e5ed..6aa3a1709 100644
--- a/play.pokemonshowdown.com/js/replay-embed.template.js
+++ b/play.pokemonshowdown.com/js/replay-embed.template.js
@@ -28,27 +28,30 @@ function requireScript(url) {
 	document.head.appendChild(scriptEl);
 }
 
-linkStyle('https://play.pokemonshowdown.com/style/font-awesome.css?');
-linkStyle('https://play.pokemonshowdown.com/style/battle.css?a7');
-linkStyle('https://play.pokemonshowdown.com/style/replay.css?a7');
-linkStyle('https://play.pokemonshowdown.com/style/utilichart.css?a7');
-
-requireScript('https://play.pokemonshowdown.com/js/lib/ps-polyfill.js');
-requireScript('https://play.pokemonshowdown.com/config/config.js?a7');
-requireScript('https://play.pokemonshowdown.com/js/lib/jquery-1.11.0.min.js');
-requireScript('https://play.pokemonshowdown.com/js/lib/html-sanitizer-minified.js');
-requireScript('https://play.pokemonshowdown.com/js/battle-sound.js');
-requireScript('https://play.pokemonshowdown.com/js/battledata.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/pokedex-mini.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/pokedex-mini-bw.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/graphics.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/pokedex.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/moves.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/abilities.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/items.js?a7');
-requireScript('https://play.pokemonshowdown.com/data/teambuilder-tables.js?a7');
-requireScript('https://play.pokemonshowdown.com/js/battle-tooltips.js?a7');
-requireScript('https://play.pokemonshowdown.com/js/battle.js?a7');
+linkStyle('http://petmodsdh.com/style/font-awesome.css?');
+linkStyle('http://petmodsdh.com/style/battle.css?a7');
+linkStyle('http://petmodsdh.com/style/replay.css?a7');
+linkStyle('http://petmodsdh.com/style/utilichart.css?a7');
+
+requireScript('http://petmodsdh.com/js/lib/ps-polyfill.js');
+requireScript('http://petmodsdh.com/config/config.js?a7');
+requireScript('http://petmodsdh.com/js/lib/jquery-1.11.0.min.js');
+requireScript('http://petmodsdh.com/js/lib/lodash.compat.js');
+requireScript('http://petmodsdh.com/js/lib/html-sanitizer-minified.js');
+requireScript('http://petmodsdh.com/js/battle-sound.js');
+requireScript('http://petmodsdh.com/js/battledata.js?a7');
+requireScript('http://petmodsdh.com/data/pokedex-mini.js?a7');
+requireScript('http://petmodsdh.com/data/pokedex-mini-bw.js?a7');
+requireScript('http://petmodsdh.com/data/graphics.js?a7');
+requireScript('http://petmodsdh.com/data/pokedex.js?a7');
+requireScript('http://petmodsdh.com/data/moves.js?a7');
+requireScript('http://petmodsdh.com/data/abilities.js?a7');
+requireScript('http://petmodsdh.com/data/items.js?a7');
+requireScript('http://petmodsdh.com/data/teambuilder-tables.js?a7');
+requireScript('http://petmodsdh.com/data/mod-sprites.js?a7');
+requireScript('http://petmodsdh.com/data/mod-config.js?a7');
+requireScript('http://petmodsdh.com/js/battle-tooltips.js?a7');
+requireScript('http://petmodsdh.com/js/battle.js?a7');
 
 var Replays = {
 	battle: null,
diff --git a/play.pokemonshowdown.com/js/search.js b/play.pokemonshowdown.com/js/search.js
index 5fafa1ba3..8b8293481 100644
--- a/play.pokemonshowdown.com/js/search.js
+++ b/play.pokemonshowdown.com/js/search.js
@@ -108,9 +108,8 @@
 		var buf = '<p>Filters: ';
 		for (var i = 0; i < this.filters.length; i++) {
 			var text = this.filters[i][1];
-			if (this.filters[i][0] === 'move') text = Dex.moves.get(text).name;
-			if (this.filters[i][0] === 'pokemon') text = Dex.species.get(text).name;
-			buf += '<button class="filter" value="' + BattleLog.escapeHTML(this.filters[i].join(':')) + '">' + text + ' <i class="fa fa-times-circle"></i></button> ';
+			if (this.filters[i][0] === 'move') text = this.engine.dex.species.get(text).name;
+			if (this.filters[i][0] === 'pokemon') text = this.engine.dex.moves.get(text).name;			buf += '<button class="filter" value="' + BattleLog.escapeHTML(this.filters[i].join(':')) + '">' + text + ' <i class="fa fa-times-circle"></i></button> ';
 		}
 		if (!q) buf += '<small style="color: #888">(backspace = delete filter)</small>';
 		return buf + '</p>';
@@ -197,7 +196,7 @@
 		case 'sortmove':
 			return this.renderMoveSortRow();
 		case 'pokemon':
-			var pokemon = this.engine.dex.species.get(id);
+			var pokemon = this.engine.dex.species.get(id, (id, undefined, "from renderRow"));
 			return this.renderPokemonRow(pokemon, matchStart, matchLength, errorMessage, attrs);
 		case 'move':
 			var move = this.engine.dex.moves.get(id);
@@ -303,7 +302,7 @@
 
 		// icon
 		buf += '<span class="col iconcol">';
-		buf += '<span style="' + Dex.getPokemonIcon(pokemon.name) + '"></span>';
+		buf += '<span style="' + Dex.getPokemonIcon(pokemon.name, false, this.engine.dex.modid) + '"></span>';
 		buf += '</span> ';
 
 		// name
@@ -338,13 +337,12 @@
 		buf += '<span class="col typecol">';
 		var types = pokemon.types;
 		for (var i = 0; i < types.length; i++) {
-			buf += Dex.getTypeIcon(types[i]);
+			buf += Dex.getTypeIcon(types[i], null, this.mod);
 		}
 		buf += '</span> ';
-
 		// abilities
 		if (gen >= 3) {
-			var abilities = Dex.forGen(gen).species.get(id).abilities;
+			var abilities = pokemon.abilities;
 			if (gen >= 5) {
 				if (abilities['1']) {
 					buf += '<span class="col twoabilitycol">' + abilities['0'] + '<br />' +
@@ -407,7 +405,7 @@
 
 		// icon
 		buf += '<span class="col iconcol">';
-		buf += '<span style="' + Dex.getPokemonIcon(pokemon.name) + '"></span>';
+		buf += '<span style="' + Dex.getPokemonIcon(pokemon.name, false, this.mod) + '"></span>';
 		buf += '</span> ';
 
 		// name
@@ -475,7 +473,7 @@
 
 		// icon
 		buf += '<span class="col itemiconcol">';
-		buf += '<span style="' + Dex.getItemIcon(item) + '"></span>';
+		buf += '<span style="' + Dex.getItemIcon(item, null, this.mod) + '"></span>';
 		buf += '</span> ';
 
 		// name
@@ -559,7 +557,7 @@
 
 		// type
 		buf += '<span class="col typecol">';
-		buf += Dex.getTypeIcon(move.type);
+		buf += Dex.getTypeIcon(move.type, null, this.engine.dex.modid);
 		buf += Dex.getCategoryIcon(move.category);
 		buf += '</span> ';
 
@@ -571,6 +569,9 @@
 		buf += '<span class="col pplabelcol"><em>PP</em><br />' + pp + '</span> ';
 
 		// desc
+		if (this.engine.dex.gen < 9 && !BattleTeambuilderTable[this.engine.dex.modid]?.overrideMoveInfo?.[id]?.shortDesc) {
+			move.shortDesc = Dex.mod("gen" + this.engine.dex.gen).moves.get(id).shortDesc; // this does not correctly give the gen 1 description, but if it did this would be a fix
+		}
 		buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(move.shortDesc) + '</span> ';
 
 		buf += '</a></li>';
@@ -596,7 +597,7 @@
 
 		// type
 		buf += '<span class="col typecol">';
-		buf += Dex.getTypeIcon(move.type);
+		buf += Dex.getTypeIcon(move.type, null, this.engine.dex.modid);
 		buf += Dex.getCategoryIcon(move.category);
 		buf += '</span> ';
 
diff --git a/play.pokemonshowdown.com/js/storage.js b/play.pokemonshowdown.com/js/storage.js
index 8ca36a79b..58b7be737 100644
--- a/play.pokemonshowdown.com/js/storage.js
+++ b/play.pokemonshowdown.com/js/storage.js
@@ -888,7 +888,15 @@ Storage.fastUnpackTeam = function (buf) {
 	while (true) {
 		var set = {};
 		team.push(set);
-
+		
+		var thisDex = Dex;
+		for (var teamid in this.teams) {
+			var teamData = this.teams[teamid];
+			if (teamData.team === buf && teamData.mod) {
+				thisDex = Dex.mod(teamData.mod);
+			}
+		}
+		
 		// name
 		j = buf.indexOf('|', i);
 		set.name = buf.substring(i, j);
@@ -907,7 +915,7 @@ Storage.fastUnpackTeam = function (buf) {
 		// ability
 		j = buf.indexOf('|', i);
 		var ability = buf.substring(i, j);
-		var species = Dex.species.get(set.species);
+		var species = thisDex.species.get(set.species);
 		if (species.baseSpecies === 'Zygarde' && ability === 'H') ability = 'Power Construct';
 		set.ability = (species.abilities && ['', '0', '1', 'H', 'S'].includes(ability) ? species.abilities[ability] || '!!!ERROR!!!' : ability);
 		i = j + 1;
@@ -999,6 +1007,14 @@ Storage.fastUnpackTeam = function (buf) {
 Storage.unpackTeam = function (buf) {
 	if (!buf) return [];
 
+	var thisDex = Dex;
+	for (var teamid in this.teams) {
+		var teamData = this.teams[teamid];
+		if (teamData.team === buf && teamData.mod) {
+			thisDex = Dex.mod(teamData.mod);
+		}
+	}
+
 	var team = [];
 	var i = 0, j = 0;
 
@@ -1013,25 +1029,25 @@ Storage.unpackTeam = function (buf) {
 
 		// species
 		j = buf.indexOf('|', i);
-		set.species = Dex.species.get(buf.substring(i, j)).name || set.name;
+		set.species = thisDex.species.get(buf.substring(i, j)).name || set.name;
 		i = j + 1;
 
 		// item
 		j = buf.indexOf('|', i);
-		set.item = Dex.items.get(buf.substring(i, j)).name;
+		set.item = thisDex.items.get(buf.substring(i, j)).name;
 		i = j + 1;
 
 		// ability
 		j = buf.indexOf('|', i);
-		var ability = Dex.abilities.get(buf.substring(i, j)).name;
-		var species = Dex.species.get(set.species);
+		var ability = thisDex.abilities.get(buf.substring(i, j)).name;
+		var species = thisDex.species.get(set.species);
 		set.ability = (species.abilities && ability in {'':1, 0:1, 1:1, H:1} ? species.abilities[ability || '0'] : ability);
 		i = j + 1;
 
 		// moves
 		j = buf.indexOf('|', i);
 		set.moves = buf.substring(i, j).split(',').map(function (moveid) {
-			return Dex.moves.get(moveid).name;
+			return thisDex.moves.get(moveid).name;
 		});
 		i = j + 1;
 
@@ -1140,15 +1156,31 @@ Storage.packedTeamNames = function (buf) {
 	return team;
 };
 
-Storage.packedTeamIcons = function (buf) {
+Storage.packedTeamIcons = function (buf, mod) {
 	if (!buf) return '<em>(empty team)</em>';
 
 	return this.packedTeamNames(buf).map(function (species) {
-		return '<span class="picon" style="' + Dex.getPokemonIcon(species) + ';float:left;overflow:visible"><span style="font-size:0px">' + toID(species) + '</span></span>';
+		return '<span class="picon" style="' + Dex.getPokemonIcon(species, false, mod) + ';float:left;overflow:visible"><span style="font-size:0px">' + toID(species) + '</span></span>';
 	}).join('');
 };
 
 Storage.getTeamIcons = function (team) {
+	let formatmod = '';
+	const format = team.format;
+	//Bruteforcing through our list of mods to check if one has our team's format
+	//(For empty teams this isn't necessary)
+	if (team.team) {
+		for (const mod in window.ModConfig) {
+			const modformats = window.ModConfig[mod].formats;
+			for (const formatid in modformats) {
+				if (format === formatid) {
+					formatmod = mod;
+					break;
+				}
+			}
+			if (formatmod) break;
+		}
+	}
 	if (team.iconCache === '!') {
 		// an icon cache of '!' means that not only are the icons not cached,
 		// but the packed team isn't guaranteed to be updated to the latest
@@ -1160,12 +1192,12 @@ Storage.getTeamIcons = function (team) {
 		// a packed team.
 		team.team = Storage.packTeam(Storage.activeSetList);
 		if ('teambuilder' in app.rooms) {
-			return Storage.packedTeamIcons(team.team);
+			return Storage.packedTeamIcons(team.team, formatmod);
 		}
 		Storage.activeSetList = null;
-		team.iconCache = Storage.packedTeamIcons(team.team);
+		team.iconCache = Storage.packedTeamIcons(team.team, formatmod);
 	} else if (!team.iconCache) {
-		team.iconCache = Storage.packedTeamIcons(team.team);
+		team.iconCache = Storage.packedTeamIcons(team.team, formatmod);
 	}
 	return team.iconCache;
 };
@@ -1197,6 +1229,7 @@ Storage.importTeam = function (buffer, teams) {
 	} else if (text.length === 1 || (text.length === 2 && !text[1])) {
 		return Storage.unpackTeam(text[0]);
 	}
+	const mod = (window.room.curTeam && window.room.curTeam.mod) ? window.room.curTeam.mod : "";
 	for (var i = 0; i < text.length; i++) {
 		var line = $.trim(text[i]);
 		if (line === '' || line === '---') {
@@ -1258,11 +1291,29 @@ Storage.importTeam = function (buffer, teams) {
 			var parenIndex = line.lastIndexOf(' (');
 			if (line.substr(line.length - 1) === ')' && parenIndex !== -1) {
 				line = line.substr(0, line.length - 1);
-				curSet.species = Dex.species.get(line.substr(parenIndex + 2)).name;
+				var thisDex = Dex.species.get(line.substr(parenIndex + 2)).exists ? Dex : null;
+				if (!thisDex) {
+					for (var modid in (ModConfig)) {
+						if (Dex.mod(modid).species.get(line.substr(parenIndex + 2)).exists) {
+							thisDex = Dex.mod(modid);
+						}
+					}
+				}
+				curSet.species = thisDex.species.get(line.substr(parenIndex + 2)).name;
 				line = line.substr(0, parenIndex);
 				curSet.name = line;
 			} else {
-				curSet.species = Dex.species.get(line).name;
+				var thisDex = Dex.species.get(line).exists ? Dex : null;
+				if (!thisDex) {
+					for (var modid in (ModConfig)) {
+						if (Dex.mod(modid).species.get(line).exists) {
+							thisDex = Dex.mod(modid);
+						}
+					}
+				}
+				console.log(curSet);
+				console.log(line);
+				curSet.species = thisDex.species.get(line).name;
 				curSet.name = '';
 			}
 		} else if (line.substr(0, 7) === 'Trait: ') {
diff --git a/play.pokemonshowdown.com/src/battle-animations-moves.ts b/play.pokemonshowdown.com/src/battle-animations-moves.ts
index f80971cb8..c6172b929 100644
--- a/play.pokemonshowdown.com/src/battle-animations-moves.ts
+++ b/play.pokemonshowdown.com/src/battle-animations-moves.ts
@@ -711,7 +711,7 @@ export const BattleMoveAnims: AnimTable = {
 					time: 1550,
 				}, 'decel');
 			}
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-hail.png')`, 750, 1, 800);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-hail.png')`, 750, 1, 800);
 		},
 	},
 	sandstorm: {
@@ -1533,7 +1533,7 @@ export const BattleMoveAnims: AnimTable = {
 	orderup: {
 		anim(scene, [attacker, defender]) {
 			const tatsugiriSprite = {
-				url: `https://${Config.routes.client}/sprites/gen5/tatsugiri${['-droopy', '-stretchy', ''][Math.floor(Math.random() * 3)]}.png`,
+				url: `https://${Config.routes.psmain}/sprites/gen5/tatsugiri${['-droopy', '-stretchy', ''][Math.floor(Math.random() * 3)]}.png`,
 				w: 96,
 				h: 96,
 			};
@@ -3610,7 +3610,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	morningsun: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-sunnyday.jpg')`, 700, 0.5);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-sunnyday.jpg')`, 700, 0.5);
 			scene.showEffect('wisp', {
 				x: attacker.x + 40,
 				y: attacker.y - 40,
@@ -3746,7 +3746,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	cosmicpower: {
 		anim(scene, [attacker]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 600, 0.6);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 600, 0.6);
 			scene.showEffect('wisp', {
 				x: attacker.x + 40,
 				y: attacker.y - 40,
@@ -5586,7 +5586,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	seismictoss: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 500, 0.6, 300);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 500, 0.6, 300);
 			scene.showEffect('wisp', {
 				x: defender.x,
 				y: defender.y + 10,
@@ -8418,7 +8418,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	meteormash: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 1000, 0.4);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 1000, 0.4);
 			scene.showEffect(attacker.sp, {
 				x: attacker.leftof(20),
 				y: attacker.y,
@@ -18832,7 +18832,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	psystrike: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-psychicterrain.png')`, 950, 0.6);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-psychicterrain.png')`, 950, 0.6);
 			scene.showEffect('poisonwisp', {
 				x: defender.x - 100,
 				y: defender.y,
@@ -19999,7 +19999,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	wish: {
 		anim(scene, [attacker]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 600, 0.4);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 600, 0.4);
 
 			scene.showEffect('wisp', {
 				x: attacker.x,
@@ -20013,7 +20013,7 @@ export const BattleMoveAnims: AnimTable = {
 			}, 'accel');
 		},
 		residualAnim(scene, [attacker]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 600, 0.4);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 600, 0.4);
 
 			scene.showEffect('wisp', {
 				x: attacker.x,
@@ -21262,7 +21262,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	dracometeor: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 1100, 0.8);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 1100, 0.8);
 			scene.showEffect('flareball', {
 				x: defender.leftof(-200),
 				y: defender.y + 175,
@@ -22767,7 +22767,7 @@ export const BattleMoveAnims: AnimTable = {
 			let ystep = (defender.x - 200 - attacker.x) / 5;
 			let zstep = (defender.z - attacker.z) / 5;
 
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-sunnyday.jpg')`, 900, 0.5);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-sunnyday.jpg')`, 900, 0.5);
 
 			for (let i = 0; i < 5; i++) {
 				scene.showEffect('energyball', {
@@ -23126,7 +23126,7 @@ export const BattleMoveAnims: AnimTable = {
 			let ystep = 20;
 			let zstep = 0;
 
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-sunnyday.jpg')`, 900, 0.5);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-sunnyday.jpg')`, 900, 0.5);
 
 			scene.showEffect('sword', {
 				x: attacker.leftof(10),
@@ -23411,7 +23411,7 @@ export const BattleMoveAnims: AnimTable = {
 			let ystep = (defender.x - 200 - attacker.x) / 5;
 			let zstep = (defender.z - attacker.z) / 5;
 
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-sandstorm.png')`, 900, 0.5);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-sandstorm.png')`, 900, 0.5);
 
 			for (let i = 0; i < 5; i++) {
 				scene.showEffect('mudwisp', {
@@ -23611,7 +23611,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	sheercold: { // Reminder: Improve this later
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/sprites/gen6bgs/bg-icecave.jpg')`, 1000, 0.6);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/sprites/gen6bgs/bg-icecave.jpg')`, 1000, 0.6);
 			scene.showEffect('icicle', {
 				x: defender.x,
 				y: defender.y,
@@ -23629,7 +23629,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	glaciallance: {
 		anim(scene, [attacker, ...defenders]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/sprites/gen6bgs/bg-icecave.jpg')`, 1000, 0.6);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/sprites/gen6bgs/bg-icecave.jpg')`, 1000, 0.6);
 			for (const defender of defenders) {
 				scene.showEffect('icicle', {
 					x: defender.x,
@@ -25944,7 +25944,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	dragonascent: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 1000, 0.7);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 1000, 0.7);
 			scene.showEffect('iceball', {
 				x: attacker.leftof(-25),
 				y: attacker.y + 250,
@@ -29435,7 +29435,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	plasmafists: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/sprites/gen6bgs/bg-earthycave.jpg')`, 2000, 1);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/sprites/gen6bgs/bg-earthycave.jpg')`, 2000, 1);
 			scene.backgroundEffect('#000000', 1000, 0.6);
 			scene.backgroundEffect('#FFFFFF', 300, 0.6, 1000);
 			scene.showEffect('electroball', {
@@ -29678,7 +29678,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	collisioncourse: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-sunnyday.jpg')`, 1300, 0.5);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-sunnyday.jpg')`, 1300, 0.5);
 			scene.showEffect(attacker.sp, {
 				x: attacker.x,
 				y: attacker.y,
@@ -29824,7 +29824,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	electrodrift: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-electricterrain.png')`, 1300, 0.5);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-electricterrain.png')`, 1300, 0.5);
 			scene.showEffect(attacker.sp, {
 				x: attacker.x,
 				y: attacker.y,
@@ -33847,7 +33847,7 @@ export const BattleMoveAnims: AnimTable = {
 	oceanicoperetta: {
 		anim(scene, [attacker, defender]) {
 			scene.backgroundEffect('linear-gradient(#000000 20%, #0000DD)', 2700, 0.4);
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-raindance.jpg')`, 700, 0.2, 2000);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-raindance.jpg')`, 700, 0.2, 2000);
 			scene.showEffect('iceball', {
 				x: attacker.x,
 				y: attacker.y + 120,
@@ -34170,7 +34170,7 @@ export const BattleMoveAnims: AnimTable = {
 	},
 	splinteredstormshards: {
 		anim(scene, [attacker, defender]) {
-			scene.backgroundEffect(`url('https://${Config.routes.client}/sprites/gen6bgs/bg-earthycave.jpg')`, 2700, 0.8, 300);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/sprites/gen6bgs/bg-earthycave.jpg')`, 2700, 0.8, 300);
 			scene.backgroundEffect('linear-gradient(#FFC720 15%, #421800)', 2700, 0.7);
 			scene.backgroundEffect('#ffffff', 400, 0.6, 2500);
 			scene.showEffect('rock3', {
@@ -34893,7 +34893,7 @@ export const BattleMoveAnims: AnimTable = {
 			}
 			const defender = defenders[1] || defenders[0];
 			scene.backgroundEffect('#000000', 300, 0.9);
-			scene.backgroundEffect(`url('https://${Config.routes.client}/sprites/gen6bgs/bg-earthycave.jpg')`, 2000, 0.7, 300);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/sprites/gen6bgs/bg-earthycave.jpg')`, 2000, 0.7, 300);
 			scene.backgroundEffect('linear-gradient(#FB5C1E 20%, #3F1D0F', 2000, 0.6, 300);
 			scene.backgroundEffect('#FFFFFF', 1000, 0.9, 2200);
 			scene.showEffect('shine', {
@@ -35565,8 +35565,8 @@ export const BattleMoveAnims: AnimTable = {
 			let ystep = (defender.x - 200 - attacker.x) / 5;
 			let zstep = (defender.z - attacker.z) / 5;
 
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/weather-trickroom.png')`, 700, 1);
-			scene.backgroundEffect(`url('https://${Config.routes.client}/fx/bg-space.jpg')`, 2500, 1, 700);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/weather-trickroom.png')`, 700, 1);
+			scene.backgroundEffect(`url('https://${Config.routes.psmain}/fx/bg-space.jpg')`, 2500, 1, 700);
 			scene.backgroundEffect('#FFFFFF', 1500, 1, 2500);
 
 			scene.showEffect('flareball', {
diff --git a/play.pokemonshowdown.com/src/battle-animations.ts b/play.pokemonshowdown.com/src/battle-animations.ts
index cbc70486a..75cae9eca 100644
--- a/play.pokemonshowdown.com/src/battle-animations.ts
+++ b/play.pokemonshowdown.com/src/battle-animations.ts
@@ -115,7 +115,7 @@ export class BattleScene implements BattleSceneStub {
 		let numericId = 0;
 		if (battle.id) {
 			numericId = parseInt(battle.id.slice(battle.id.lastIndexOf('-') + 1), 10);
-			if (this.battle.id.includes('digimon')) this.mod = 'digimon';
+			if (this.battle.id.includes('digimon')) this.battle.mod = 'digimon';
 		}
 		if (!numericId) {
 			numericId = Math.floor(Math.random() * 1000000);
@@ -676,14 +676,15 @@ export class BattleScene implements BattleSceneStub {
 				pokemonhtml += `<span class="picon" style="` + Dex.getPokemonIcon('zoroark') + `" title="Unrevealed Illusion user" aria-label="Unrevealed Illusion user"></span>`;
 			} else if (!poke) {
 				pokemonhtml += `<span class="picon" style="` + Dex.getPokemonIcon('pokeball') + `" title="Not revealed" aria-label="Not revealed"></span>`;
-			} else if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) {
-				// in VGC (bring 6 pick 4) and other pick-less-than-you-bring formats, this is
-				// a pokemon that's been brought but not necessarily picked
-				const details = this.getDetailsText(poke);
-				pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon(poke, !side.isFar) + `;opacity:0.6" aria-label="${details}"></span>`;
 			} else {
+				pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon(poke, !side.isFar, this.battle.mod);
 				const details = this.getDetailsText(poke);
-				pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon(poke, !side.isFar) + `" aria-label="${details}"></span>`;
+				if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) {
+					// in VGC (bring 6 pick 4) and other pick-less-than-you-bring formats, this is
+					// a pokemon that's been brought but not necessarily picked
+					pokemonhtml += `;opacity:0.6`;
+				}
+				pokemonhtml += `" aria-label="${details}"></span>`;
 			}
 			if (i % 3 === 2) pokemonhtml += `</div><div class="teamicons">`;
 		}
@@ -842,7 +843,7 @@ export class BattleScene implements BattleSceneStub {
 				let spriteData = Dex.getSpriteData(pokemon, !!spriteIndex, {
 					gen: this.gen,
 					noScale: true,
-					mod: this.mod,
+					mod: this.battle.mod,
 				});
 				let y = 0;
 				let x = 0;
@@ -856,8 +857,11 @@ export class BattleScene implements BattleSceneStub {
 				if (textBuf) textBuf += ' / ';
 				textBuf += pokemon.speciesForme;
 				let url = spriteData.url;
+				var placeholderSprite = spriteData.isFrontSprite // Pet Mods placeholder sprites
+				? "https://play.pokemonshowdown.com/sprites/gen5/substitute.png"
+				: "https://play.pokemonshowdown.com/sprites/gen5-back/substitute.png";
 				// if (this.paused) url.replace('/xyani', '/xy').replace('.gif', '.png');
-				buf += '<img src="' + url + '" width="' + spriteData.w + '" height="' + spriteData.h + '" style="position:absolute;top:' + Math.floor(y - spriteData.h / 2) + 'px;left:' + Math.floor(x - spriteData.w / 2) + 'px" />';
+				buf += '<img src="' + url + '" width="' + spriteData.w + '" height="' + spriteData.h + '" style="position:absolute;top:' + Math.floor(y - spriteData.h / 2) + 'px;left:' + Math.floor(x - spriteData.w / 2) + 'px" onerror="this.src=\'' + placeholderSprite + '\'"/>';
 				buf2 += '<div style="position:absolute;top:' + (y + 45) + 'px;left:' + (x - 40) + 'px;width:80px;font-size:10px;text-align:center;color:#FFF;">';
 				const gender = pokemon.gender;
 				if (gender === 'M' || gender === 'F') {
@@ -1090,7 +1094,7 @@ export class BattleScene implements BattleSceneStub {
 	addPokemonSprite(pokemon: Pokemon) {
 		const sprite = new PokemonSprite(Dex.getSpriteData(pokemon, pokemon.side.isFar, {
 			gen: this.gen,
-			mod: this.mod,
+			mod: this.battle.mod,
 		}), {
 			x: pokemon.side.x,
 			y: pokemon.side.y,
@@ -1395,7 +1399,7 @@ export class BattleScene implements BattleSceneStub {
 
 	typeAnim(pokemon: Pokemon, types: string) {
 		const result = BattleLog.escapeHTML(types).split('/').map(type =>
-			'<img src="' + Dex.resourcePrefix + 'sprites/types/' + encodeURIComponent(type) + '.png" alt="' + type + '" class="pixelated" />'
+			Dex.getTypeIcon(encodeURIComponent(type),null,this.battle.mod)
 		).join(' ');
 		this.resultAnim(pokemon, result, 'neutral');
 	}
@@ -1541,6 +1545,21 @@ export class BattleScene implements BattleSceneStub {
 		return pokemon.sprite.afterMove();
 	}
 
+	updateSpritesForSide(side: Side) {
+		side.missedPokemon?.sprite?.destroy();
+
+		side.missedPokemon = {
+			sprite: new PokemonSprite(null, {
+				x: side.leftof(-100),
+				y: side.y,
+				z: side.z,
+				opacity: 0,
+			}, this, side.isFar),
+		} as any;
+
+		side.missedPokemon.sprite.isMissedPokemon = true;
+	}
+
 	// Misc
 	/////////////////////////////////////////////////////////////////////
 
@@ -1989,7 +2008,7 @@ export class PokemonSprite extends Sprite {
 		if (this.$sub) return;
 		const subsp = Dex.getSpriteData('substitute', this.isFrontSprite, {
 			gen: this.scene.gen,
-			mod: this.scene.mod,
+			mod: this.scene.battle.mod,
 		});
 		this.subsp = subsp;
 		this.$sub = $('<img src="' + subsp.url + '" style="display:block;opacity:0;position:absolute"' + (subsp.pixelated ? ' class="pixelated"' : '') + ' />');
@@ -2104,7 +2123,7 @@ export class PokemonSprite extends Sprite {
 			if (!this.oldsp) this.oldsp = this.sp;
 			this.sp = Dex.getSpriteData(pokemon, this.isFrontSprite, {
 				gen: this.scene.gen,
-				mod: this.scene.mod,
+				mod: this.scene.battle.mod,
 			});
 		} else if (this.oldsp) {
 			this.sp = this.oldsp;
@@ -2509,7 +2528,7 @@ export class PokemonSprite extends Sprite {
 		if (!this.scene.animating && !isPermanent) return;
 		let sp = Dex.getSpriteData(pokemon, this.isFrontSprite, {
 			gen: this.scene.gen,
-			mod: this.scene.mod,
+			mod: this.scene.battle.mod,
 		});
 		let oldsp = this.sp;
 		if (isPermanent) {
@@ -2517,7 +2536,7 @@ export class PokemonSprite extends Sprite {
 				// if a permanent forme change happens while dynamaxed, we need an undynamaxed sprite to go back to
 				this.oldsp = Dex.getSpriteData(pokemon, this.isFrontSprite, {
 					gen: this.scene.gen,
-					mod: this.scene.mod,
+					mod: this.scene.battle.mod,
 					dynamax: false,
 				});
 			} else {
@@ -2818,12 +2837,12 @@ export class PokemonSprite extends Sprite {
 		} else if (pokemon.volatiles.typechange && pokemon.volatiles.typechange[1]) {
 			const types = pokemon.volatiles.typechange[1].split('/');
 			for (const type of types) {
-				status += '<img src="' + Dex.resourcePrefix + 'sprites/types/' + encodeURIComponent(type) + '.png" alt="' + type + '" class="pixelated" /> ';
+				status += Dex.getTypeIcon(encodeURIComponent(type),null,this.scene.battle.mod);
 			}
 		}
 		if (pokemon.volatiles.typeadd) {
 			const type = pokemon.volatiles.typeadd[1];
-			status += '+<img src="' + Dex.resourcePrefix + 'sprites/types/' + type + '.png" alt="' + type + '" class="pixelated" /> ';
+			status += Dex.getTypeIcon(type,null,this.scene.battle.mod);
 		}
 		for (const stat in pokemon.boosts) {
 			if (pokemon.boosts[stat]) {
diff --git a/play.pokemonshowdown.com/src/battle-choices.ts b/play.pokemonshowdown.com/src/battle-choices.ts
index e44fd75a8..1d59b585f 100644
--- a/play.pokemonshowdown.com/src/battle-choices.ts
+++ b/play.pokemonshowdown.com/src/battle-choices.ts
@@ -189,7 +189,7 @@ class BattleChoiceBuilder {
 		}
 		if (choice.choiceType === 'move') {
 			if (!choice.targetLoc && this.requestLength() > 1) {
-				const choosableTargets = ['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe'];
+				const choosableTargets = ['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'anyAlly', 'adjacentFoe'];
 				if (choosableTargets.includes(this.getChosenMove(choice, this.index()).target)) {
 					this.current.move = choice.move;
 					this.current.mega = choice.mega;
diff --git a/play.pokemonshowdown.com/src/battle-dex-data.ts b/play.pokemonshowdown.com/src/battle-dex-data.ts
index 4761bc499..4afbdf5e0 100644
--- a/play.pokemonshowdown.com/src/battle-dex-data.ts
+++ b/play.pokemonshowdown.com/src/battle-dex-data.ts
@@ -1091,6 +1091,7 @@ class Item implements Effect {
 	readonly id: ID;
 	readonly name: string;
 	readonly gen: number;
+	readonly isNonstandard: string;
 	readonly exists: boolean;
 
 	readonly num: number;
@@ -1118,6 +1119,7 @@ class Item implements Effect {
 		this.name = Dex.sanitizeName(name);
 		this.id = id;
 		this.gen = data.gen || 0;
+		this.isNonstandard = data.isNonstandard || undefined;
 		this.exists = ('exists' in data ? !!data.exists : true);
 
 		this.num = data.num || 0;
@@ -1204,7 +1206,7 @@ interface MoveFlags {
 	wind?: 1 | 0;
 }
 
-type MoveTarget = 'normal' | 'any' | 'adjacentAlly' | 'adjacentFoe' | 'adjacentAllyOrSelf' | // single-target
+type MoveTarget = 'normal' | 'any' | 'adjacentAlly' | 'adjacentFoe' | 'adjacentAllyOrSelf' | 'anyAlly' | // single-target
 	'self' | 'randomNormal' | // single-target, automatic
 	'allAdjacent' | 'allAdjacentFoes' | // spread
 	'allySide' | 'foeSide' | 'all'; // side and field
@@ -1231,6 +1233,7 @@ class Move implements Effect {
 	readonly desc: string;
 	readonly shortDesc: string;
 	readonly isNonstandard: string | null;
+	readonly viable: boolean | null;
 	readonly isZ: ID;
 	readonly zMove?: {
 		basePower?: number,
@@ -1239,7 +1242,7 @@ class Move implements Effect {
 	};
 	readonly isMax: boolean | string;
 	readonly maxMove: {basePower: number};
-	readonly ohko: true | 'Ice' | null;
+	readonly ohko: true | TypeName | null;
 	readonly recoil: number[] | null;
 	readonly heal: number[] | null;
 	readonly multihit: number[] | number | null;
@@ -1273,6 +1276,7 @@ class Move implements Effect {
 		this.desc = data.desc;
 		this.shortDesc = data.shortDesc;
 		this.isNonstandard = data.isNonstandard || null;
+		this.viable = ('viable' in data ? !!data.viable : null);
 		this.isZ = data.isZ || '';
 		this.zMove = data.zMove || {};
 		this.ohko = data.ohko || null;
diff --git a/play.pokemonshowdown.com/src/battle-dex-search.ts b/play.pokemonshowdown.com/src/battle-dex-search.ts
index a9ff39557..2f2caab56 100644
--- a/play.pokemonshowdown.com/src/battle-dex-search.ts
+++ b/play.pokemonshowdown.com/src/battle-dex-search.ts
@@ -26,6 +26,7 @@ declare const BattleSearchIndex: [ID, SearchType, number?, number?][];
 declare const BattleSearchIndexOffset: any;
 declare const BattleTeambuilderTable: any;
 
+
 /**
  * Backend for search UIs.
  */
@@ -80,8 +81,11 @@ class DexSearch {
 	 */
 	filters: SearchFilter[] | null = null;
 
+
 	constructor(searchType: SearchType | '' = '', formatid = '' as ID, species = '' as ID) {
 		this.setType(searchType, formatid, species);
+		if (window.room.curTeam.mod) this.dex = Dex.mod(window.room.curTeam.mod);
+
 	}
 
 	getTypedSearch(searchType: SearchType | '', format = '' as ID, speciesOrSet: ID | PokemonSet = '' as ID) {
@@ -402,6 +406,29 @@ class DexSearch {
 				topbufIndex = 2;
 			}
 
+						// determine if the element comes from the current mod
+						const table = BattleTeambuilderTable[window.room.curTeam.mod];
+						if (
+							typeIndex === 1 && (!BattlePokedex[id] || BattlePokedex[id].exists === false) &&
+							(!table || !table.overrideDexInfo || id in table.overrideDexInfo === false)
+						) continue;
+						else if (
+							typeIndex === 5 && (!BattleItems[id] || BattleItems[id].exists === false) &&
+							(!table || !table.overrideItemData || id in table.overrideItemData === false)
+						) continue;
+						else if (
+							typeIndex === 4 && (!BattleMovedex[id] || BattleMovedex[id].exists === false) &&
+							(!table || !table.overrideMoveInfo || id in table.overrideMoveInfo === false)
+						) continue;
+						else if (
+							typeIndex === 6 && (!BattleAbilities[id] || BattleAbilities[id].exists === false) &&
+							(!table || !table.overrideAbilityDesc || id in table.overrideAbilityDesc === false)
+						) continue;
+						else if (
+							typeIndex === 2 && id.replace(id.charAt(0), id.charAt(0).toUpperCase()) in window.BattleTypeChart === false &&
+							(!table || id.replace(id.charAt(0), id.charAt(0).toUpperCase()) in table.overrideTypeChart === false)
+						) continue;
+
 			if (illegal && typeIndex === searchTypeIndex) {
 				// Always show illegal results under legal results.
 				// This is done by putting legal results (and the type header)
@@ -454,6 +481,28 @@ class DexSearch {
 		let buf: SearchRow[] = [];
 		let illegalBuf: SearchRow[] = [];
 		let illegal = this.typedSearch?.illegalReasons;
+		// Change object to look in if using a mod
+		let pokedex = BattlePokedex;
+		let moveDex = BattleMovedex;
+		if (window.room.curTeam.mod) {
+			pokedex = {};
+			moveDex = {};
+			const table = BattleTeambuilderTable[window.room.curTeam.mod];
+			for (const id in table.overrideDexInfo) {
+				pokedex[id] = {
+					types: table.overrideDexInfo[id].types,
+					abilities: table.overrideDexInfo[id].abilities,
+				};
+			}
+			for (const id in table.overrideMoveInfo) {
+				moveDex[id] = {
+					type: table.overrideMoveInfo.type,
+					category: table.overrideMoveInfo.category,
+				};
+			}
+			pokedex = {...pokedex, ...BattlePokedex};
+			moveDex = {...moveDex, ...BattleMovedex};
+		}
 		if (searchType === 'pokemon') {
 			switch (fType) {
 			case 'type':
@@ -538,6 +587,12 @@ abstract class BattleTypedSearch<T extends SearchType> {
 	 * "Doubles" and "Let's Go" from the name.
 	 */
 	format = '' as ID;
+	/**
+	*
+	* mod formats can set the format variable to a standard format, so modFormat
+	* keeps track of the original format in such a case
+	*/
+   modFormat = '' as ID;
 	/**
 	 * `species` is the second of two base filters. It constrains results to
 	 * things that species can use, and affects the default sort.
@@ -548,6 +603,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
 	 * (Abilities/items can affect what moves are sorted as usable.)
 	 */
 	set: PokemonSet | null = null;
+	mod = '';
 
 	protected formatType: 'doubles' | 'bdsp' | 'bdspdoubles' | 'bw1' | 'letsgo' | 'metronome' | 'natdex' | 'nfe' |
 	'ssdlc1' | 'ssdlc1doubles' | 'predlc' | 'predlcdoubles' | 'predlcnatdex' | 'svdlc1' | 'svdlc1doubles' |
@@ -573,11 +629,38 @@ abstract class BattleTypedSearch<T extends SearchType> {
 
 		this.baseResults = null;
 		this.baseIllegalResults = null;
-
+		this.modFormat = format;
+		let gen = 9;
+		const ClientMods = window.ModConfig;
 		if (format.slice(0, 3) === 'gen') {
 			const gen = (Number(format.charAt(3)) || 6);
-			format = (format.slice(4) || 'customgame') as ID;
+			// format = (format.slice(4) || 'customgame') as ID;
 			this.dex = Dex.forGen(gen);
+			let mod = '';
+			let overrideFormat = '';
+			let modFormatType = '';
+			for (const modid in (ClientMods)) {
+				for (const formatid in ClientMods[modid].formats) {
+					if (formatid === format || format.slice(4) === formatid) {
+						if (format.slice(4) === formatid) this.modFormat = formatid;
+						mod = modid;
+						const formatTable = ClientMods[modid].formats[formatid];
+						if (mod && formatTable.teambuilderFormat) overrideFormat = toID(formatTable.teambuilderFormat);
+						if (mod && formatTable.formatType) modFormatType = toID(formatTable.formatType);
+						break;
+					}
+				}
+			}
+			if (mod) {
+				this.dex = Dex.mod(mod as ID);
+				this.dex.gen = gen;
+				this.mod = mod;
+			} else {
+				this.dex = Dex.forGen(gen);
+			}
+			if (overrideFormat) format = overrideFormat as ID;
+			else format = (format.slice(4) || 'customgame') as ID;
+			if (modFormatType) this.formatType = modFormatType as 'doubles' | 'letsgo' | 'metronome' | 'natdex' | 'nfe' | 'dlc1' | 'dlc1doubles' | null;
 		} else if (!format) {
 			this.dex = Dex;
 		}
@@ -820,10 +903,28 @@ abstract class BattleTypedSearch<T extends SearchType> {
 			if (this.formatType === 'bw1') table = table['gen5bw1'];
 			let learnset = table.learnsets[learnsetid];
 			const eggMovesOnly = this.eggMovesOnly(learnsetid, speciesid);
-			if (learnset && (moveid in learnset) && (!this.format.startsWith('tradebacks') ? learnset[moveid].includes(genChar) :
-				learnset[moveid].includes(genChar) || (learnset[moveid].includes(`${gen + 1}`) && move.gen === gen)) &&
+			if (this.mod) {
+				const overrideLearnsets = BattleTeambuilderTable[this.mod].overrideLearnsets;
+				if (overrideLearnsets[learnsetid]) {
+					if (!learnset) learnset = overrideLearnsets[learnsetid]; //Didn't have learnset and mod gave it one
+					learnset = JSON.parse(JSON.stringify(learnset));
+					for (const learnedMove in overrideLearnsets[learnsetid]) learnset[learnedMove] = overrideLearnsets[learnsetid][learnedMove];
+				}
+			}
+			try {
+				if (!Object.keys(learnset).length) { //Doesn't have learnset but one is loaded; some other mod gave it one
+					learnsetid = toID(this.dex.species.get(learnsetid).baseSpecies);
+				}
+			} catch (e) {
+				console.log("Error: Unable to load learnset data for " + learnsetid + " in " + this.mod);
+			}
+
+			// Modified this function to account for pet mods with tradebacks enabled
+			const tradebacksMod = ['gen1expansionpack', 'gen1burgundy'];
+			if (learnset && (moveid in learnset) && (!(this.format.startsWith('tradebacks') || tradebacksMod.includes(this.mod)) ? learnset[moveid].includes(genChar) :
+				(learnset[moveid].includes(genChar) || (learnset[moveid].includes(`${gen + 1}`) && move.gen === gen)) &&
 				(!eggMovesOnly || (learnset[moveid].includes('e') && this.dex.gen === 9))
-				) {
+				) {=
 				return true;
 			}
 			learnsetid = this.nextLearnsetid(learnsetid, speciesid, true);
@@ -834,7 +935,9 @@ abstract class BattleTypedSearch<T extends SearchType> {
 		if (this.formatType === 'metronome') {
 			return pokemon.num >= 0 ? String(pokemon.num) : pokemon.tier;
 		}
+		const modFormatTable = this.mod ? window.ModConfig[this.mod].formats[this.modFormat] : {};
 		let table = window.BattleTeambuilderTable;
+		if (this.mod) table = modFormatTable.gameType !== 'doubles' ? BattleTeambuilderTable[this.mod] : BattleTeambuilderTable[this.mod].doubles;
 		const gen = this.dex.gen;
 		const tableKey = this.formatType === 'doubles' ? `gen${gen}doubles` :
 			this.formatType === 'letsgo' ? 'gen7letsgo' :
@@ -892,7 +995,8 @@ abstract class BattleTypedSearch<T extends SearchType> {
 class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
 	sortRow: SearchRow = ['sortpokemon', ''];
 	getTable() {
-		return BattlePokedex;
+		if (!this.mod) return BattlePokedex;
+		else return {...BattleTeambuilderTable[this.mod].overrideDexInfo, ...BattlePokedex};
 	}
 	getDefaultResults(): SearchRow[] {
 		let results: SearchRow[] = [];
@@ -946,9 +1050,11 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
 		const isHackmons = format.includes('hackmons') || format.endsWith('bh');
 		let isDoublesOrBS = isVGCOrBS || this.formatType?.includes('doubles');
 		const dex = this.dex;
-
+		const modFormatTable = this.mod ? window.ModConfig[this.mod].formats[this.modFormat] : {};
 		let table = BattleTeambuilderTable;
-		if ((format.endsWith('cap') || format.endsWith('caplc')) && dex.gen < 9) {
+		if (this.mod) {
+			table = modFormatTable.gameType !== 'doubles' ? BattleTeambuilderTable[this.mod] : BattleTeambuilderTable[this.mod].doubles;
+		} else if ((format.endsWith('cap') || format.endsWith('caplc')) && dex.gen < 9) {
 			table = table['gen' + dex.gen];
 		} else if (isVGCOrBS) {
 			table = table['gen' + dex.gen + 'vgc'];
@@ -1103,11 +1209,45 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
 				});
 			}
 		}
+		if (this.mod && !table.customTierSet) {
+			table.customTierSet = table.customTiers.map((r: any) => {
+				if (typeof r === 'string') return ['pokemon', r];
+				return [r[0], r[1]];
+			});
+			table.customTiers = null;
+		}
+		let customTierSet: SearchRow[] = table.customTierSet;
+		if (customTierSet) {
+			tierSet = customTierSet.concat(tierSet);
+			if (modFormatTable.bans.length > 0 && !modFormatTable.bans.includes("All Pokemon")) {
+				tierSet = tierSet.filter(([type, id]) => {
+					let banned = modFormatTable.bans;
+					return !(banned.includes(id));
+				});
+			} else if (modFormatTable.unbans.length > 0 && modFormatTable.bans.includes("All Pokemon")) {
+				tierSet = tierSet.filter(([type, id]) => {
+					let unbanned = modFormatTable.unbans;
+					return (unbanned.includes(id) || type === 'header');
+				});
+			}
+			let headerCount = 0;
+			let lastHeader = '';
+			const emptyHeaders: string[] = [];
+			for (const i in tierSet) {
+				headerCount = tierSet[i][0] === 'header' ? headerCount + 1 : 0;
+				if (headerCount > 1) emptyHeaders.push(lastHeader);
+				if (headerCount > 0) lastHeader = tierSet[i][1];
+			}
+			if (headerCount === 1) emptyHeaders.push(lastHeader);
+			tierSet = tierSet.filter(([type, id]) => {
+				return (type !== 'header' || !emptyHeaders.includes(id));
+			});
+		}
 		if (format === 'zu' && dex.gen === 5 && table.gen5zuBans) {
 			tierSet = tierSet.filter(([type, id]) => {
 				if (id in table.gen5zuBans) return false;
 				return true;
-			});
+			}
 		}
 
 		// Filter out Gmax Pokemon from standard tier selection
@@ -1147,14 +1287,27 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
 	}
 	sort(results: SearchRow[], sortCol: string, reverseSort?: boolean) {
 		const sortOrder = reverseSort ? -1 : 1;
+		const table = !this.mod ? '' : BattleTeambuilderTable[this.mod].overrideDexInfo;
 		if (['hp', 'atk', 'def', 'spa', 'spd', 'spe'].includes(sortCol)) {
 			return results.sort(([rowType1, id1], [rowType2, id2]) => {
+				let pokedex1 = BattlePokedex;
+				let pokedex2 = BattlePokedex;
+				if (this.mod) {
+					if (table[id1] && table[id1].baseStats) pokedex1 = table;
+					if (table[id2] && table[id2].baseStats) pokedex2 = table;
+				}
 				const stat1 = this.dex.species.get(id1).baseStats[sortCol as StatName];
 				const stat2 = this.dex.species.get(id2).baseStats[sortCol as StatName];
 				return (stat2 - stat1) * sortOrder;
 			});
 		} else if (sortCol === 'bst') {
 			return results.sort(([rowType1, id1], [rowType2, id2]) => {
+				let pokedex1 = BattlePokedex;
+				let pokedex2 = BattlePokedex;
+				if (this.mod) {
+					if (table[id1] && table[id1].baseStats) pokedex1 = table;
+					if (table[id2] && table[id2].baseStats) pokedex2 = table;
+				}
 				const base1 = this.dex.species.get(id1).baseStats;
 				const base2 = this.dex.species.get(id2).baseStats;
 				let bst1 = base1.hp + base1.atk + base1.def + base1.spa + base1.spd + base1.spe;
@@ -1178,8 +1331,9 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
 
 class BattleAbilitySearch extends BattleTypedSearch<'ability'> {
 	getTable() {
-		return BattleAbilities;
-	}
+		if (!this.mod) return BattleAbilities;
+		else return {...BattleTeambuilderTable[this.mod].fullAbilityName, ...BattleAbilities};
+		}
 	getDefaultResults(): SearchRow[] {
 		const results: SearchRow[] = [];
 		for (let id in BattleAbilities) {
@@ -1265,11 +1419,14 @@ class BattleAbilitySearch extends BattleTypedSearch<'ability'> {
 
 class BattleItemSearch extends BattleTypedSearch<'item'> {
 	getTable() {
-		return BattleItems;
+		if (!this.mod) return BattleItems;
+		else return {...BattleTeambuilderTable[this.mod].overrideItemData, ...BattleItems};
 	}
 	getDefaultResults(): SearchRow[] {
 		let table = BattleTeambuilderTable;
-		if (this.formatType?.startsWith('bdsp')) {
+		if (this.mod) {
+			table = table[this.mod];
+		} else if (this.formatType?.startsWith('bdsp')) {
 			table = table['gen8bdsp'];
 		} else if (this.formatType === 'bw1') {
 			table = table['gen5bw1'];
@@ -1292,19 +1449,30 @@ class BattleItemSearch extends BattleTypedSearch<'item'> {
 		return table.itemSet;
 	}
 	getBaseResults(): SearchRow[] {
-		if (!this.species) return this.getDefaultResults();
-		const speciesName = this.dex.species.get(this.species).name;
 		const results = this.getDefaultResults();
+		const species = this.dex.species.get(this.species);
 		const speciesSpecific: SearchRow[] = [];
-		for (const row of results) {
+		for (let i = results.length - 1; i > 0; i--) {
+			const row = results[i];
 			if (row[0] !== 'item') continue;
-			if (this.dex.items.get(row[1]).itemUser?.includes(speciesName)) {
+			const id = row[1];
+			let item = this.dex.items.get(id);
+			if (!item.exists || item.isNonstandard) {
+				if (item.isNonstandard !== "Past" || this.formatType !== 'natdex') {
+					results.splice(i, 1);
+					continue;
+				}
+			}
+			if (item.itemUser?.includes(species.name)) {
+				speciesSpecific.push(row);
+			}
+			if(id === 'boosterenergy' && species.tags?.includes('Paradox')) {
 				speciesSpecific.push(row);
 			}
 		}
 		if (speciesSpecific.length) {
 			return [
-				['header', "Specific to " + speciesName],
+				['header', "Specific to " + species.name],
 				...speciesSpecific,
 				...results,
 			];
@@ -1332,12 +1500,13 @@ class BattleItemSearch extends BattleTypedSearch<'item'> {
 class BattleMoveSearch extends BattleTypedSearch<'move'> {
 	sortRow: SearchRow = ['sortmove', ''];
 	getTable() {
-		return BattleMovedex;
+		if (!this.mod) return BattleMovedex;
+		else return {...BattleTeambuilderTable[this.mod].overrideMoveInfo, ...BattleMovedex};
 	}
 	getDefaultResults(): SearchRow[] {
 		let results: SearchRow[] = [];
 		results.push(['header', "Moves"]);
-		for (let id in BattleMovedex) {
+		for (let id in this.getTable()) {
 			switch (id) {
 			case 'paleowave':
 				results.push(['header', "CAP moves"]);
@@ -1354,6 +1523,12 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 
 		let abilityid: ID = set ? toID(set.ability) : '' as ID;
 		const itemid: ID = set ? toID(set.item) : '' as ID;
+		
+		// Check if mod declared forced viability
+		if (this.mod && id in BattleTeambuilderTable[this.mod].overrideMoveInfo) {
+			if(BattleTeambuilderTable[this.mod].overrideMoveInfo[id].viable === true) return true;
+			if(BattleTeambuilderTable[this.mod].overrideMoveInfo[id].viable === false) return false;
+		}
 
 		if (dex.gen === 1) {
 			// Usually not useless for Gen 1
@@ -1405,7 +1580,9 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 
 		if (itemid === 'pidgeotite') abilityid = 'noguard' as ID;
 		if (itemid === 'blastoisinite') abilityid = 'megalauncher' as ID;
-		if (itemid === 'aerodactylite') abilityid = 'toughclaws' as ID;
+		if (itemid === 'heracronite') abilityid = 'skilllink' as ID;
+		if (itemid === 'cameruptite') abilityid = 'sheerforce' as ID;
+		if (itemid === 'aerodactylite' || itemid === 'charizardmegax') abilityid = 'toughclaws' as ID;
 		if (itemid === 'glalitite') abilityid = 'refrigerate' as ID;
 
 		switch (id) {
@@ -1455,17 +1632,15 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		case 'hex':
 			return !moves.includes('infernalparade');
 		case 'hiddenpowerelectric':
-			return (dex.gen < 4 && !moves.includes('thunderpunch')) && !moves.includes('thunderbolt');
+			return !(moves.includes('thunderbolt') || (dex.gen < 4 && moves.includes('thunderpunch')));
 		case 'hiddenpowerfighting':
-			return (dex.gen < 4 && !moves.includes('brickbreak')) && !moves.includes('aurasphere') && !moves.includes('focusblast');
+			return !(moves.includes('aurasphere') || moves.includes('focusblast') || (dex.gen < 4 && moves.includes('brickbreak')));
 		case 'hiddenpowerfire':
-			return (dex.gen < 4 && !moves.includes('firepunch')) && !moves.includes('flamethrower') &&
-				!moves.includes('mysticalfire') && !moves.includes('burningjealousy');
+			return !(moves.includes('flamethrower') || moves.includes('mysticalfire') || (dex.gen < 4 && moves.includes('firepunch')));
 		case 'hiddenpowergrass':
-			return !moves.includes('energyball') && !moves.includes('grassknot') && !moves.includes('gigadrain');
+			return !(moves.includes('energyball') || moves.includes('grassknot') || moves.includes('gigadrain'));
 		case 'hiddenpowerice':
-			return !moves.includes('icebeam') && (dex.gen < 4 && !moves.includes('icepunch')) ||
-				(dex.gen > 5 && !moves.includes('aurorabeam') && !moves.includes('glaciate'));
+			return !(moves.includes('icebeam') || (dex.gen > 5 && (moves.includes('aurorabeam') || moves.includes('glaciate'))) || (dex.gen < 4 && moves.includes('icepunch')));
 		case 'hiddenpowerflying':
 			return dex.gen < 4 && !moves.includes('drillpeck');
 		case 'hiddenpowerbug':
@@ -1508,7 +1683,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		case 'petaldance':
 			return abilityid === 'owntempo';
 		case 'phantomforce':
-			return (!moves.includes('poltergeist') && !moves.includes('shadowclaw')) || this.formatType === 'doubles';
+			return !(moves.includes('shadowforce') || moves.includes('poltergeist') || moves.includes('shadowclaw')) || this.formatType === 'doubles';
 		case 'poisonfang':
 			return species.types.includes('Poison') && !moves.includes('gunkshot') && !moves.includes('poisonjab');
 		case 'relicsong':
@@ -1562,6 +1737,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 
 		const move = dex.moves.get(id);
 		if (!move.exists) return true;
+		if (!BattleMovedex[id].exists) return true; //Flag custom moves as viable by default
 		if ((move.status === 'slp' || id === 'yawn') && dex.gen === 9 && !this.formatType) {
 			return false;
 		}
@@ -1582,6 +1758,9 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		if (move.flags['slicing'] && abilityid === 'sharpness') {
 			return true;
 		}
+		if (move.basePower < 75 && !(abilityid === 'technician' && move.basePower <= 60 && move.basePower >= 50)) {
+			return BattleMoveSearch.GOOD_WEAK_MOVES.includes(id);
+		}
 		return !BattleMoveSearch.BAD_STRONG_MOVES.includes(id);
 	}
 	static readonly GOOD_STATUS_MOVES = [
@@ -1602,11 +1781,24 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		let species = dex.species.get(this.species);
 		const format = this.format;
 		const isHackmons = (format.includes('hackmons') || format.endsWith('bh'));
-		const isSTABmons = (format.includes('stabmons') || format === 'staaabmons');
-		const isTradebacks = format.includes('tradebacks');
+		const isSTABmons = (format.includes('stabmons') || format.includes('stylemons')|| format === 'staaabmons');
+		const isTradebacks = (format.includes('tradebacks') || this.mod === 'gen1expansionpack' || this.mod === 'gen1burgundy');
 		const regionBornLegality = dex.gen >= 6 &&
 			(/^battle(spot|stadium|festival)/.test(format) || format.startsWith('bss') ||
 				format.startsWith('vgc') || (dex.gen === 9 && this.formatType !== 'natdex'));
+		
+				let hasOwnUsefulCheck = false;
+				switch(typeof window.ModConfig[this.mod]?.moveIsNotUseless){
+					case 'string':
+						hasOwnUsefulCheck = true;
+						const usefulCheck = JSON.parse(window.ModConfig[this.mod].moveIsNotUseless);
+						const checkParameters = usefulCheck.substring(usefulCheck.indexOf('(')+1,usefulCheck.indexOf(')')).split(',');
+						window.ModConfig[this.mod].moveIsNotUseless = new Function(...checkParameters, usefulCheck.substring(usefulCheck.indexOf('{')));
+						break;
+					case 'function':
+						hasOwnUsefulCheck = true;
+						break;
+				}
 
 		let learnsetid = this.firstLearnsetid(species.id);
 		let moves: string[] = [];
@@ -1622,7 +1814,19 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		if (this.formatType?.startsWith('svdlc1')) lsetTable = lsetTable['gen9dlc1'];
 		while (learnsetid) {
 			let learnset = lsetTable.learnsets[learnsetid];
+			if (this.mod) {
+				const overrideLearnsets = BattleTeambuilderTable[this.mod].overrideLearnsets;
+				if (overrideLearnsets[learnsetid]) {
+					if(!learnset) learnset = overrideLearnsets[learnsetid]; //Didn't have learnset and mod gave it one
+					learnset = JSON.parse(JSON.stringify(learnset));
+					for (const moveid in overrideLearnsets[learnsetid]) learnset[moveid] = overrideLearnsets[learnsetid][moveid];
+				}
+			}
 			if (learnset) {
+				if (!Object.keys(learnset).length) { //Doesn't have learnset but one is loaded; some other mod gave it one
+					learnsetid = toID(this.dex.species.get(learnsetid).baseSpecies);
+					continue;
+				}
 				for (let moveid in learnset) {
 					let learnsetEntry = learnset[moveid];
 					const move = dex.moves.get(moveid);
@@ -1677,22 +1881,22 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		}
 		if (sketch || isHackmons) {
 			if (isHackmons) moves = [];
-			for (let id in BattleMovedex) {
+			for (let id in this.getTable()) {
 				if (!format.startsWith('cap') && (id === 'paleowave' || id === 'shadowstrike')) continue;
-				const move = dex.moves.get(id);
-				if (move.gen > dex.gen) continue;
+				let move = dex.moves.get(id);
+				if (!move.exists || moves.includes(id) || move.gen > dex.gen) continue;
 				if (sketch) {
 					if (move.flags['nosketch'] || move.isMax || move.isZ) continue;
 					if (move.isNonstandard && move.isNonstandard !== 'Past') continue;
 					if (move.isNonstandard === 'Past' && this.formatType !== 'natdex') continue;
-					sketchMoves.push(move.id);
+					sketchMoves.push(id);
 				} else {
 					if (!(dex.gen < 8 || this.formatType === 'natdex') && move.isZ) continue;
 					if (typeof move.isMax === 'string') continue;
 					if (move.isMax && dex.gen > 8) continue;
 					if (move.isNonstandard === 'Past' && this.formatType !== 'natdex') continue;
 					if (move.isNonstandard === 'LGPE' && this.formatType !== 'letsgo') continue;
-					moves.push(move.id);
+					moves.push(id);
 				}
 			}
 		}
@@ -1750,7 +1954,11 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 		let usableMoves: SearchRow[] = [];
 		let uselessMoves: SearchRow[] = [];
 		for (const id of moves) {
-			const isUsable = this.moveIsNotUseless(id as ID, species, moves, this.set);
+			let isUsable = this.moveIsNotUseless(id as ID, species, moves, this.set);
+			if (hasOwnUsefulCheck) {
+				const modIsUsable = window.ModConfig[this.mod].moveIsNotUseless.apply(window.ModConfig[this.mod], [id as ID, species, moves, this.set, this.dex]);
+				if (typeof modIsUsable === 'boolean' && modIsUsable !== isUsable) isUsable = modIsUsable;
+			}
 			if (isUsable) {
 				if (!usableMoves.length) usableMoves.push(['header', "Moves"]);
 				usableMoves.push(['move', id as ID]);
@@ -1764,7 +1972,11 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
 			uselessMoves.push(['header', "Useless sketched moves"]);
 		}
 		for (const id of sketchMoves) {
-			const isUsable = this.moveIsNotUseless(id as ID, species, sketchMoves, this.set);
+			let isUsable = this.moveIsNotUseless(id as ID, species, sketchMoves, this.set);
+			if (hasOwnUsefulCheck) {
+				const modIsUsable = window.ModConfig[this.mod].moveIsNotUseless.apply(window.ModConfig[this.mod], [id as ID, species, moves, this.set, this.dex]);
+				if (typeof modIsUsable === 'boolean' && modIsUsable !== isUsable) isUsable = modIsUsable;
+			}
 			if (isUsable) {
 				usableMoves.push(['move', id as ID]);
 			} else {
@@ -1860,7 +2072,8 @@ class BattleCategorySearch extends BattleTypedSearch<'category'> {
 
 class BattleTypeSearch extends BattleTypedSearch<'type'> {
 	getTable() {
-		return window.BattleTypeChart;
+		if (!this.mod) return window.BattleTypeChart;
+		else return {...BattleTeambuilderTable[this.mod].overrideTypeChart, ...window.BattleTypeChart};
 	}
 	getDefaultResults(): SearchRow[] {
 		const results: SearchRow[] = [];
diff --git a/play.pokemonshowdown.com/src/battle-dex.ts b/play.pokemonshowdown.com/src/battle-dex.ts
index e45aacc0f..2c11241dc 100644
--- a/play.pokemonshowdown.com/src/battle-dex.ts
+++ b/play.pokemonshowdown.com/src/battle-dex.ts
@@ -179,15 +179,19 @@ const Dex = new class implements ModdedDex {
 
 	pokeballs: string[] | null = null;
 
+	//TODO we might want to move this to something like data/petmods
+	readonly modResourcePrefix = 'https://raw.githubusercontent.com/scoopapa/dh2/master/data/mods/';
+
+
 	resourcePrefix = (() => {
 		let prefix = '';
 		if (window.document?.location?.protocol !== 'http:') prefix = 'https:';
-		return `${prefix}//${window.Config ? Config.routes.client : 'play.pokemonshowdown.com'}/`;
+		return `${prefix}//${'play.pokemonshowdown.com'}/`;
 	})();
 
 	fxPrefix = (() => {
 		const protocol = (window.document?.location?.protocol !== 'http:') ? 'https:' : '';
-		return `${protocol}//${window.Config ? Config.routes.client : 'play.pokemonshowdown.com'}/fx/`;
+		return `${protocol}//${'play.pokemonshowdown.com'}/fx/`;
 	})();
 
 	loadedSpriteData = {xy: 1, bw: 0};
@@ -302,8 +306,8 @@ const Dex = new class implements ModdedDex {
 					basePower: Number(id.slice(11)),
 				};
 			}
-
 			if (!data) data = {exists: false};
+			
 			let move = new Move(id, name, data);
 			window.BattleMovedex[id] = move;
 			return move;
@@ -315,6 +319,16 @@ const Dex = new class implements ModdedDex {
 			'Fire', 'Water', 'Grass', 'Electric', 'Ice', 'Psychic', 'Dark', 'Dragon',
 		].includes(type) ? 'Special' : 'Physical';
 	}
+	getKEPCategory(type: string) {
+		return [
+			'Fire', 'Water', 'Grass', 'Electric', 'Ice', 'Psychic', 'Dark', 'Dragon', 'Fairy'
+		].includes(type) ? 'Special' : 'Physical';
+	}
+	getCSICategory(type: string) {
+		return [
+			'Fire', 'Water', 'Grass', 'Electric', 'Ice', 'Psychic', 'Dark', 'Dragon', 'Cosmic'
+		].includes(type) ? 'Special' : 'Physical';
+	}
 
 	items = {
 		get: (nameOrItem: string | Item | null | undefined): Item => {
@@ -353,7 +367,6 @@ const Dex = new class implements ModdedDex {
 			if (!window.BattleAbilities) window.BattleAbilities = {};
 			let data = window.BattleAbilities[id];
 			if (data && typeof data.exists === 'boolean') return data;
-			if (!data) data = {exists: false};
 			let ability = new Ability(id, name, data);
 			window.BattleAbilities[id] = ability;
 			return ability;
@@ -361,7 +374,7 @@ const Dex = new class implements ModdedDex {
 	};
 
 	species = {
-		get: (nameOrSpecies: string | Species | null | undefined): Species => {
+		get: (nameOrSpecies: string | Species | null | undefined, modded = false, debug = ""): Species => {
 			if (nameOrSpecies && typeof nameOrSpecies !== 'string') {
 				// TODO: don't accept Species' here
 				return nameOrSpecies;
@@ -384,7 +397,6 @@ const Dex = new class implements ModdedDex {
 			}
 			if (!window.BattlePokedex) window.BattlePokedex = {};
 			let data = window.BattlePokedex[id];
-
 			let species: Species;
 			if (data && typeof data.exists === 'boolean') {
 				species = data;
@@ -460,6 +472,22 @@ const Dex = new class implements ModdedDex {
 		}
 		return false;
 	}
+	// getSpriteMod is used to find the correct mod folder for the sprite url to use
+	// id is the name of the pokemon, type, or item. folder refers to "front", or "back-shiny" etc. overrideStandard is false for custom elements and true for canon elements
+	getSpriteMod(optionsMod: string, spriteId: string, filepath: string, overrideStandard: boolean = false) {
+		if (!window.ModSprites[spriteId]) return '';
+		if ((!optionsMod || !window.ModSprites[spriteId][optionsMod]) && !overrideStandard) { // for custom elements only, it will use sprites from another mod if the mod provided doesn't have one
+			for (const modName in window.ModSprites[spriteId]) {
+				if (window.ModSprites[spriteId][modName].includes(filepath)) return modName;
+				if (window.ModSprites[spriteId][modName].includes('ani' + filepath)) return modName;
+			}
+		}
+		if (optionsMod && window.ModSprites[spriteId][optionsMod]) {		
+			if (window.ModSprites[spriteId][optionsMod].includes('ani' + filepath)) return optionsMod;
+			if (window.ModSprites[spriteId][optionsMod].includes(filepath)) return optionsMod;
+		}
+		return ''; // must be a real Pokemon or not have custom sprite data
+	}
 
 	loadSpriteData(gen: 'xy' | 'bw') {
 		if (this.loadedSpriteData[gen]) return;
@@ -473,16 +501,18 @@ const Dex = new class implements ModdedDex {
 		el.src = path + 'data/pokedex-mini-bw.js' + qs;
 		document.getElementsByTagName('body')[0].appendChild(el);
 	}
+
 	getSpriteData(pokemon: Pokemon | Species | string, isFront: boolean, options: {
 		gen?: number,
 		shiny?: boolean,
 		gender?: GenderName,
 		afd?: boolean,
 		noScale?: boolean,
-		mod?: string,
+		mod: string,
 		dynamax?: boolean,
-	} = {gen: 6}) {
-		const mechanicsGen = options.gen || 6;
+	} = {gen: 6, mod: ''}) {
+		let mechanicsGen = options.gen || 6;
+		if (options.mod && window.ModConfig[options.mod].spriteGen) mechanicsGen = window.ModConfig[options.mod].spriteGen;
 		let isDynamax = !!options.dynamax;
 		if (pokemon instanceof Pokemon) {
 			if (pokemon.volatiles.transform) {
@@ -502,6 +532,19 @@ const Dex = new class implements ModdedDex {
 			}
 			pokemon = pokemon.getSpeciesForme() + (isGigantamax ? '-Gmax' : '');
 		}
+		const modSpecies = Dex.species.get(pokemon);
+		let resourcePrefix = Dex.resourcePrefix;
+		let spriteDir = 'sprites/';
+		let hasCustomSprite = false;
+		let modSpriteId = toID(modSpecies.spriteid);		
+		options.mod = this.getSpriteMod(options.mod, modSpriteId, isFront ? 'front' : 'back', modSpecies.exists);
+		if (options.mod) {
+			resourcePrefix = Dex.modResourcePrefix;
+			spriteDir = `${options.mod}/sprites/`;
+			hasCustomSprite = true;
+			if (this.getSpriteMod(options.mod, modSpriteId, (isFront ? 'front' : 'back') + '-shiny', modSpecies.exists) === '') options.shiny = false;
+		}
+
 		const species = Dex.species.get(pokemon);
 		// Gmax sprites are already extremely large, so we don't need to double.
 		if (species.name.endsWith('-Gmax')) isDynamax = false;
@@ -510,7 +553,7 @@ const Dex = new class implements ModdedDex {
 			w: 96,
 			h: 96,
 			y: 0,
-			url: Dex.resourcePrefix + 'sprites/',
+			url: resourcePrefix + spriteDir,
 			pixelated: true,
 			isFrontSprite: false,
 			cryurl: '',
@@ -527,7 +570,7 @@ const Dex = new class implements ModdedDex {
 			dir = '-back';
 			facing = 'back';
 		}
-
+		if (hasCustomSprite) dir = isFront ? 'front' : 'back';
 		// Decide which gen sprites to use.
 		//
 		// There are several different generations we care about here:
@@ -621,13 +664,29 @@ const Dex = new class implements ModdedDex {
 		}
 
 		// Mod Cries
-		if (options.mod) {
-			spriteData.cryurl = `sprites/${options.mod}/audio/${toID(species.baseSpecies)}`;
-			spriteData.cryurl += '.mp3';
+		if (options.mod === 'digimon') {
+			spriteData.cryurl = `sprites/${options.mod}/audio/${toID(species.baseSpecies)}.mp3`;
+		}
+		//If we already have a cry url we load from the main server, otherwise we try to search for the presence of a custom cry
+		if (!(spriteData.cryurl &&= 'https://' + Config.routes.psmain + '/' + spriteData.cryurl)) {			
+			//For whatever reason if there is a cry but no true sprite data then options.mod becomes '' regardless of mod
+			//TODO: Possibly fix that? I wouldn't prioritize it though
+			if (window.ModSprites[modSpriteId]?.[options.mod]?.includes('cries')) {
+				spriteData.cryurl = resourcePrefix + options.mod + '/audio/cries/' + speciesid + '.mp3';
+			} else { //We couldn't find a cry
+				spriteData.cryurl = '';
+			}
+		}
+		
+		let hasCustomAnim = false;
+		if (hasCustomSprite && window.ModSprites[modSpriteId][options.mod].includes('ani' + facing)){
+			hasCustomAnim = true;
+			animationData[facing] = {};
+			animationData[facing].w = 192;
+			animationData[facing].h = 192;
 		}
-
 		if (animationData[facing + 'f'] && options.gender === 'F') facing += 'f';
-		let allowAnim = !Dex.prefs('noanim') && !Dex.prefs('nogif');
+		let allowAnim = (!hasCustomSprite || (hasCustomSprite && hasCustomAnim)) && !Dex.prefs('noanim') && !Dex.prefs('nogif');
 		if (allowAnim && spriteData.gen >= 6) spriteData.pixelated = false;
 		if (allowAnim && animationData[facing] && spriteData.gen >= 5) {
 			if (facing.slice(-1) === 'f') name += '-f';
@@ -636,15 +695,23 @@ const Dex = new class implements ModdedDex {
 			spriteData.w = animationData[facing].w;
 			spriteData.h = animationData[facing].h;
 			spriteData.url += dir + '/' + name + '.gif';
+			console.log(animationData[facing]);
 		} else {
 			// There is no entry or enough data in pokedex-mini.js
 			// Handle these in case-by-case basis; either using BW sprites or matching the played gen.
-			dir = (baseDir || 'gen5') + dir;
+			if (!hasCustomSprite) dir = (baseDir || 'gen5') + dir;
 
 			// Gender differences don't exist prior to Gen 4,
 			// so there are no sprites for it
-			if (spriteData.gen >= 4 && miscData['frontf'] && options.gender === 'F') {
-				name += '-f';
+			if (spriteData.gen >= 4 && options.gender === 'F') {
+				//Is it a realmon with a gender difference?
+				if (miscData['frontf']) {
+					name += '-f';
+				}
+				//If it's a custom sprite, does it have separate sprites for male and female?
+				else if (window.ModSprites[modSpriteId] && window.ModSprites[modSpriteId + 'f']) {
+					name += 'f';
+				}
 			}
 
 			spriteData.url += dir + '/' + name + '.png';
@@ -674,7 +741,14 @@ const Dex = new class implements ModdedDex {
 			spriteData.h *= 1.5;
 			spriteData.y += -11;
 		}
-
+		if (window.BattlePokemonSprites) {
+			if (!window.ModSprites[modSpriteId] && !window.BattlePokemonSprites[modSpriteId] && pokemon !== 'substitute') {
+				spriteData = Dex.getSpriteData('substitute', spriteData.isFrontSprite, {
+					gen: options.gen,
+					mod: options.mod,
+				});
+			}
+		}
 		return spriteData;
 	}
 
@@ -705,7 +779,7 @@ const Dex = new class implements ModdedDex {
 		return num;
 	}
 
-	getPokemonIcon(pokemon: string | Pokemon | ServerPokemon | PokemonSet | null, facingLeft?: boolean) {
+	getPokemonIcon(pokemon: string | Pokemon | ServerPokemon | PokemonSet | null, facingLeft?: boolean, mod: string = '') {
 		if (pokemon === 'pokeball') {
 			return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-pokeball-sheet.png) no-repeat scroll -0px 4px`;
 		} else if (pokemon === 'pokeball-statused') {
@@ -732,16 +806,32 @@ const Dex = new class implements ModdedDex {
 		let top = Math.floor(num / 12) * 30;
 		let left = (num % 12) * 40;
 		let fainted = ((pokemon as Pokemon | ServerPokemon)?.fainted ? `;opacity:.3;filter:grayscale(100%) brightness(.5)` : ``);
+		Dex.species.get(id);
+		let species = window.BattlePokedexAltForms && window.BattlePokedexAltForms[id] ? window.BattlePokedexAltForms[id] : Dex.species.get(id);
+		mod = this.getSpriteMod(mod, id, 'icons', species.exists !== false);
+		if (mod) return `background:transparent url(${this.modResourcePrefix}${mod}/sprites/icons/${id}.png) no-repeat scroll -0px -0px${fainted}`;
 		return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-sheet.png?v16) no-repeat scroll -${left}px -${top}px${fainted}`;
+
 	}
 
-	getTeambuilderSpriteData(pokemon: any, gen: number = 0): TeambuilderSpriteData {
+	getTeambuilderSpriteData(pokemon: any, gen: number = 0, mod: string = ''): TeambuilderSpriteData {
 		let id = toID(pokemon.species);
 		let spriteid = pokemon.spriteid;
-		let species = Dex.species.get(pokemon.species);
+		let species = window.BattlePokedexAltForms && window.BattlePokedexAltForms[id] ? window.BattlePokedexAltForms[id] : Dex.species.get(pokemon.species);;
 		if (pokemon.species && !spriteid) {
 			spriteid = species.spriteid || toID(pokemon.species);
 		}
+		if (mod && window.ModConfig[mod].spriteGen) gen = window.ModConfig[mod].spriteGen;
+		mod = this.getSpriteMod(mod, id, 'front', species.exists !== false);
+		if (mod) {
+			return {
+				spriteDir: `${mod}/sprites/front`,
+				spriteid,
+				shiny: (this.getSpriteMod(mod, id, 'front-shiny', species.exists !== false) !== null && pokemon.shiny),
+				x: 10,
+				y: 5,
+			};
+		}
 		if (species.exists === false) return { spriteDir: 'sprites/gen5', spriteid: '0', x: 10, y: 5 };
 		if (window.Config?.server?.afd || Dex.prefs('afd')) {
 			return {
@@ -791,16 +881,20 @@ const Dex = new class implements ModdedDex {
 		return spriteData;
 	}
 
-	getTeambuilderSprite(pokemon: any, gen: number = 0) {
+	getTeambuilderSprite(pokemon: any, gen: number = 0, mod: string = '') {
 		if (!pokemon) return '';
-		const data = this.getTeambuilderSpriteData(pokemon, gen);
+		const data = this.getTeambuilderSpriteData(pokemon, gen, mod);
 		const shiny = (data.shiny ? '-shiny' : '');
-		return 'background-image:url(' + Dex.resourcePrefix + data.spriteDir + shiny + '/' + data.spriteid + '.png);background-position:' + data.x + 'px ' + data.y + 'px;background-repeat:no-repeat';
+		let resourcePrefix = Dex.resourcePrefix;
+		if (data.spriteDir.includes('front')) resourcePrefix = Dex.modResourcePrefix;
+		return 'background-image:url(' + resourcePrefix + data.spriteDir + shiny + '/' + data.spriteid + '.png);background-position:' + data.x + 'px ' + data.y + 'px;background-repeat:no-repeat';
 	}
 
-	getItemIcon(item: any) {
+	getItemIcon(item: any, mod: string = '') {
 		let num = 0;
 		if (typeof item === 'string' && exports.BattleItems) item = exports.BattleItems[toID(item)];
+		mod = this.getSpriteMod(mod, item.id, 'items');
+		if (mod) return `background:transparent url(${this.modResourcePrefix}${mod}/sprites/items/${item.id}.png) no-repeat`;
 		if (item?.spritenum) num = item.spritenum;
 
 		let top = Math.floor(num / 16) * 24;
@@ -808,11 +902,16 @@ const Dex = new class implements ModdedDex {
 		return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/itemicons-sheet.png?v1) no-repeat scroll -' + left + 'px -' + top + 'px';
 	}
 
-	getTypeIcon(type: string | null, b?: boolean) { // b is just for utilichart.js
+	getTypeIcon(type: string | null, b?: boolean, mod: string = '') { // b is just for utilichart.js
 		type = this.types.get(type).name;
 		if (!type) type = '???';
 		let sanitizedType = type.replace(/\?/g, '%3f');
-		return `<img src="${Dex.resourcePrefix}sprites/types/${sanitizedType}.png" alt="${type}" height="14" width="32" class="pixelated${b ? ' b' : ''}" />`;
+		mod = this.getSpriteMod(mod, toID(type), 'types');
+		if (mod && (type !== '???')) {
+			return `<img src="${this.modResourcePrefix}${mod}/sprites/types/${toID(type)}.png" alt="${type}" class="pixelated${b ? ' b' : ''}" />`;
+		} else {
+			return `<img src="${Dex.resourcePrefix}sprites/types/${sanitizedType}.png" alt="${type}" height="14" width="32" class="pixelated${b ? ' b' : ''}" />`;
+		}
 	}
 
 	getCategoryIcon(category: string | null) {
@@ -844,7 +943,7 @@ const Dex = new class implements ModdedDex {
 };
 
 class ModdedDex {
-	readonly gen: number;
+	gen: number;
 	readonly modid: ID;
 	readonly cache = {
 		Moves: {} as any as {[k: string]: Move},
@@ -856,9 +955,9 @@ class ModdedDex {
 	pokeballs: string[] | null = null;
 	constructor(modid: ID) {
 		this.modid = modid;
-		const gen = parseInt(modid.substr(3, 1), 10);
-		if (!modid.startsWith('gen') || !gen) throw new Error("Unsupported modid");
-		this.gen = gen;
+		const gen = parseInt(modid.slice(3), 10);
+		if (!modid.startsWith('gen') || !gen) this.gen = 9;
+		else this.gen = gen;
 	}
 	moves = {
 		get: (name: string): Move => {
@@ -867,26 +966,30 @@ class ModdedDex {
 				name = BattleAliases[id];
 				id = toID(name);
 			}
-			if (this.cache.Moves.hasOwnProperty(id)) return this.cache.Moves[id];
+			// if (this.cache.Moves.hasOwnProperty(id)) return this.cache.Moves[id];
 
 			let data = {...Dex.moves.get(name)};
 
-			for (let i = Dex.gen - 1; i >= this.gen; i--) {
-				const table = window.BattleTeambuilderTable[`gen${i}`];
-				if (id in table.overrideMoveData) {
-					Object.assign(data, table.overrideMoveData[id]);
-				}
-			}
-			if (this.modid !== `gen${this.gen}`) {
-				const table = window.BattleTeambuilderTable[this.modid];
-				if (id in table.overrideMoveData) {
-					Object.assign(data, table.overrideMoveData[id]);
+			const table = window.BattleTeambuilderTable[this.modid];
+			if (table.overrideMoveInfo[id]) {
+				for (const key in table.overrideMoveInfo[id]) {
+					data = {...Dex.moves.get(name), ...table.overrideMoveInfo[id]};
 				}
 			}
 			if (this.gen <= 3 && data.category !== 'Status') {
-				data.category = Dex.getGen3Category(data.type);
+				switch(this.modid) {
+					case 'gen1expansionpack':
+					case 'gen2expansionpack':
+						data.category = Dex.getKEPCategory(data.type);
+						break;
+					case 'gen2crystalseviiislands':
+						data.category = Dex.getCSICategory(data.type);	
+						break;
+					default: 
+						data.category = Dex.getGen3Category(data.type);
+						break;
+				}
 			}
-
 			const move = new Move(id, name, data);
 			this.cache.Moves[id] = move;
 			return move;
@@ -903,18 +1006,19 @@ class ModdedDex {
 			if (this.cache.Items.hasOwnProperty(id)) return this.cache.Items[id];
 
 			let data = {...Dex.items.get(name)};
-
+			const table = window.BattleTeambuilderTable[this.modid];
+			if (table.fullItemName && id in table.fullItemName) {
+				data.name = table.fullItemName[id];
+				data.exists = true;
+			}
 			for (let i = Dex.gen - 1; i >= this.gen; i--) {
-				const table = window.BattleTeambuilderTable[`gen${i}`];
-				if (id in table.overrideItemData) {
-					Object.assign(data, table.overrideItemData[id]);
+				const genTable = window.BattleTeambuilderTable[`gen${i}`];
+				if (id in genTable.overrideItemData) {
+					Object.assign(data, genTable.overrideItemData[id]);
 				}
 			}
-			if (this.modid !== `gen${this.gen}`) {
-				const table = window.BattleTeambuilderTable[this.modid];
-				if (id in table.overrideItemData) {
-					Object.assign(data, table.overrideItemData[id]);
-				}
+			if (this.modid !== `gen${this.gen}`) && id in table.overrideItemData) {
+				Object.assign(data, table.overrideItemData[id]);
 			}
 
 			const item = new Item(id, name, data);
@@ -933,7 +1037,7 @@ class ModdedDex {
 			if (this.cache.Abilities.hasOwnProperty(id)) return this.cache.Abilities[id];
 
 			let data = {...Dex.abilities.get(name)};
-
+			
 			for (let i = Dex.gen - 1; i >= this.gen; i--) {
 				const table = window.BattleTeambuilderTable[`gen${i}`];
 				if (id in table.overrideAbilityData) {
@@ -942,11 +1046,17 @@ class ModdedDex {
 			}
 			if (this.modid !== `gen${this.gen}`) {
 				const table = window.BattleTeambuilderTable[this.modid];
-				if (id in table.overrideAbilityData) {
+				if (table.overrideAbilityData && id in table.overrideAbilityData) {
 					Object.assign(data, table.overrideAbilityData[id]);
 				}
+				if (table.overrideAbilityDesc && id in table.overrideAbilityDesc) {
+					data.shortDesc = table.overrideAbilityDesc[id];
+				}
+				if (table.fullAbilityName && id in table.fullAbilityName) {
+					data.name = table.fullAbilityName[id];
+					data.exists = true;
+				}
 			}
-
 			const ability = new Ability(id, name, data);
 			this.cache.Abilities[id] = ability;
 			return ability;
@@ -954,42 +1064,76 @@ class ModdedDex {
 	};
 
 	species = {
-		get: (name: string): Species => {
+		get: (name: string, hasData = true, debug = ""): Species => {
+			if (name.id) name = name.id; 
 			let id = toID(name);
+			let formid = id;
 			if (window.BattleAliases && id in BattleAliases) {
 				name = BattleAliases[id];
 				id = toID(name);
 			}
-			if (this.cache.Species.hasOwnProperty(id)) return this.cache.Species[id];
-
-			let data = {...Dex.species.get(name)};
-
-			for (let i = Dex.gen - 1; i >= this.gen; i--) {
-				const table = window.BattleTeambuilderTable[`gen${i}`];
-				if (id in table.overrideSpeciesData) {
-					Object.assign(data, table.overrideSpeciesData[id]);
+			
+			if (name.includes('-')) this.species.get(name.split('-')[0]);
+			const table = window.BattleTeambuilderTable[this.modid];
+			if (!table.BattlePokedexAltForms) table.BattlePokedexAltForms = {};
+			if (formid in table.BattlePokedexAltForms) {
+				return table.BattlePokedexAltForms[formid];
+			}
+			if (!table.BattleBaseSpeciesChart) table.BattleBaseSpeciesChart = [];
+			if (window.BattleAliases && id in BattleAliases && !table.overrideDexInfo[id]) {
+				name = BattleAliases[id];
+				id = toID(name);
+			} else if (table.overrideDexInfo && !(id in table.overrideDexInfo) && table.BattleBaseSpeciesChart) {
+				for (const baseSpeciesId of table.BattleBaseSpeciesChart) {
+					if (formid.startsWith(baseSpeciesId)) {
+						id = baseSpeciesId;
+						break;
+					}
 				}
 			}
-			if (this.modid !== `gen${this.gen}`) {
-				const table = window.BattleTeambuilderTable[this.modid];
-				if (id in table.overrideSpeciesData) {
-					Object.assign(data, table.overrideSpeciesData[id]);
+			// if (this.cache.Species.hasOwnProperty(id)) return this.cache.Species[id];
+			var data;
+			if (hasData) {
+				data = {...Dex.species.get(name, true, "from moddedDex: getSpecies 1")};
+				if (table.overrideDexInfo && table.overrideDexInfo[id]) {
+					data = {...Dex.species.get(name, true, "from moddedDex: getSpecies 2"), ...table.overrideDexInfo[id]};
+				}
+			} else {
+				if (table.overrideDexInfo && table.overrideDexInfo[id]) {
+					data = {...table.overrideDexInfo[id]};
 				}
 			}
+			
 			if (this.gen < 3 || this.modid === 'gen7letsgo') {
-				data.abilities = {0: "No Ability"};
+				data.abilities = {0: "None"};
 			}
-
-			const table = window.BattleTeambuilderTable[this.modid];
-			if (id in table.overrideTier) data.tier = table.overrideTier[id];
+			
+			if (table.overrideTier && id in table.overrideTier) data.tier = table.overrideTier[id];
+			if (table.doubles?.overrideTier && id in table.doubles.overrideTier) data.doublesTier = table.doubles.overrideTier[id];
 			if (!data.tier && id.slice(-5) === 'totem') {
 				data.tier = this.species.get(id.slice(0, -5)).tier;
 			}
 			if (!data.tier && data.baseSpecies && toID(data.baseSpecies) !== id) {
 				data.tier = this.species.get(data.baseSpecies).tier;
 			}
+			if (data.cosmeticFormes) {
+				if (!table.BattleBaseSpeciesChart.includes(id)) table.BattleBaseSpeciesChart.push(id);
+				for (const forme of data.cosmeticFormes) {
+					if (toID(forme) === formid) {
+						data = new Species(formid, name, {
+							...data,
+							name: forme,
+							forme: forme.slice(data.name.length + 1),
+							baseForme: "",
+							baseSpecies: data.name,
+							otherFormes: null,
+						});
+						table.BattlePokedexAltForms[formid] = data;
+						break;
+					}
+				}
+			}
 			if (data.gen > this.gen) data.tier = 'Illegal';
-
 			const species = new Species(id, name, data);
 			this.cache.Species[id] = species;
 			return species;
@@ -1001,7 +1145,7 @@ class ModdedDex {
 			const id = toID(name) as ID;
 			name = id.substr(0, 1).toUpperCase() + id.substr(1);
 
-			if (this.cache.Types.hasOwnProperty(id)) return this.cache.Types[id];
+			// if (this.cache.Types.hasOwnProperty(id)) return this.cache.Types[id];
 
 			let data = {...Dex.types.get(name)};
 
diff --git a/play.pokemonshowdown.com/src/battle-log-misc.js b/play.pokemonshowdown.com/src/battle-log-misc.js
index 898c3b99c..f8f6ea98c 100644
--- a/play.pokemonshowdown.com/src/battle-log-misc.js
+++ b/play.pokemonshowdown.com/src/battle-log-misc.js
@@ -27,3 +27,18 @@ d=k(d,e,b,c,g[f+15],14,3634488961),c=k(c,d,e,b,g[f+4],20,3889429448),b=k(b,c,d,e
 e=l(e,b,c,d,g[f+4],11,1272893353),d=l(d,e,b,c,g[f+7],16,4139469664),c=l(c,d,e,b,g[f+10],23,3200236656),b=l(b,c,d,e,g[f+13],4,681279174),e=l(e,b,c,d,g[f+0],11,3936430074),d=l(d,e,b,c,g[f+3],16,3572445317),c=l(c,d,e,b,g[f+6],23,76029189),b=l(b,c,d,e,g[f+9],4,3654602809),e=l(e,b,c,d,g[f+12],11,3873151461),d=l(d,e,b,c,g[f+15],16,530742520),c=l(c,d,e,b,g[f+2],23,3299628645),b=m(b,c,d,e,g[f+0],6,4096336452),e=m(e,b,c,d,g[f+7],10,1126891415),d=m(d,e,b,c,g[f+14],15,2878612391),c=m(c,d,e,b,g[f+5],21,4237533241),
 b=m(b,c,d,e,g[f+12],6,1700485571),e=m(e,b,c,d,g[f+3],10,2399980690),d=m(d,e,b,c,g[f+10],15,4293915773),c=m(c,d,e,b,g[f+1],21,2240044497),b=m(b,c,d,e,g[f+8],6,1873313359),e=m(e,b,c,d,g[f+15],10,4264355552),d=m(d,e,b,c,g[f+6],15,2734768916),c=m(c,d,e,b,g[f+13],21,1309151649),b=m(b,c,d,e,g[f+4],6,4149444226),e=m(e,b,c,d,g[f+11],10,3174756917),d=m(d,e,b,c,g[f+2],15,718787259),c=m(c,d,e,b,g[f+9],21,3951481745),b=i(b,o),c=i(c,p),d=i(d,q),e=i(e,r);return(n(b)+n(c)+n(d)+n(e)).toLowerCase()};
 /* eslint-enable */
+
+// text formatter, transpiled from server chat-formatter.js
+var formatText = (function(){function g(d,a){a=void 0===a?!1:a;d=(""+d).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;");this.f=d=d.replace(h,function(a){if(/^[a-z0-9.]+@/ig.test(a))var c="mailto:"+a;else if(c=a.replace(/^([a-z]*[^a-z:])/g,"http://$1"),"https://docs.google.com/"===a.substr(0,24)||"docs.google.com/"===a.substr(0,16)){"https"===a.slice(0,5)&&(a=a.slice(8));if("?usp=sharing"===a.substr(-12)||"&usp=sharing"===a.substr(-12))a=a.slice(0,-12);
+"#gid=0"===a.substr(-6)&&(a=a.slice(0,-6));var b=a.lastIndexOf("/");18<a.length-b&&(b=a.length);22<b-4&&(a=a.slice(0,19)+'<small class="message-overflow">'+a.slice(19,b-4)+"</small>"+a.slice(b-4))}return'<a href="'+c+'" rel="noopener" target="_blank">'+a+"</a>"});this.b=[];this.stack=[];this.isTrusted=a;this.offset=0}var h=/(?:(?:(?:https?:\/\/|\bwww[.])[a-z0-9-]+(?:[.][a-z0-9-]+)*|\b[a-z0-9-]+(?:[.][a-z0-9-]+)*[.](?:com?|org|net|edu|info|us|jp|[a-z]{2,3}(?=[:/])))(?:[:][0-9]+)?\b(?:\/(?:(?:[^\s()&<>]|&amp;|&quot;|[(](?:[^\s()<>&]|&amp;)*[)])*(?:[^\s`()[\]{}'".,!?;:&<>*`^~\\]|[(](?:[^\s()<>&]|&amp;)*[)]))?)?|[a-z0-9.]+\b@[a-z0-9-]+(?:[.][a-z0-9-]+)*[.][a-z]{2,3})(?![^ ]*&gt;)/ig;
+g.prototype.slice=function(d,a){return this.f.slice(d,a)};g.prototype.a=function(d){return this.f.charAt(d)};g.prototype.i=function(d,a,b){this.c(a);this.stack.push([d,this.b.length]);this.b.push(this.slice(a,b));this.offset=b};g.prototype.c=function(d){d!==this.offset&&(this.b.push(this.slice(this.offset,d)),this.offset=d)};g.prototype.m=function(d){for(var a=-1,b=this.stack.length-1;0<=b;b--){var c=this.stack[b];if("("===c[0]){a=b;break}if("spoiler"!==c[0])break}if(-1!==a){for(this.c(d);this.stack.length>
+a;)this.h(d);this.offset=d}};g.prototype.o=function(d,a,b){for(var c=-1,e=this.stack.length-1;0<=e;e--)if(this.stack[e][0]===d){c=e;break}if(-1===c)return!1;for(this.c(a);this.stack.length>c+1;)this.h(a);a=this.stack.pop()[1];c="";switch(d){case "_":c="i";break;case "*":c="b";break;case "~":c="s";break;case "^":c="sup";break;case "\\":c="sub"}c&&(this.b[a]="<"+c+">",this.b.push("</"+c+">"),this.offset=b);return!0};g.prototype.h=function(d){var a=this.stack.pop();if(a)switch(this.c(d),a[0]){case "spoiler":this.b.push("</span>");
+this.b[a[1]]='<span class="spoiler">';break;case ">":this.b.push("</span>"),this.b[a[1]]='<span class="greentext">'}};g.prototype.j=function(d){for(;this.stack.length;)this.h(d);this.c(d)};g.prototype.l=function(d){d=d.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&apos;/g,"'");return encodeURIComponent(d)};g.prototype.g=function(d,a){switch(d){case "`":for(var b=0,c=a;"`"===this.a(c);)b++,c++;for(var e=0;c<this.f.length;){var f=this.a(c);if("\n"===
+f)break;if("`"===f)e++;else{if(e===b)break;e=0}c++}if(e!==b)break;this.c(a);this.b.push("<code>");e=a+b;b=c-b;e+1>=b||(" "===this.a(e)&&" "===this.a(b-1)?(e++,b--):" "===this.a(e)&&"`"===this.a(e+1)?e++:" "===this.a(b-1)&&"`"===this.a(b-2)&&b--);this.b.push(this.slice(e,b));this.b.push("</code>");this.offset=c;break;case "[":if("[["!==this.slice(a,a+2))break;c=a+2;for(f=e=-1;c<this.f.length;){b=this.a(c);if("]"===b||"\n"===b)break;":"===b&&0>e&&(e=c);"&"===b&&"&lt;"===this.slice(c,c+4)&&(f=c);c++}if("]]"!==
+this.slice(c,c+2))break;var g=c;b="";0<=f&&"&gt;"===this.slice(c-4,c)&&(b=this.slice(f+4,c-4),g=f," "===this.a(g-1)&&g--,b=encodeURI(b.replace(/^([a-z]*[^a-z:])/g,"http://$1")));f=this.slice(a+2,g).replace(/<\/?a(?: [^>]+)?>/g,"");b&&!this.isTrusted&&(g=b.replace(/^https?:\/\//,"").replace(/^www\./,"").replace(/\/$/,""),f+="<small> &lt;"+g+"&gt;</small>",b+='" rel="noopener');if(0<e)switch(e=this.slice(a+2,e).toLowerCase(),e){case "w":case "wiki":f=f.slice(" "===f.charAt(e.length+1)?e.length+2:e.length+
+1);b="//en.wikipedia.org/w/index.php?title=Special:Search&search="+this.l(f);f="wiki: "+f;break;case "pokemon":case "item":f=f.slice(" "===f.charAt(e.length+1)?e.length+2:e.length+1),g=this.isTrusted?"<psicon "+e+'="'+f+'"/>':"["+f+"]",b=e,"item"===e&&(b+="s"),b="//dex.pokemonshowdown.com/"+b+"/"+toID(f),f=g}b||(b="//www.google.com/search?ie=UTF-8&btnI&q="+this.l(f));this.c(a);this.b.push('<a href="'+b+'" target="_blank">'+f+"</a>");this.offset=c+2;break;case "<":if("&lt;&lt;"!==this.slice(a,a+8))break;
+for(c=a+8;/[a-z0-9-]/.test(this.a(c));)c++;if("&gt;&gt;"!==this.slice(c,c+8))break;this.c(a);b=this.slice(a+8,c);this.b.push('&laquo;<a href="/'+b+'" target="_blank">'+b+"</a>&raquo;");this.offset=c+8;break;case "a":for(c=a+1;"/"!==this.a(c)||"a"!==this.a(c+1)||">"!==this.a(c+2);)c++;this.c(c+3)}};g.prototype.get=function(){for(var d=this.offset,a=d;a<this.f.length;a++){var b=this.a(a);switch(b){case "_":case "*":case "~":case "^":case "\\":if(this.a(a+1)===b&&this.a(a+2)!==b&&(" "!==this.a(a-1)&&
+this.o(b,a,a+2)||" "!==this.a(a+2)&&this.i(b,a,a+2),a<this.offset)){a=this.offset-1;break}for(;this.a(a+1)===b;)a++;break;case "(":this.stack.push(["(",-1]);break;case ")":this.m(a);a<this.offset&&(a=this.offset-1);break;case "`":"`"===this.a(a+1)&&this.g("`",a);if(a<this.offset){a=this.offset-1;break}for(;"`"===this.a(a+1);)a++;break;case "[":this.g("[",a);if(a<this.offset){a=this.offset-1;break}for(;"["===this.a(a+1);)a++;break;case ":":if(7>a)break;if("spoiler:"===this.slice(a-7,a+1).toLowerCase()||
+"spoilers:"===this.slice(a-8,a+1).toLowerCase())" "===this.a(a+1)&&a++,this.i("spoiler",a+1,a+1);break;case "&":a===d&&"&gt;"===this.slice(a,a+4)?"._/=:;".includes(this.a(a+4))||["w&lt;","w&gt;"].includes(this.slice(a+4,a+9))||this.i(">",a,a):this.g("<",a);if(a<this.offset){a=this.offset-1;break}for(;"lt;&"===this.slice(a+1,a+5);)a+=4;break;case "<":this.g("a",a);a<this.offset&&(a=this.offset-1);break;case "\r":case "\n":this.j(a),d=a+1}}this.j(this.f.length);return this.b.join("")};return function(d,
+a){return(new g(d,void 0===a?!1:a)).get()}})();
+/* eslint-enable */
\ No newline at end of file
diff --git a/play.pokemonshowdown.com/src/battle-log.ts b/play.pokemonshowdown.com/src/battle-log.ts
index c078c16a4..cb38f0cfb 100644
--- a/play.pokemonshowdown.com/src/battle-log.ts
+++ b/play.pokemonshowdown.com/src/battle-log.ts
@@ -763,8 +763,7 @@ export class BattleLog {
 				return false;
 			},
 			getURI(uri: string) {
-				return `http://${Config.routes.root}/interstice?uri=${encodeURIComponent(uri)}`;
-			},
+				return `http://${Config.routes.psmain}/interstice?uri=${encodeURIComponent(uri)}`;			},
 		};
 	})();
 
@@ -1194,7 +1193,7 @@ export class BattleLog {
 		buf += '<div class="battle-log battle-log-inline"><div class="inner">' + battle.scene.log.elem.innerHTML + '</div></div>\n';
 		buf += '</div>\n';
 		buf += '<script>\n';
-		buf += `let daily = Math.floor(Date.now()/1000/60/60/24);document.write('<script src="https://${Config.routes.client}/js/replay-embed.js?version'+daily+'"></'+'script>');\n`;
+		buf += `let daily = Math.floor(Date.now()/1000/60/60/24);document.write('<script src="http://191.101.232.116//js/replay-embed.js?version'+daily+'"></'+'script>');\n`;
 		buf += '</script>\n';
 		return buf;
 	}
diff --git a/play.pokemonshowdown.com/src/battle-scene-stub.ts b/play.pokemonshowdown.com/src/battle-scene-stub.ts
index 7fc99892f..db64678e9 100644
--- a/play.pokemonshowdown.com/src/battle-scene-stub.ts
+++ b/play.pokemonshowdown.com/src/battle-scene-stub.ts
@@ -75,6 +75,7 @@ export class BattleSceneStub {
 	anim(pokemon: Pokemon, end: ScenePos, transition?: string) { }
 	beforeMove(pokemon: Pokemon) { }
 	afterMove(pokemon: Pokemon) { }
+	updateSpritesForSide(side: Side) { }
 }
 
 if (typeof require === 'function') {
diff --git a/play.pokemonshowdown.com/src/battle-sound.ts b/play.pokemonshowdown.com/src/battle-sound.ts
index 5307e73f9..52c079471 100644
--- a/play.pokemonshowdown.com/src/battle-sound.ts
+++ b/play.pokemonshowdown.com/src/battle-sound.ts
@@ -115,7 +115,7 @@ export const BattleSound = new class {
 		if (this.soundCache[url]) return this.soundCache[url];
 		try {
 			const sound = document.createElement('audio');
-			sound.src = 'https://' + Config.routes.client + '/' + url;
+			sound.src = url;
 			sound.volume = this.effectVolume / 100;
 			this.soundCache[url] = sound;
 			return sound;
@@ -142,7 +142,7 @@ export const BattleSound = new class {
 			this.deleteBgm(replaceBGM);
 		}
 
-		const bgm = new BattleBGM(url, loopstart, loopend);
+		const bgm = new BattleBGM('https://' + Config.routes.psmain + '/' + url, loopstart, loopend);
 		this.bgm.push(bgm);
 		return bgm;
 	}
diff --git a/play.pokemonshowdown.com/src/battle-tooltips.ts b/play.pokemonshowdown.com/src/battle-tooltips.ts
index ebeb17136..1f3df4ff5 100644
--- a/play.pokemonshowdown.com/src/battle-tooltips.ts
+++ b/play.pokemonshowdown.com/src/battle-tooltips.ts
@@ -203,12 +203,10 @@ class BattleTooltips {
 		$elem.on('touchstart', '.has-tooltip', e => {
 			e.preventDefault();
 			this.holdLockTooltipEvent(e);
-			if (!BattleTooltips.parentElem) {
-				// should never happen, but in case there's a bug in the tooltip handler
-				BattleTooltips.parentElem = e.currentTarget;
+			if (e.currentTarget === BattleTooltips.parentElem && BattleTooltips.parentElem!.tagName === 'BUTTON') {
+				$(BattleTooltips.parentElem!).addClass('pressed');
+				BattleTooltips.isPressed = true;
 			}
-			$(BattleTooltips.parentElem!).addClass('pressed');
-			BattleTooltips.isPressed = true;
 		});
 		$elem.on('touchend', '.has-tooltip', e => {
 			e.preventDefault();
@@ -1068,6 +1066,19 @@ class BattleTooltips {
 				stats.atk *= 2;
 			}
 		}
+		//Vaporemons
+		if (this.battle.tier.includes("VaporeMons")) {
+			if (item === 'mantisclaw') {
+				if (speciesName === 'Scyther') {
+					speedModifiers.push(1.5);
+				} else if (speciesName === 'Scizor') {
+					stats.def = Math.floor(stats.def * 1.3);
+					stats.spd = Math.floor(stats.spd * 1.3);
+				} else if (speciesName === 'Kleavor') {
+					stats.atk = Math.floor(stats.atk * 1.5);
+				}
+			}
+		}
 
 		if (speciesName === 'Ditto' && !(clientPokemon && 'transform' in clientPokemon.volatiles)) {
 			if (item === 'quickpowder') {
@@ -1165,6 +1176,17 @@ class BattleTooltips {
 					} else {
 						stats[statName] = Math.floor(stats[statName] * 1.3);
 					}
+				} 
+				if (this.battle.tier.includes("VaporeMons")) {//Vaporemons
+					if (clientPokemon.volatiles['protomosis' + statName] || clientPokemon.volatiles['photondrive' + statName] ||
+						clientPokemon.volatiles['protocrysalis' + statName] || clientPokemon.volatiles['neurondrive' + statName] ||
+						clientPokemon.volatiles['protostasis' + statName] || clientPokemon.volatiles['runedrive' + statName]) {
+						if (statName === 'spe') {
+							speedModifiers.push(1.5);
+						} else {
+							stats[statName] = Math.floor(stats[statName] * 1.3);
+						}
+					}
 				}
 			}
 		}
@@ -1176,6 +1198,25 @@ class BattleTooltips {
 				speedModifiers.push(1.5);
 			}
 		}
+		if (this.battle.tier.includes("VaporeMons")) {//Vaporemons
+			if (item === 'tuffytuff' && (['igglybuff','jigglypuff','wigglytuff'].includes(
+			this.battle.dex.species.get(serverPokemon.speciesForme).id))) {
+				stats.def *= 2;
+				stats.spd *= 2;
+			}
+			else if (item === 'mithrilarmor') {
+				stats.def = Math.floor(stats.def * 1.2);
+			} 
+			else if (item === 'snowglobe' && this.pokemonHasType(pokemon, 'Ice')) {
+				stats.def = Math.floor(stats.def * 1.5);
+			} 
+			else if (item === 'sandclock' && this.pokemonHasType(pokemon, 'Rock')) {
+				stats.spd = Math.floor(stats.spd * 1.5);
+			} 
+			else if (item === 'desertrose' && species === 'Florges' && this.battle.weather === 'sandstorm') {
+				stats.spd = Math.floor(stats.spd * 1.5);
+			}
+		}
 		const isNFE = this.battle.dex.species.get(serverPokemon.speciesForme).evos?.some(evo => {
 			const evoSpecies = this.battle.dex.species.get(evo);
 			return !evoSpecies.isNonstandard ||
@@ -1188,7 +1229,11 @@ class BattleTooltips {
 			stats.spd = Math.floor(stats.spd * 1.5);
 		}
 		if (ability === 'grasspelt' && this.battle.hasPseudoWeather('Grassy Terrain')) {
-			stats.def = Math.floor(stats.def * 1.5);
+			if (this.battle.tier.includes("VaporeMons")) {//Vaporemons
+				stats.def = Math.floor(stats.def * 1.3333);
+			} else {
+				stats.def = Math.floor(stats.def * 1.5);
+			}
 		}
 		if (this.battle.hasPseudoWeather('Electric Terrain')) {
 			if (ability === 'surgesurfer') {
@@ -1667,15 +1712,15 @@ class BattleTooltips {
 			}
 			if (move.id === 'weatherball' && value.weatherModify(0)) {
 				if (this.battle.weather === 'stormsurge') moveType = 'Water';
-				if (this.battle.weather === 'deserteddunes') moveType = 'Rock';
+				else if (this.battle.weather === 'deserteddunes') moveType = 'Rock';
 			}
 			if (move.id === 'o' || move.id === 'worriednoises') {
 				moveType = pokemonTypes[0];
 			}
-			if (move.id === 'dillydally') {
+			else if (move.id === 'dillydally') {
 				moveType = pokemonTypes[pokemonTypes.length - 1];
 			}
-			if (move.id === 'magicalfocus') {
+			else if (move.id === 'magicalfocus') {
 				if (this.battle.turn % 3 === 1) {
 					moveType = 'Fire';
 				} else if (this.battle.turn % 3 === 2) {
@@ -1684,10 +1729,10 @@ class BattleTooltips {
 					moveType = 'Ice';
 				}
 			}
-			if (move.id === 'hydrostatics' && pokemon.terastallized) {
+			else if (move.id === 'hydrostatics' && pokemon.terastallized) {
 				moveType = 'Water';
 			}
-			if (move.id === 'asongoficeandfire' && pokemon.getSpeciesForme() === 'Volcarona') moveType = 'Ice';
+			else if (move.id === 'asongoficeandfire' && pokemon.getSpeciesForme() === 'Volcarona') moveType = 'Ice';
 			if (this.battle.abilityActive('dynamictyping')) {
 				moveType = '???';
 			}
@@ -1698,6 +1743,10 @@ class BattleTooltips {
 				}
 			}
 		}
+		/*else if (this.battle.tier.includes("VaporeMons") && move.id === 'terablast' && itemName === 'Tera Shard') {
+			const stats = this.calculateModifiedStats(pokemon, serverPokemon, true);
+			if (stats.atk > stats.spa) category = 'Physical';
+		}*/
 		return [moveType, category];
 	}
 
@@ -1873,6 +1922,9 @@ class BattleTooltips {
 		if (move.id === 'terablast' && pokemon.terastallized === 'Stellar') {
 			value.set(100, 'Tera Stellar boost');
 		}
+		/*if (['terablast'].includes(move.id) && 	this.battle.tier.includes("VaporeMons") && itemName === 'Tera Shard') {
+			value.set(100, 'Tera Shard boost');
+		}*/
 		if (move.id === 'brine' && target && target.hp * 2 <= target.maxhp) {
 			value.modify(2, 'Brine + target below half HP');
 		}
@@ -2046,7 +2098,15 @@ class BattleTooltips {
 			}
 		}
 		// Base power based on times hit
-		if (move.id === 'ragefist') {
+		if (this.battle.tier.includes("VaporeMons")) {//Vaporemons
+			if (move.id === 'ragefist' || move.id === 'ragingfury') {
+				value.set(Math.min(200, 50 + 50 * pokemon.timesAttacked),
+					pokemon.timesAttacked > 0
+						? `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}`
+						: undefined);
+			}
+		}
+		if (move.id === 'ragefist' && !this.battle.tier.includes("VaporeMons")) {
 			value.set(Math.min(350, 50 + 50 * pokemon.timesAttacked),
 				pokemon.timesAttacked > 0
 					? `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}`
@@ -2073,7 +2133,10 @@ class BattleTooltips {
 		if (['psn', 'tox'].includes(pokemon.status) && move.category === 'Physical') {
 			value.abilityModify(1.5, "Toxic Boost");
 		}
-		if (['Rock', 'Ground', 'Steel'].includes(moveType) && this.battle.weather === 'sandstorm') {
+		if (['Rock', 'Ground', 'Steel'].includes(moveType) && this.battle.weather === 'sandstorm' && !this.battle.tier.includes("VaporeMons")) {
+			if (value.tryAbility("Sand Force")) value.weatherModify(1.3, "Sandstorm", "Sand Force");
+		}
+		if (this.battle.weather === 'sandstorm' && this.battle.tier.includes("VaporeMons")) {
 			if (value.tryAbility("Sand Force")) value.weatherModify(1.3, "Sandstorm", "Sand Force");
 		}
 		if (move.secondaries) {
@@ -2139,7 +2202,11 @@ class BattleTooltips {
 				} else if (allyAbility === 'Power Spot' && ally !== pokemon) {
 					value.modify(1.3, 'Power Spot');
 				} else if (allyAbility === 'Steely Spirit' && moveType === 'Steel') {
-					value.modify(1.5, 'Steely Spirit');
+					if (this.battle.tier.includes("VaporeMons")) {
+						value.modify(2, 'Steely Spirit');
+					} else {
+						value.modify(1.5, 'Steely Spirit');
+					}
 				}
 			}
 			for (const foe of pokemon.side.foe.active) {
@@ -2405,10 +2472,36 @@ class BattleTooltips {
 
 		if (itemName === 'Muscle Band' && move.category === 'Physical' ||
 			itemName === 'Wise Glasses' && move.category === 'Special' ||
-			itemName === 'Punching Glove' && move.flags['punch']) {
+			itemName === 'Punching Glove' && move.flags['punch'] && !this.battle.tier.includes("VaporeMons")) {
 			value.itemModify(1.1);
 		}
-
+		//Vaporemons
+		if (this.battle.tier.includes("VaporeMons")) {
+			if (itemName === 'Protective Pads' && (move.recoil || move.hasCrashDamage) ||
+				itemName === 'Quick Claw' && move.priority > 0.1 ||
+				itemName === 'Razor Fang' && move.flags['bite'] ||
+				itemName === 'Razor Claw' && move.flags['slicing'] ||
+				itemName === 'Big Root' && move.flags['heal'] ||
+				itemName === 'Punching Glove' && move.flags['punch']) {
+				value.itemModify(1.3);
+			} else if (itemName === 'Baseball Bat' && move.flags['contact']) {
+				value.itemModify(1.25);
+			} else if (itemName === 'Tie-Dye Band') {
+				if (this.pokemonHasType(pokemon, moveType)) {
+					value.itemModify(0.67);
+				} else {
+					value.itemModify(1.3);
+				}
+			} else if (itemName === 'Hero\' Bubble' && moveType === 'Water' && speciesName === 'Palafin') {
+				value.itemModify(2);
+			}/* else if (
+				(speciesName === 'Charizard' && itemName === 'Wellspring Mask') ||
+				(speciesName.startsWith('Ogerpon-Hearthflame') && itemName === 'Hearthflame Mask') ||
+				(speciesName.startsWith('Ogerpon-Cornerstone') && itemName === 'Cornerstone Mask')) {
+				value.itemModify(1.2);
+				return value;
+			}*/
+		}
 		return value;
 	}
 	getPokemonTypes(pokemon: Pokemon | ServerPokemon, preterastallized = false): ReadonlyArray<TypeName> {
@@ -2553,9 +2646,9 @@ class BattleStatGuesser {
 	supportsEVs: boolean;
 	supportsAVs: boolean;
 
-	constructor(formatid: ID) {
+	constructor(formatid: ID, modid: ID) {
 		this.formatid = formatid;
-		this.dex = formatid ? Dex.mod(formatid.slice(0, 4) as ID) : Dex;
+		this.dex = modid ? Dex.mod(modid) : formatid ? Dex.mod(formatid.slice(0, 4) as ID) : Dex;
 		this.ignoreEVLimits = (
 			this.dex.gen < 3 ||
 			((this.formatid.endsWith('hackmons') || this.formatid.endsWith('bh')) && this.dex.gen !== 6) ||
@@ -3295,4 +3388,4 @@ if (typeof require === 'function') {
 	// in Node
 	(global as any).BattleStatGuesser = BattleStatGuesser;
 	(global as any).BattleStatOptimizer = BattleStatOptimizer;
-}
+}
\ No newline at end of file
diff --git a/play.pokemonshowdown.com/src/battle.ts b/play.pokemonshowdown.com/src/battle.ts
index 4a9b73e9a..b0e8d27a2 100644
--- a/play.pokemonshowdown.com/src/battle.ts
+++ b/play.pokemonshowdown.com/src/battle.ts
@@ -658,9 +658,14 @@ export class Side {
 	}
 	reset() {
 		this.clearPokemon();
+		this.updateSprites();
 		this.sideConditions = {};
 		this.faintCounter = 0;
 	}
+	updateSprites() {
+		this.z = (this.isFar ? 200 : 0);
+		this.battle.scene.updateSpritesForSide(this);
+	}
 	setAvatar(avatar: string) {
 		this.avatar = avatar;
 	}
@@ -1087,6 +1092,7 @@ export class Battle {
 	myAllyPokemon: ServerPokemon[] | null = null;
 	lastMove = '';
 
+	mod = '' as ID;
 	gen = 8;
 	dex: ModdedDex = Dex;
 	teamPreviewCount = 0;
@@ -1147,6 +1153,19 @@ export class Battle {
 			throw new Error(`You must specify $frame and $logFrame simultaneously`);
 		}
 
+		const format = this.id.slice(this.id.indexOf('-') + 1, this.id.lastIndexOf('-'));
+		for (const mod in window.ModConfig) {
+			for (const formatid in window.ModConfig[mod].formats) {
+				if (format === formatid) {
+					this.mod = mod as ID;
+					this.dex = Dex.mod(mod as ID);
+					break;
+				}
+			}
+			if (this.mod) break;
+		}
+		if (this.id.includes('digimon')) this.mod = 'digimon' as ID;
+
 		this.paused = !!options.paused;
 		this.started = !this.paused;
 		this.debug = !!options.debug;
@@ -1510,7 +1529,7 @@ export class Battle {
 
 				if (
 					!target && this.gameType === 'singles' &&
-					!['self', 'allies', 'allySide', 'adjacentAlly', 'adjacentAllyOrSelf', 'allyTeam'].includes(moveTarget)
+					!['self', 'allies', 'allySide', 'adjacentAlly', 'adjacentAllyOrSelf', 'anyAlly', 'allyTeam'].includes(moveTarget)
 				) {
 					// Hardcode for moves without a target in singles
 					foeTargets.push(pokemon.side.foe.active[0]);
@@ -3536,9 +3555,9 @@ export class Battle {
 			side.setName(args[2]);
 			if (args[3]) side.setAvatar(args[3]);
 			if (args[4]) side.rating = args[4];
+			this.scene.updateSidebar(side);
 			if (this.joinButtons) this.scene.hideJoinButtons();
 			this.log(args);
-			this.scene.updateSidebar(side);
 			break;
 		}
 		case 'badge': {
@@ -3683,7 +3702,7 @@ export class Battle {
 		}
 		case 'gen': {
 			this.gen = parseInt(args[1], 10);
-			this.dex = Dex.forGen(this.gen);
+			this.dex = this.mod ? Dex.mod(this.mod) : Dex.forGen(this.gen);
 			this.scene.updateGen();
 			this.log(args);
 			break;
diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts
index 8e33e27e2..44a5338c2 100644
--- a/play.pokemonshowdown.com/src/client-main.ts
+++ b/play.pokemonshowdown.com/src/client-main.ts
@@ -274,9 +274,9 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
  *********************************************************************/
 
 class PSUser extends PSModel {
-	name = "";
+	name = "Guest";
 	group = '';
-	userid = "" as ID;
+	userid = "guest" as ID;
 	named = false;
 	registered = false;
 	avatar = "1";
@@ -296,22 +296,22 @@ class PSUser extends PSModel {
 			}
 		}
 	}
-	logOut() {
-		PSLoginServer.query({
-			act: 'logout',
-			userid: this.userid,
-		});
-		PS.send('|/logout');
-		PS.connection?.disconnect();
-
-		alert("You have been logged out and disconnected.\n\nIf you wanted to change your name while staying connected, use the 'Change Name' button or the '/nick' command.");
-		this.name = "";
-		this.group = '';
-		this.userid = "" as ID;
-		this.named = false;
-		this.registered = false;
-		this.update();
-	}
+	// logOut() {
+	// 	PSLoginServer.query({
+	// 		act: 'logout',
+	// 		userid: this.userid,
+	// 	});
+	// 	PS.send('|/logout');
+	// 	PS.connection?.disconnect();
+
+	// 	alert("You have been logged out and disconnected.\n\nIf you wanted to change your name while staying connected, use the 'Change Name' button or the '/nick' command.");
+	// 	this.name = "";
+	// 	this.group = '';
+	// 	this.userid = "" as ID;
+	// 	this.named = false;
+	// 	this.registered = false;
+	// 	this.update();
+	// }
 }
 
 /**********************************************************************
@@ -535,15 +535,6 @@ class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
 		}}
 	}
 	handleMessage(line: string) {
-		if (!line.startsWith('/') || line.startsWith('//')) return false;
-		const spaceIndex = line.indexOf(' ');
-		const cmd = spaceIndex >= 0 ? line.slice(1, spaceIndex) : line.slice(1);
-		// const target = spaceIndex >= 0 ? line.slice(spaceIndex + 1) : '';
-		switch (cmd) {
-		case 'logout': {
-			PS.user.logOut();
-			return true;
-		}}
 		return false;
 	}
 	send(msg: string, direct?: boolean) {
@@ -671,17 +662,6 @@ const PS = new class extends PSModel {
 	leftRoomWidth = 0;
 	mainmenu: MainMenuRoom = null!;
 
-	/**
-	 * The drag-and-drop API is incredibly dumb and doesn't let us know
-	 * what's being dragged until the `drop` event, so we track it here.
-	 *
-	 * Note that `PS.dragging` will be null if the drag was initiated
-	 * outside PS (e.g. dragging a team from File Explorer to PS), and
-	 * for security reasons it's impossible to know what they are until
-	 * they're dropped.
-	 */
-	dragging: {type: 'room', roomid: RoomID} | null = null;
-
 	/** Tracks whether or not to display the "Use arrow keys" hint */
 	arrowKeysUsed = false;
 
@@ -843,11 +823,7 @@ const PS = new class extends PSModel {
 		const roomid = fullMsg.slice(0, pipeIndex) as RoomID;
 		const msg = fullMsg.slice(pipeIndex + 1);
 		console.log('\u25b6\ufe0f ' + (roomid ? '[' + roomid + '] ' : '') + '%c' + msg, "color: #776677");
-		if (!this.connection) {
-			alert(`You are not connected and cannot send ${msg}.`);
-			return;
-		}
-		this.connection.send(fullMsg);
+		this.connection!.send(fullMsg);
 	}
 	isVisible(room: PSRoom) {
 		if (this.leftRoomWidth === 0) {
@@ -905,7 +881,7 @@ const PS = new class extends PSModel {
 			case 'news':
 				options.type = options.id;
 				break;
-			case 'battle-': case 'user-': case 'team-': case 'ladder-':
+				case 'battle-': case 'user-': case 'team-':
 				options.type = options.id.slice(0, hyphenIndex);
 				break;
 			case 'view-':
diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx
index 482885ce6..f9164a097 100644
--- a/play.pokemonshowdown.com/src/panel-chat.tsx
+++ b/play.pokemonshowdown.com/src/panel-chat.tsx
@@ -85,7 +85,7 @@ class ChatRoom extends PSRoom {
 			this.update(null);
 			return false;
 		}}
-		return super.handleMessage(line);
+		return false;
 	}
 	openChallenge() {
 		if (!this.pmTarget) {
diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx
index 66f7ff982..30fbe5fa5 100644
--- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx
+++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx
@@ -5,9 +5,7 @@
  * @license AGPLv3
  */
 
-type RoomInfo = {
-	title: string, desc?: string, userCount?: number, section?: string, spotlight?: string, subRooms?: string[],
-};
+type RoomInfo = {title: string, desc?: string, userCount?: number, subRooms?: string[]};
 
 class MainMenuRoom extends PSRoom {
 	readonly classType: string = 'mainmenu';
@@ -16,7 +14,7 @@ class MainMenuRoom extends PSRoom {
 		avatar?: string | number,
 		status?: string,
 		group?: string,
-		customgroup?: string,
+		// customgroup?: string,
 		rooms?: {[roomid: string]: {isPrivate?: true, p1?: string, p2?: string}},
 	}} = {};
 	roomsCache: {
@@ -24,6 +22,8 @@ class MainMenuRoom extends PSRoom {
 		userCount?: number,
 		chat?: RoomInfo[],
 		sectionTitles?: string[],
+		official?: RoomInfo[],
+		pspl?: RoomInfo[],
 	} = {};
 	receiveLine(args: Args) {
 		const [cmd] = args;
@@ -247,16 +247,6 @@ class MainMenuRoom extends PSRoom {
 			if (userRoom) userRoom.update(null);
 			break;
 		case 'rooms':
-			if (response.pspl) {
-				for (const roomInfo of response.pspl) roomInfo.spotlight = "Spotlight";
-				response.chat = [...response.pspl, ...response.chat];
-				response.pspl = null;
-			}
-			if (response.official) {
-				for (const roomInfo of response.official) roomInfo.section = "Official";
-				response.chat = [...response.official, ...response.chat];
-				response.official = null;
-			}
 			this.roomsCache = response;
 			const roomsRoom = PS.rooms[`rooms`] as RoomsRoom;
 			if (roomsRoom) roomsRoom.update(null);
@@ -274,12 +264,6 @@ class MainMenuRoom extends PSRoom {
 				battlesRoom.update(null);
 			}
 			break;
-		case 'laddertop':
-			const ladderRoomEntries = Object.entries(PS.rooms).filter(entry => entry[0].startsWith('ladder'));
-			for (const [, ladderRoom] of ladderRoomEntries) {
-				(ladderRoom as LadderRoom).update(response);
-			}
-			break;
 		}
 	}
 }
@@ -299,10 +283,10 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
 	submit = (e: Event) => {
 		alert('todo: implement');
 	};
-	handleDragStart = (e: DragEvent) => {
-		const roomid = (e.currentTarget as HTMLElement).getAttribute('data-roomid') as RoomID;
-		PS.dragging = {type: 'room', roomid};
-	};
+	// handleDragStart = (e: DragEvent) => {
+	// 	const roomid = (e.currentTarget as HTMLElement).getAttribute('data-roomid') as RoomID;
+	// 	PS.dragging = {type: 'room', roomid};
+	// };
 	renderMiniRoom(room: PSRoom) {
 		const roomType = PS.roomTypes[room.type];
 		const Panel = roomType ? roomType.Component : PSRoomPanel;
@@ -313,7 +297,7 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
 			const room = PS.rooms[roomid]!;
 			return <div class="pmbox">
 				<div class="mini-window">
-					<h3 draggable onDragStart={this.handleDragStart} data-roomid={roomid}>
+					<h3>
 						<button class="closebutton" name="closeRoom" value={roomid} aria-label="Close" tabIndex={-1}><i class="fa fa-times-circle"></i></button>
 						<button class="minimizebutton" tabIndex={-1}><i class="fa fa-minus-circle"></i></button>
 						{room.title}
@@ -323,41 +307,60 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
 			</div>;
 		});
 	}
-	renderSearchButton() {
-		if (PS.down) {
-			return <div class="menugroup" style="background: rgba(10,10,10,.6)">
-				{PS.down === 'ddos' ?
-					<p class="error"><strong>Pok&eacute;mon Showdown is offline due to a DDoS attack!</strong></p>
-				:
-					<p class="error"><strong>Pok&eacute;mon Showdown is offline due to technical difficulties!</strong></p>
-				}
-				<p>
-					<div style={{textAlign: 'center'}}>
-						<img width="96" height="96" src={`//${Config.routes.client}/sprites/gen5/teddiursa.png`} alt="" />
-					</div>
-					Bear with us as we freak out.
-				</p>
-				<p>(We'll be back up in a few hours.)</p>
-			</div>;
-		}
+	// renderSearchButton() {
+	// 	if (PS.down) {
+	// 		return <div class="menugroup" style="background: rgba(10,10,10,.6)">
+	// 			{PS.down === 'ddos' ?
+	// 				<p class="error"><strong>Pok&eacute;mon Showdown is offline due to a DDoS attack!</strong></p>
+	// 			:
+	// 				<p class="error"><strong>Pok&eacute;mon Showdown is offline due to technical difficulties!</strong></p>
+	// 			}
+	// 			<p>
+	// 				<div style={{textAlign: 'center'}}>
+	// 					<img width="96" height="96" src={`//${Config.routes.client}/sprites/gen5/teddiursa.png`} alt="" />
+	// 				</div>
+	// 				Bear with us as we freak out.
+	// 			</p>
+	// 			<p>(We'll be back up in a few hours.)</p>
+	// 		</div>;
+	// 	}
 
-		if (!PS.user.userid || PS.isOffline) {
-			return <TeamForm class="menugroup" onSubmit={this.submit}>
-				<button class="mainmenu1 big button disabled" name="search">
-					<em>{PS.isOffline ? "Disconnected" : "Connecting..."}</em>
-				</button>
-			</TeamForm>;
-		}
+	// 	if (!PS.user.userid || PS.isOffline) {
+	// 		return <TeamForm class="menugroup" onSubmit={this.submit}>
+	// 			<button class="mainmenu1 big button disabled" name="search">
+	// 				<em>{PS.isOffline ? "Disconnected" : "Connecting..."}</em>
+	// 			</button>
+	// 		</TeamForm>;
+	// 	}
 
-		return <TeamForm class="menugroup" onSubmit={this.submit}>
-			<button class="mainmenu1 big button" name="search">
+	// 	return <TeamForm class="menugroup" onSubmit={this.submit}>
+	// 		<button class="mainmenu1 big button" name="search">
+	// 			<strong>Battle!</strong><br />
+	// 			<small>Find a random opponent</small>
+	// 		</button>
+	// 	</TeamForm>;
+	// }
+	render() {
+		const onlineButton = ' button' + (PS.isOffline ? ' disabled' : '');
+		const searchButton = (PS.down ? <div class="menugroup" style="background: rgba(10,10,10,.6)">
+			{PS.down === 'ddos' ?
+				<p class="error"><strong>Pok&eacute;mon Showdown is offline due to a DDoS attack!</strong></p>
+			:
+				<p class="error"><strong>Pok&eacute;mon Showdown is offline due to technical difficulties!</strong></p>
+			}
+			<p>
+				<div style={{textAlign: 'center'}}>
+					<img width="96" height="96" src={`//${Config.routes.client}/sprites/gen5/teddiursa.png`} alt="" />
+				</div>
+				Bear with us as we freak out.
+			</p>
+			<p>(We'll be back up in a few hours.)</p>
+		</div> : <TeamForm class="menugroup" onSubmit={this.submit}>
+			<button class={"mainmenu1 big" + onlineButton} name="search">
 				<strong>Battle!</strong><br />
 				<small>Find a random opponent</small>
 			</button>
-		</TeamForm>;
-	}
-	render() {
-		const onlineButton = ' button' + (PS.isOffline ? ' disabled' : '');
+		</TeamForm>);
 		return <PSPanelWrapper room={this.props.room} scrollable>
 			<div class="mainmenuwrapper">
 				<div class="leftmenu">
@@ -365,7 +368,7 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
 						{this.renderMiniRooms()}
 					</div>
 					<div class="mainmenu">
-						{this.renderSearchButton()}
+						{searchButton}
 
 						<div class="menugroup">
 							<p><button class="mainmenu2 button" name="joinRoom" value="teambuilder">Teambuilder</button></p>
diff --git a/play.pokemonshowdown.com/src/panel-rooms.tsx b/play.pokemonshowdown.com/src/panel-rooms.tsx
index 74ec5d177..677af0e51 100644
--- a/play.pokemonshowdown.com/src/panel-rooms.tsx
+++ b/play.pokemonshowdown.com/src/panel-rooms.tsx
@@ -126,6 +126,8 @@ class RoomsPanel extends PSRoomPanel {
 		} else {
 			roomList = [
 				this.renderRoomList("Chat rooms", rooms.chat),
+				this.renderRoomList("Official chat rooms", rooms.official),
+				this.renderRoomList("PSPL winner", rooms.pspl),
 			];
 		}
 
diff --git a/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx b/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx
index 47dce8ab8..64fa73652 100644
--- a/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx
+++ b/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx
@@ -186,6 +186,10 @@ class TeamTextbox extends preact.Component<{team: Team}> {
 }
 
 class TeamPanel extends PSRoomPanel<TeamRoom> {
+	backToList = () => {
+		PS.removeRoom(this.props.room);
+		PS.join('teambuilder' as RoomID);
+	};
 	rename = (e: Event) => {
 		const textbox = e.currentTarget as HTMLInputElement;
 		const room = this.props.room;
@@ -198,8 +202,7 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
 		const team = PS.teams.byKey[room.id.slice(5)];
 		if (!team) {
 			return <PSPanelWrapper room={room}>
-				<button class="button" data-href="teambuilder" data-target="replace">
-					<i class="fa fa-chevron-left"></i> List
+				<button class="button" onClick={this.backToList}>					<i class="fa fa-chevron-left"></i> List
 				</button>
 				<p class="error">
 					Team doesn't exist
@@ -210,8 +213,7 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
 		if (!room.team) room.team = team;
 		return <PSPanelWrapper room={room} scrollable>
 			<div class="pad">
-				<button class="button" data-href="teambuilder" data-target="replace">
-					<i class="fa fa-chevron-left"></i> List
+			<button class="button" onClick={this.backToList}>					<i class="fa fa-chevron-left"></i> List
 				</button>
 				<label class="label teamname">
 					Team name:
diff --git a/play.pokemonshowdown.com/src/panel-teamdropdown.tsx b/play.pokemonshowdown.com/src/panel-teamdropdown.tsx
index 0794f16ca..29c59afc1 100644
--- a/play.pokemonshowdown.com/src/panel-teamdropdown.tsx
+++ b/play.pokemonshowdown.com/src/panel-teamdropdown.tsx
@@ -398,7 +398,7 @@ class PSTeambuilder {
 			if (line.startsWith('Hidden Power [')) {
 				const hpType = line.slice(14, -1) as TypeName;
 				line = 'Hidden Power ' + hpType;
-				if (!set.ivs && Dex.types.isName(hpType)) {
+				if (!set.ivs && window.BattleTypeChart && window.BattleTypeChart[hpType]) {
 					set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31};
 					const hpIVs = Dex.types.get(hpType).HPivs || {};
 					for (let stat in hpIVs) {
diff --git a/play.pokemonshowdown.com/src/panels.tsx b/play.pokemonshowdown.com/src/panels.tsx
index 3bffff1b1..fddda1a3a 100644
--- a/play.pokemonshowdown.com/src/panels.tsx
+++ b/play.pokemonshowdown.com/src/panels.tsx
@@ -3,7 +3,7 @@
  *
  * Main view - sets up the frame, and the generic panels.
  *
- * Also sets up most global event listeners.
+ * Also sets up global event listeners.
  *
  * @author Guangcong Luo <guangcongluo@gmail.com>
  * @license AGPLv3
@@ -20,34 +20,6 @@ class PSRouter {
 			this.subscribeHash();
 		}
 	}
-	extractRoomID(url: string) {
-		if (url.startsWith(document.location.origin)) {
-			url = url.slice(document.location.origin.length);
-		} else {
-			if (url.startsWith('http://')) {
-				url = url.slice(7);
-			} else if (url.startsWith('https://')) {
-				url = url.slice(8);
-			}
-			if (url.startsWith(document.location.host)) {
-				url = url.slice(document.location.host.length);
-			} else if (PS.server.id === 'showdown' && url.startsWith('play.pokemonshowdown.com')) {
-				url = url.slice(24);
-			} else if (PS.server.id === 'showdown' && url.startsWith('psim.us')) {
-				url = url.slice(7);
-			} else if (url.startsWith('replay.pokemonshowdown.com')) {
-				url = url.slice(26).replace('/', '/battle-');
-			}
-		}
-		if (url.startsWith('/')) url = url.slice(1);
-
-		if (!/^[a-z0-9-]*$/.test(url)) return null;
-
-		const redirects = /^(appeals?|rooms?suggestions?|suggestions?|adminrequests?|bugs?|bugreports?|rules?|faq|credits?|privacy|contact|dex|insecure)$/;
-		if (redirects.test(url)) return null;
-
-		return url as RoomID;
-	}
 	subscribeHash() {
 		if (location.hash) {
 			const currentRoomid = location.hash.slice(1);
@@ -224,14 +196,8 @@ class PSMain extends preact.Component {
 					return;
 				}
 				if (elem.tagName === 'A' || elem.getAttribute('data-href')) {
-					const href = elem.getAttribute('data-href') || (elem as HTMLAnchorElement).href;
-					const roomid = PS.router.extractRoomID(href);
-
+					const roomid = this.roomidFromLink(elem as HTMLAnchorElement);
 					if (roomid !== null) {
-						if (elem.getAttribute('data-target') === 'replace') {
-							const room = this.getRoom(elem);
-							if (room) PS.leave(room.id);
-						}
 						PS.addRoom({
 							id: roomid,
 							parentElem: elem,
@@ -246,19 +212,8 @@ class PSMain extends preact.Component {
 					if (this.handleButtonClick(elem as HTMLButtonElement)) {
 						e.preventDefault();
 						e.stopImmediatePropagation();
-						return;
-					} else if (!elem.getAttribute('type')) {
-						// the spec says that buttons with no `type` attribute should be
-						// submit buttons, but this is a bad default so we're going
-						// to just assume they're not
-
-						// don't return, to allow <a><button> to make links that look
-						// like buttons
-						e.preventDefault();
-					} else {
-						// presumably a different part of the app is handling this button
-						return;
 					}
+					return;
 				}
 				if (elem.id.startsWith('room-')) {
 					clickedRoom = PS.rooms[elem.id.slice(5)];
@@ -307,18 +262,9 @@ class PSMain extends preact.Component {
 			}
 		});
 
-		const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
-		if (colorSchemeQuery?.media !== 'not all') {
-			colorSchemeQuery.addEventListener('change', function (cs) {
-				if (PS.prefs.theme === 'system') document.body.className = cs.matches ? 'dark' : '';
-			});
-		}
-
 		PS.prefs.subscribeAndRun(key => {
-			if (!key || key === 'theme') {
-				const dark = PS.prefs.theme === 'dark' ||
-					(PS.prefs.theme === 'system' && colorSchemeQuery && colorSchemeQuery.matches);
-				document.body.className = dark ? 'dark' : '';
+			if (!key || key === 'dark') {
+				document.body.className = PS.prefs.dark ? 'dark' : '';
 			}
 		});
 	}
@@ -351,6 +297,29 @@ class PSMain extends preact.Component {
 		}
 		return false;
 	}
+	roomidFromLink(elem: HTMLAnchorElement) {
+		let href = elem.getAttribute('data-href');
+		if (href) {
+			// yes that's what we needed
+		} else if (PS.server.id === 'showdown') {
+			if (elem.host && elem.host !== Config.routes.client && elem.host !== 'psim.us') {
+				return null;
+			}
+			href = elem.pathname;
+		} else {
+			if (elem.host !== location.host) {
+				return null;
+			}
+			href = elem.pathname;
+		}
+		const roomid = href.slice(1);
+		if (!/^[a-z0-9-]*$/.test(roomid)) {
+			return null; // not a roomid
+		}
+		const redirects = /^(appeals?|rooms?suggestions?|suggestions?|adminrequests?|bugs?|bugreports?|rules?|faq|credits?|news|privacy|contact|dex|insecure)$/;
+		if (redirects.test(roomid)) return null;
+		return roomid as RoomID;
+	}
 	static containingRoomid(elem: HTMLElement) {
 		let curElem: HTMLElement | null = elem;
 		while (curElem) {
@@ -511,8 +480,4 @@ class PSMain extends preact.Component {
 	}
 }
 
-type PanelPosition = {top?: number, bottom?: number, left?: number, right?: number} | null;
-
-function SanitizedHTML(props: {children: string}) {
-	return <div dangerouslySetInnerHTML={{__html: BattleLog.sanitizeHTML(props.children)}}/>;
-}
+type PanelPosition = {top?: number, bottom?: number, left?: number, right?: number} | null;
\ No newline at end of file
diff --git a/play.pokemonshowdown.com/style/STYLING.html b/play.pokemonshowdown.com/style/STYLING.html
index c3fcbaecf..4e9249f1f 100644
--- a/play.pokemonshowdown.com/style/STYLING.html
+++ b/play.pokemonshowdown.com/style/STYLING.html
@@ -172,13 +172,13 @@ <h2>Buttons and links</h2>
   </p>
 
   <div class="light-container">
-    Play <a href="https://pokemonshowdown.com/">Showdown!</a>
+    Play <a href="https://pokemonshowdown.com/">Showdown Pet Mods</a>
   </div>
   <div class="dark-container dark">
-    Play <a href="https://pokemonshowdown.com/">Showdown!</a>
+    Play <a href="https://pokemonshowdown.com/">Showdown Pet Mods</a>
   </div>
   <div class="dark-container code dark">
-    Play &lt;a href="https://pokemonshowdown.com/">Showdown!&lt;/a>
+    Play &lt;a href="https://pokemonshowdown.com/">Showdown Pet Mods&lt;/a>
   </div>
   <div class="clear"></div>
 
diff --git a/play.pokemonshowdown.com/style/client.css b/play.pokemonshowdown.com/style/client.css
index 80ed47b03..08ec18629 100644
--- a/play.pokemonshowdown.com/style/client.css
+++ b/play.pokemonshowdown.com/style/client.css
@@ -28,32 +28,235 @@ body {
 .pad p {
 	margin: 9px 0;
 }
+.label {
+	font-size: 9pt;
+	font-weight: bold;
+	display: block;
+}
+.optlabel {
+	font-size: 9pt;
+	display: block;
+}
 .label strong {
 	font-size: 11pt;
 	display: block;
 }
+.label .textbox {
+	display: block;
+}
+.textbox {
+	border: 1px solid #AAAAAA;
+	border-radius: 3px;
+	padding: 2px 3px;
+	font-family: Verdana, Helvetica, Arial, sans-serif;
+	font-size: 9pt;
+
+	box-shadow: inset 0px 1px 2px #CCCCCC, 1px 1px 0 rgba(255,255,255,.6);
+	background: #F8FBFD;
+	color: black;
+}
+.textbox:hover {
+	border-color: #474747;
+	box-shadow: inset 0px 1px 2px #D2D2D2, 1px 1px 0 rgba(255,255,255,.6);
+	background: #FFFFFF;
+}
+.textbox:focus,
+.button:focus {
+	outline: 0 none;
+	border-color: #004488;
+	box-shadow: inset 0px 1px 2px #D2D2D2, 0px 0px 5px #2266AA;
+}
+.textbox:focus {
+	background: #FFFFFF;
+}
+.textbox.disabled, .textbox:disabled {
+	background: #CCCCCC;
+	color: #555555;
+}
 .buttonbar {
 	margin-top: 1em;
 	text-align: center;
 }
 
+hr {
+	border-top: 1px solid #999999;
+	border-bottom: 0;
+	border-left: 0;
+	border-right: 0;
+}
+
 select {
 	font-size: 9pt;
 	font-family: Verdana, Helvetica, Arial, sans-serif;
 }
 
-.dark .tabbar a.button {
-	box-shadow: inset 0.5px 1px 1px rgba(255, 255, 255, 0.5);
+/* .dark a.button {
+	color: #222222;
+	text-shadow: 0 1px 0 white;
+	border: solid 1px #AAAAAA;
+	background: #e3e3e3;
+	background: linear-gradient(to bottom,  #f6f6f6,  #e3e3e3);
+}
+.dark a.button:hover {
+	background: #cfcfcf;
+	background: linear-gradient(to bottom,  #f2f2f2,  #c2c2c2);
+	border-color: #606060;
+}
+.dark a.button:active {
+	background: linear-gradient(to bottom,  #cfcfcf,  #f2f2f2);
+} */
+
+.dark .button.notifying {
+	background: #6BACC5;
+	border-color: #2C9CC1;
+}
+
+.dark .button.notifying.subtle {
+	border-color: #000000;
+	background: #b2d7f7;
+	color: black;
+}
+
+.dark .button.notifying:hover {
+	background: #6BACC5;
+	border-color: #FFFFFF;
+}
+
+.dark a.button:visited {
+	color: #F9F9F9;
+}
+.dark a.button.subtle-notifying {
+	color: #61A3BB;
+}
+
+.dark .textbox {
+	border-color: #888888;
+
+	box-shadow: none;
+	background: #282B2D;
+	color: #DDD;
 }
-.dark .tabbar a.button:active,
-.dark .tabbar a.button.cur:active {
+.dark .textbox:hover {
+	border-color: #BBBBBB;
 	box-shadow: none;
+	background: #222222;
+}
+.dark .textbox:focus,
+.dark .button:focus {
+	outline: 0 none;
+	border-color: #BBBBBB;
+	box-shadow: 0px 0px 4px #88CCFF, 0px 0px 4px #88CCFF;
+}
+.dark .textbox:focus {
+	background: #111111;
+}
+/* .dark .roomtab.button.closable,
+.dark .roomtab.button {
+	background: #3A3A3A;
+	border-color: #A9A9A9;
+	color: #F9F9F9;
+	text-shadow: 1px 1px 0 #111;
+}
+.dark .roomtab.button:hover {
+	background: #606060;
+	border-color: #EEEEEE;
+}
+.dark .roomtab.button.cur.closable,
+.dark .roomtab.button.cur {
+	background: #636363;
+	border-color: #A9A9A9;
+	color: #F9F9F9;
+	text-shadow: 1px 1px 0 #111;
+} */
+.dark .tabbar a.button.subtle-notifying {
+	color: #6BACC5;
+}
+.dark .tabbar a.button.notifying {
+	background: #6BACC5;
+	border-color: #2C9CC1;
+	color: #000;
+	text-shadow: none;
 }
 .dark .maintabbarbottom {
+	background: #636363;
+	border-color: #A9A9A9;
+}
+
+/* .dark button {
+	background: #777777;
+	box-shadow: none;
+	border: 1px solid #EEEEEE;
+	color: #EEEEEE;
+	border-radius: 5px;
+}
+*/
+.dark .folderButton,
+.dark .tabbar a.button,
+.dark .popupmenu .folderButton,
+.dark .popupmenu .folderButtonOpen,
+.dark .popupmenu button.button {
+	background: #484848;
+	box-shadow: inset 0 3px 4px rgba(255, 255, 255, 0.25);
+	border-color: #A9A9A9;
+	text-shadow: none;
+	color: #F9F9F9;
+}
+
+.dark .popupmenu button.folderButtonOver {
+	background: linear-gradient(to bottom, #2a2a2a, #313131);
+}
+
+.dark .popupmenu button.folderButtonOpen:hover {
+	color: #F9F9F9;
+}
+
+.dark .popupmenu button.folderButtonOpen:hover,
+.dark .folderButton:hover,
+.dark .popupmenu button.folderButton:hover,
+.dark .popupmenu button.button:hover,
+.dark .tabbar a.button:hover {
+	background: #606060;
+	border-color: #EEEEEE;
+}
+
+.dark .folderButton:hover,
+.dark .popupmenu button.folderButton:hover,
+.dark .popupmenu button.button:hover {
+	color: #F9F9F9;
+}
+
+.dark .tabbar a.button.notifying:hover {
+	background: #92C2D3;
+}
+.dark .tabbar a.button:active {
+	background: #3A3A3A;
+	border-color: #EEEEEE;
+	box-shadow: none;
+}
+.dark .button.cur,
+.dark .button.cur:hover,
+.dark .tabbar a.button.cur,
+.dark .tabbar a.button.cur:hover {
+	background: #636363;
+	border-color: #A9A9A9;
+	box-shadow: none;
+	color: #F9F9F9;
+}
+.dark .button.disabled,
+.dark .button.disabled:hover,
+.dark .button.disabled:active {
 	background: #555555;
-	border-color: #5A5A5A;
-	border-top-color: #34373b;
+	border-color: #555555;
+	box-shadow: none;
+	color: #999999;
 }
+/*
+.dark .closebutton,
+.dark .minimizebutton,
+.dark button.subtle {
+	background: none;
+	border: none;
+} */
 
 /*********************************************************
  * Header
@@ -111,7 +314,7 @@ select {
 	display: block;
 	list-style: none;
 	margin: 0;
-	padding: 2px 0 0 0;
+	padding: 0 0 0 0;
 	height: 37px;
 	text-align: left;
 
@@ -132,7 +335,6 @@ select {
 	background: #f8f8f8;
 	border: solid 1px #AAAAAA;
 	border-left: 0;
-	border-right: 0;
 	margin: -1px 0 0 0;
 	-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2);
 	-moz-box-shadow: 0 1px 2px rgba(0,0,0,.2);
@@ -163,15 +365,63 @@ select {
 	border-radius: 0;
 }
 
-.popupmenu .option {
-	width: 204px;
+.popupmenu button.button {
+	outline: none;
+	cursor: pointer;
+	text-align: center;
+	text-decoration: none;
+	border-radius: 5px;
+	-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 -1px 2px rgba(255,255,255,1);
+	-moz-box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 -1px 2px rgba(255,255,255,1);
+	box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 -1px 2px rgba(255,255,255,1);
+	border-radius: 5px;
+	font-family: Verdana, Helvetica, Arial, sans-serif;
+	display: inline-block;
+
+	/* default colors */
+	color: #222222;
+	text-shadow: 0 1px 0 white;
+	border: solid 1px #AAAAAA;
+	background: #e3e3e3;
+	background: linear-gradient(to bottom,  #f6f6f6,  #e3e3e3);
+
+	font-size: 9pt;
+	padding: 3px 8px;
 }
+
 .popupmenu i {
 	display: inline-block;
 	width: 16px;
 	color: #777777;
 }
 
+.popupmenu button.folderButton,
+.popupmenu button.folderButtonOpen {
+	width: 172px;
+	outline: none;
+	cursor: pointer;
+	text-decoration: none;
+	border-radius: 5px;
+	-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 -1px 2px rgba(255,255,255,1);
+	-moz-box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 -1px 2px rgba(255,255,255,1);
+	box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 -1px 2px rgba(255,255,255,1);
+	border-radius: 5px;
+	font-family: Verdana, Helvetica, Arial, sans-serif;
+	display: inline-block;
+	/* default colors */
+	color: #222222;
+	text-shadow: 0 1px 0 white;
+	border: solid 1px #AAAAAA;
+	background: #e3e3e3;
+	background: linear-gradient(to bottom,  #f6f6f6,  #e3e3e3);
+	font-size: 9pt;
+	padding: 3px 8px;
+}
+
+.popupmenu button.folderButtonOpen {
+	background: linear-gradient(to bottom,  #e4e4e4,  #d3d3d3);
+}
+
 .button.small,
 .button.small {
 	font-size: 8pt;
@@ -182,6 +432,16 @@ select {
 	font-size: 12pt;
 	padding: 5px 10px;
 }
+.popupmenu button.folderButton:hover,
+.popupmenu button.folderButtonOpen:hover,
+.popupmenu button.button:hover {
+	background: #cfcfcf;
+	background: linear-gradient(to bottom,  #f2f2f2,  #c2c2c2);
+	border-color: #606060;
+}
+.popupmenu button.button:active {
+	background: linear-gradient(to bottom,  #cfcfcf,  #f2f2f2);
+}
 
 .mainmenuwrapper .menugroup .button.disabled,
 .mainmenuwrapper .menugroup .button.disabled:hover,
@@ -196,6 +456,31 @@ select {
 	box-shadow: 0 1px 2px rgba(0,0,0,.1);
 }
 
+.button.notifying {
+	border-color: #AA8866;
+	background: #e3c3a3;
+}
+.button.notifying:hover {
+	border-color: #604020;
+	background: #cfaf8f;
+}
+.button.notifying.subtle {
+	border-color: #000000;
+	background: #b2d7f7;
+	color: black;
+}
+.button.cur,
+.folderButton.cur,
+.button.cur:hover {
+	color: #777777;
+	background: #f8f8f8;
+	box-shadow: none;
+	border-color: #AAAAAA;
+	cursor: default;
+}
+.button.subtle-notifying {
+	color: #AA6600;
+}
 .button.subtle-notifying .fa-comment-o:before,
 .button.notifying .fa-comment-o:before {
 	content: "\f0e6";
@@ -211,6 +496,7 @@ i.subtle:hover {
 	filter: alpha(opacity=100);
 }
 
+
 .tabbar li,
 .tabbar ul {
 	display: block;
@@ -231,11 +517,8 @@ i.subtle:hover {
 	margin: 0 -1px 0 0;
 	top: 1px;
 	border-radius: 0;
-	box-shadow: inset 0 -1px 2px rgba(255,255,255,1);
-	font-size: 11px;
-}
-.tabbar a.button.cur {
 	box-shadow: none;
+	font-size: 11px;
 }
 .tabbar a.button i {
 	display: block;
@@ -260,9 +543,7 @@ i.subtle:hover {
 	margin-right: -6px;
 }
 .tabbar a.button:hover,
-.tablist a.button:hover,
-.tabbar a.button:focus,
-.tablist a.button:focus {
+.tablist a.button:hover {
 	z-index: 10;
 }
 .tabbar a.button.cur,
@@ -311,9 +592,6 @@ i.subtle:hover {
 .closebutton:hover {
 	color: #BB2222;
 }
-.dark .closebutton:hover {
-	color: #EE7777;
-}
 span.header-username:hover {
 	color: #333333;
 }
@@ -321,10 +599,6 @@ span.header-username:hover {
 .pm-window h3:hover .minimizebutton {
 	color: #333333;
 }
-.dark .minimizebutton:hover,
-.dark .pm-window h3:hover .minimizebutton {
-	color: #CCCCCC;
-}
 .pm-window h3 .closebutton:hover + .minimizebutton {
 	color: #999999 !important;
 }
@@ -336,12 +610,10 @@ span.header-username:hover {
 	color: #661111;
 }
 .pm-minimized .minimizebutton .fa-minus-circle:before {
-	/** replace the minus with a plus when PM is minimized */
 	content: "\f055";
 }
 
 .tablist li, .tablist ul {
-	/** tablist is the menu that's displayed when the window is too wide for all the tabs */
 	list-style: none;
 	margin: 0;
 	padding: 0;
@@ -419,6 +691,7 @@ span.header-username:hover {
 .scrollable {
 	overflow: auto;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 }
 .ps-room.ps-room-light {
 	background: rgba(242,247,250,.85);
@@ -440,6 +713,7 @@ span.header-username:hover {
 
 	overflow: auto;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 	z-index: 20;
 }
 .ps-popup {
@@ -460,9 +734,7 @@ span.header-username:hover {
 	margin: 80px auto 20px auto;
 	max-width: 320px;
 }
-.ps-popup p {
-	margin: 4px 0;
-}
+.ps-popup p,
 .ps-popup h3 {
 	margin: 7px 0;
 }
@@ -510,6 +782,21 @@ p.or:after {
 .popupmenu li:first-child h3 {
 	margin-top: 0;
 }
+.popupmenu button {
+	display: block;
+	font-size: 8pt;
+	font-family: Verdana, Helvetica, Arial, sans-serif;
+	margin: 0 0 0 6px;
+	padding: 2px 3px;
+	border: 1px solid transparent;
+	border-radius: 2px;
+	background: transparent;
+	color: black;
+	width: 204px;
+	text-align: left;
+
+	box-sizing: border-box;
+}
 @media (max-height:590px) {
 	.popupmenu h3 {
 		margin-top: 2px;
@@ -524,7 +811,18 @@ p.or:after {
 	}
 }
 
-.popupmenu .button {
+.popupmenu button.sel {
+	border-color: #AAAAAA;
+	white-space: nowrap;
+	overflow: hidden;
+}
+.popupmenu button:hover,
+.popupmenu button.sel:hover {
+	border-color: #888888;
+	background: #D5D5D5;
+	color: black;
+}
+.popupmenu button.button {
 	margin: 2px auto 5px;
 	width: 184px;
 }
@@ -703,21 +1001,14 @@ p.or:after {
 	border-color: #AA8866;
 	background: #E3C3A3;
 }
-.dark .pm-window h3.pm-notifying {
-	background: #417589;
-	color: #BBBBBB;
-}
 .pm-window h3.pm-notifying:hover {
 	border-color: #604020;
 	background: #CFAF8F;
 }
-.dark .pm-window h3.pm-notifying:hover {
-	background: #417589;
-}
 .pm-window h3 .closebutton,
 .pm-window h3 .minimizebutton {
 	float: right;
-	margin: -2px -3px;
+	margin: -3px -3px;
 	width: 22px;
 	height: 22px;
 }
@@ -737,6 +1028,7 @@ p.or:after {
 
 	overflow: auto;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 	word-wrap: break-word;
 }
 .pm-buttonbar {
@@ -763,18 +1055,6 @@ p.or:after {
 	background: #f8f8f8;
 	color: #222222;
 }
-.dark .pm-window h3 {
-	background: rgba(50,50,50,.8);
-	color: #AAA;
-}
-.dark .pm-window h3:hover {
-	color: #CCC;
-}
-.dark .pm-window.focused h3,
-.dark .pm-window.focused h3:hover {
-	background: #222;
-	color: #EEE;
-}
 .challenge {
 	margin-top: -1px;
 	background: #fcd2b3;
@@ -1018,6 +1298,32 @@ p.or:after {
 	max-width: 480px;
 	text-align: left;
 }
+.roomlist a.ilink {
+	display: block;
+	margin: 2px 7px 4px 7px;
+	padding: 1px 4px 2px 4px;
+	border: 1px solid #BBCCDD;
+	background: rgba(248, 251, 253, 0.5);
+
+	border-radius: 4px;
+	text-decoration: none;
+	color: #336699;
+	text-shadow: #ffffff 0px -1px 0;
+	cursor: pointer;
+	font-size: 10pt;
+
+	overflow: hidden;
+	white-space: nowrap;
+}
+.roomlist a.ilink small {
+	font-size: 8pt;
+}
+.roomlist a.ilink:hover {
+	border-color: #778899;
+	background: #E5EAED;
+	color: #224466;
+	text-decoration: none;
+}
 .roomlist .subrooms {
 	font-size: 8pt;
 	padding-left: 20px;
@@ -1026,7 +1332,7 @@ p.or:after {
 .roomlist .subrooms i.fa-level-up {
 	margin-right: 5px;
 }
-.roomlist .subrooms a.blocklink {
+.roomlist .subrooms a.ilink {
 	display: inline-block;
 	margin: 0;
 	vertical-align: middle;
@@ -1050,6 +1356,7 @@ p.or:after {
 	overflow: auto;
 	overflow-x: hidden;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 	word-wrap: break-word;
 }
 .chat-log-add {
@@ -1219,7 +1526,6 @@ a.ilink.yours {
 	overflow: hidden;
 	transition: max-height 0.15s;
 	-webkit-transition: max-height 0.15s;
-	touch-action: none;
 }
 
 .tournament-bracket {
@@ -1227,19 +1533,15 @@ a.ilink.yours {
 	padding: 10px;
 	overflow: hidden;
 	font-size: 8pt;
-	touch-action: none;
 }
-
 .tournament-bracket-overflowing {
 	height: 200px;
 	padding: 0;
 	position: relative;
 	left: 0;
 	top: 0;
-	touch-action: none;
 }
 
-
 .tournament-popout-link {
 	position: absolute;
 	bottom: 0.5em;
@@ -1427,6 +1729,7 @@ a.ilink.yours {
 
 	overflow: auto;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 }
 .pm-buttonbar {
 	height: 21px;
@@ -1441,9 +1744,6 @@ a.ilink.yours {
 	border-right: 1px solid #AAAAAA;
 	border-bottom: 1px solid #AAAAAA;
 }
-.dark .userlist, .dark .pm-buttonbar button, .dark .chat-log-add, .dark .pm-window h3, .dark .pm-log, .dark .newsentry {
-	border-color: #5A5A5A;
-}
 .userlist-minimized {
 	height: 21px;
 	bottom: auto;
@@ -1480,7 +1780,8 @@ a.ilink.yours {
 	text-align: left;
 }
 .userlist li {
-	height: 20px;
+	border-bottom: 1px solid #CCCCCC;
+	height: 19px;
 	font: 10pt Verdana, sans-serif;
 	white-space: nowrap;
 }
@@ -1503,7 +1804,7 @@ a.ilink.yours {
 	border: 0;
 	padding: 1px 0;
 	margin: 0;
-	height: 20px;
+	height: 19px;
 	width: 100%;
 	white-space: nowrap;
 	font: 9pt Verdana, sans-serif;
@@ -2568,6 +2869,8 @@ a.ilink.yours {
 	color: #CC3311;
 	border-color: #CC3311;
 }
+.setchart-nickname input {
+}
 .setchart .setcell-details input {
 	width: 216px;
 }
@@ -2691,6 +2994,7 @@ a.ilink.yours {
 
 	overflow-y: scroll;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 }
 @media (max-height:410px) {
 	.teambuilder-results {
@@ -2727,6 +3031,7 @@ a.ilink.yours {
 
 	overflow: auto;
 	-webkit-overflow-scrolling: touch;
+	overflow-scrolling: touch;
 }
 .teamwrapper.scaled {
 	width: 640px;
@@ -2949,6 +3254,11 @@ a.ilink.yours {
 	float: left;
 	margin: 2px;
 	padding: 2px;
+
+	border: 1px solid transparent;
+	border-radius: 4px;
+	box-shadow: none;
+	background: transparent;
 }
 .avatarlist button {
 	width: 80px;
@@ -2961,8 +3271,20 @@ a.ilink.yours {
 	height: 90px;
 	background: url(../fx/client-bgsheet.png) no-repeat scroll 0px 0px;
 }
-.formlist button {
-	background-repeat: no-repeat;
+.avatarlist button.cur,
+.formlist button.cur,
+.bglist button.cur {
+	border-color: #999999;
+}
+.avatarlist button:hover,
+.avatarlist button.cur:hover,
+.formlist button:hover,
+.formlist button.cur:hover,
+.bglist button:hover,
+.bglist button.cur:hover {
+	border: 1px solid #8899AA;
+	background-color: #F1F4F9;
+	box-shadow: 1px 1px 1px #D5D5D5;
 }
 
 .effect-volume,
@@ -3003,13 +3325,11 @@ a.ilink.yours {
 .dark .pm-log-add {
 	background: rgba(0,0,0,.70);
 	color: #DDD;
-	border-color: #5A5A5A;
 }
 
 .dark .pm-log {
 	background: rgba(0,0,0,.85);
 	color: #DDD;
-	backdrop-filter: blur(4px);
 }
 
 .dark .userlist-maximized {
@@ -3046,7 +3366,6 @@ a.ilink.yours {
 .dark .chat-log {
 	background: rgba(0,0,0,.5);
 	color: #DDD;
-	backdrop-filter: blur(4px);
 }
 
 .dark .userbar .username {
@@ -3059,15 +3378,29 @@ a.ilink.yours {
 .dark .ps-popup {
 	background: #0D151E;
 	color: #DDD;
-	border-color: #34373b;
+	border-color: #888;
 
-	box-shadow: 2px 2px 3px rgba(0,0,0,.5), inset 0.5px 1px 1px rgba(255, 255, 255, 0.5);
+	box-shadow: 2px 2px 3px rgba(0,0,0,.2);
 }
 
 .dark .usergroup {
 	color: #BBB;
 }
 
+.dark .popupmenu button,
+.dark .bglist button,
+.dark .avatarlist button {
+	color: #AAA;
+	box-shadow: none;
+}
+.dark .popupmenu button:hover,
+.dark .popupmenu button.sel:hover,
+.dark .bglist button:hover,
+.dark .avatarlist button:hover {
+	border-color: #AAAAAA;
+	background-color: #AAAAAA;
+	color: black;
+}
 .dark .changeform i {
 	border-color: #bbb;
 	color: #bbb;
@@ -3139,7 +3472,7 @@ a.ilink.yours {
 }
 
 .dark .highlighted {
-	background: rgba(120,220,255,0.25);
+	background: rgba(120,220,255,0.28);
 }
 .dark .message-pm {
 	color: #00af00;
@@ -3149,10 +3482,9 @@ a.ilink.yours {
 }
 .dark a.ilink {
 	color: #4488EE;
-	border-color: #526c87;
 }
 .dark .chat.mine {
-	background: rgba(255,255,255,0.08);
+	background: rgba(255,255,255,0.05);
 }
 
 /* teambuilder */
@@ -3270,9 +3602,50 @@ a.ilink.yours {
 	color: #BBB;
 }
 
-/* for testclient */
+/* rooms */
+.dark .roomlist a.ilink {
+	box-shadow: none;
+	text-shadow: none;
+}
+
+.dark .roomlist a.ilink {
+	border-color: #7799BB;
+	background: rgba(30, 40, 50, .5);
+	color: #7799BB;
+}
+.dark .roomlist a.ilink:hover {
+	border-color: #AACCEE;
+	background: rgba(30, 40, 50, 1);
+	color: #AACCEE;
+}
+
+/* misc */
 .dark iframe.textbox,
 .dark iframe.textbox:hover,
 .dark iframe.textbox:focus {
 	background: #DDDDDD;
 }
+
+/*********************************************************
+ * <blink>
+ *********************************************************/
+
+@-webkit-keyframes blinker {
+	from { opacity: 1.0; }
+	to { opacity: 0.0; }
+}
+@keyframes blinker {
+	from { opacity: 1.0; }
+	to { opacity: 0.0; }
+}
+blink {
+	-webkit-animation-name: blinker;
+	-webkit-animation-iteration-count: infinite;
+	-webkit-animation-timing-function: cubic-bezier(1.0,0,0,1.0);
+	-webkit-animation-duration: 1s;
+	animation-name: blinker;
+	animation-iteration-count: infinite;
+	animation-timing-function: cubic-bezier(1.0,0,0,1.0);
+	animation-duration: 1s;
+	text-decoration: none;
+}
diff --git a/play.pokemonshowdown.com/testclient-beta.html b/play.pokemonshowdown.com/testclient-beta.html
index 6069704a8..65627275d 100644
--- a/play.pokemonshowdown.com/testclient-beta.html
+++ b/play.pokemonshowdown.com/testclient-beta.html
@@ -131,6 +131,8 @@ <h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa
 	<script src="data/abilities.js"></script>
 	<script src="data/search-index.js"></script>
 	<script src="data/teambuilder-tables.js"></script>
+	<script src="data/mod-sprites.js" onerror="loadRemoteData(this.src)"></script>
+	<script src="data/mod-config.js" onerror="loadRemoteData(this.src)"></script>
 	<script src="js/panel-teamdropdown.js"></script>
 	<script src="js/panel-teambuilder.js?"></script>
 	<script src="js/battle-dex-search.js?"></script>
diff --git a/play.pokemonshowdown.com/testclient.html b/play.pokemonshowdown.com/testclient.html
index 17d828fd5..d670c9af4 100644
--- a/play.pokemonshowdown.com/testclient.html
+++ b/play.pokemonshowdown.com/testclient.html
@@ -3,7 +3,7 @@
 	<head>
 		<meta charset="UTF-8" />
 		<meta id="viewport" name="viewport" content="width=device-width" />
-		<title>Showdown!</title>
+		<title>Showdown Pet Mods</title>
 		<link rel="shortcut icon" href="favicon.ico" id="dynamic-favicon" />
 		<link rel="stylesheet" href="style/battle.css" />
 		<link rel="stylesheet" href="style/client.css" />
@@ -12,7 +12,7 @@
 		<link rel="stylesheet" href="style/font-awesome.css" />
 		<meta name="robots" content="noindex" />
 		<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
-		<script src="https://play.pokemonshowdown.com/config/config.js"></script>
+		<script src="config/config.js"></script>
 		<script>
 			function loadRemoteData(src) {
 				var scriptEl = document.createElement('script');
@@ -50,7 +50,7 @@
 						<div class="pm-window news-embed">
 							<h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa-times-circle"></i></button><button class="minimizebutton" tabindex="-1" aria-label="Minimize"><i class="fa fa-minus-circle"></i></button>Latest News</h3>
 							<div class="pm-log" style="max-height:none">
-								<div class="newsentry"><h4>Test client</h4><p>Welcome to the test client! You can test client changes here!</p><p>&mdash;<strong>Zarel</strong> <small class="date">on Sep 25, 2015</small></p></div>
+								<div class="newsentry"><h4>Pet Mods Client</h4><p>Welcome to the Pet Mods client! We have teambuilder support for mods here!</p><strong></strong></div>
 							</div>
 						</div>
 					</div>
@@ -84,8 +84,6 @@ <h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa
 			window.exports = window;
 		</script>
 
-		<!-- fallback for if you've never done a full build -->
-		<script src="js/server/chat-formatter.js" onerror="loadRemoteData(this.src)"></script>
 		<script src="js/battledata.js" onerror="alert('You must build the client with `node build` before using testclient.html')"></script>
 		<script src="data/text.js" onerror="loadRemoteData(this.src)"></script>
 		<script src="data/pokedex-mini.js" onerror="loadRemoteData(this.src)"></script>
@@ -119,6 +117,8 @@ <h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa
 
 		<script src="data/search-index.js" onerror="loadRemoteData(this.src)"></script>
 		<script src="data/teambuilder-tables.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/mod-sprites.js" onerror="loadRemoteData(this.src)"></script>
+		<script src="data/mod-config.js" onerror="loadRemoteData(this.src)"></script>
 		<script src="js/battle-dex-search.js"></script>
 		<script src="js/search.js"></script>
 
@@ -131,4 +131,4 @@ <h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa
 		</script>
 
 	</body>
-</html>
+</html>
\ No newline at end of file
diff --git a/pokemonshowdown.com/js/ladder.js b/pokemonshowdown.com/js/ladder.js
index 48fae6671..08a2f8b09 100644
--- a/pokemonshowdown.com/js/ladder.js
+++ b/pokemonshowdown.com/js/ladder.js
@@ -26,17 +26,7 @@ var LadderPanel = Panels.StaticPanel.extend({
 	events: {
 		'change select[name=standing]': 'changeStanding',
 		'click button[name=openReset]': 'openReset',
-		'click button[name=cancelReset]': 'cancelReset',
-		'click button[name=copyUrl]': 'copyUrl'
-	},
-	copyUrl: function (e) {
-		var button = e.currentTarget;
-		var textbox = button.parentElement.querySelector('textarea');
-		textbox.select();
-		document.execCommand("copy");
-		button.textContent = 'Copied!';
-		e.preventDefault();
-		e.stopImmediatePropagation();
+		'click button[name=cancelReset]': 'cancelReset'
 	},
 	openReset: function (e) {
 		e.preventDefault();
@@ -80,4 +70,4 @@ var App = Panels.App.extend({
 	}
 });
 
-var app = new App();
+var app = new App();
\ No newline at end of file
diff --git a/pokemonshowdown.com/js/panels.js b/pokemonshowdown.com/js/panels.js
index e12f19a98..700d8305a 100755
--- a/pokemonshowdown.com/js/panels.js
+++ b/pokemonshowdown.com/js/panels.js
@@ -834,8 +834,7 @@ if (!Function.prototype.bind) {
 		updateBackButton: function() {
 			if (this.sourcePanel) {
 				if (this.sourcePanel.shortTitle) {
-					this.$('.pfx-backbutton').html(this.app.backButtonPrefix+this.sourcePanel.shortTitle.replace(/</g, '&lt;'));
-				}
+					this.$('.pfx-backbutton').html(this.app.backButtonPrefix+this.sourcePanel.shortTitle);				}
 				this.$('.pfx-backbutton').attr('href', this.app.root+this.sourcePanel.fragment);
 			}
 		},