{
"version": 3,
"sources": ["../../../../server/chat-commands/info.ts"],
"sourcesContent": ["/**\r\n * Informational Commands\r\n * Pokemon Showdown - https://pokemonshowdown.com/\r\n *\r\n * These are informational commands. For instance, you can define the command\r\n * 'whois' here, then use it by typing /whois into Pokemon Showdown.\r\n * For the API, see chat-plugins/COMMANDS.md\r\n *\r\n * @license MIT\r\n */\r\nimport * as net from 'net';\r\nimport {YouTube, Twitch} from '../chat-plugins/youtube';\r\nimport {Net, Utils} from '../../lib';\r\nimport {RoomSections} from './room-settings';\r\n\r\nconst ONLINE_SYMBOL = ` \\u25C9 `;\r\nconst OFFLINE_SYMBOL = ` \\u25CC `;\r\n\r\nexport function getCommonBattles(\r\n\tuserID1: ID, user1: User | null, userID2: ID, user2: User | null, connection: Connection\r\n) {\r\n\tconst battles = [];\r\n\tfor (const curRoom of Rooms.rooms.values()) {\r\n\t\tif (!curRoom.battle) continue;\r\n\t\tif (\r\n\t\t\t(user1?.inRooms.has(curRoom.roomid) || curRoom.auth.get(userID1) === Users.PLAYER_SYMBOL) &&\r\n\t\t\t(user2?.inRooms.has(curRoom.roomid) || curRoom.auth.get(userID2) === Users.PLAYER_SYMBOL)\r\n\t\t) {\r\n\t\t\tif (connection) {\r\n\t\t\t\tvoid curRoom.uploadReplay(connection.user, connection, \"forpunishment\");\r\n\t\t\t}\r\n\t\t\tbattles.push(curRoom.roomid);\r\n\t\t}\r\n\t}\r\n\treturn battles;\r\n}\r\n\r\nexport function findFormats(targetId: string, isOMSearch = false) {\r\n\tconst exactFormat = Dex.formats.get(targetId);\r\n\r\n\tconst formatList = exactFormat.exists ? [exactFormat] : Dex.formats.all();\r\n\r\n\t// Filter formats and group by section\r\n\tconst sections: {[k: string]: {name: string, formats: ID[]}} = {};\r\n\tlet totalMatches = 0;\r\n\tfor (const format of formatList) {\r\n\t\tconst sectionId = toID(format.section);\r\n\t\t// Skip generation prefix if it wasn't provided\r\n\t\tconst formatId = /^gen\\d+/.test(targetId) ? format.id : format.id.slice(4);\r\n\t\tif (\r\n\t\t\t!targetId || format[targetId + 'Show' as 'searchShow'] || sectionId === targetId ||\r\n\t\t\tformatId.startsWith(targetId) || exactFormat.exists\r\n\t\t) {\r\n\t\t\tif (isOMSearch) {\r\n\t\t\t\tconst officialFormats = [\r\n\t\t\t\t\t'ou', 'uu', 'ru', 'nu', 'pu', 'ubers', 'lc', 'monotype', 'customgame', 'doublescustomgame', 'gbusingles', 'gbudoubles',\r\n\t\t\t\t];\r\n\t\t\t\tif (format.id.startsWith('gen') && officialFormats.includes(format.id.slice(4))) {\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\ttotalMatches++;\r\n\t\t\tif (!sections[sectionId]) sections[sectionId] = {name: format.section!, formats: []};\r\n\t\t\tsections[sectionId].formats.push(format.id);\r\n\t\t}\r\n\t}\r\n\treturn {totalMatches, sections};\r\n}\r\n\r\nexport const commands: Chat.ChatCommands = {\r\n\tip: 'whois',\r\n\trooms: 'whois',\r\n\talt: 'whois',\r\n\talts: 'whois',\r\n\twhoare: 'whois',\r\n\taltsnorecurse: 'whois',\r\n\tprofile: 'whois',\r\n\twhois(target, room, user, connection, cmd) {\r\n\t\tif (room?.roomid === 'staff' && !this.runBroadcast()) return;\r\n\t\tconst targetUser = this.getUserOrSelf(target, {exactName: user.tempGroup === ' '});\r\n\t\tconst showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts' || cmd === 'altsnorecurse');\r\n\t\tconst showRecursiveAlts = showAll && (cmd !== 'altsnorecurse');\r\n\t\tif (!targetUser) {\r\n\t\t\tif (showAll) return this.parse('/offlinewhois ' + target);\r\n\t\t\treturn this.errorReply(`User ${target} not found.`);\r\n\t\t}\r\n\t\tif (showAll && !user.trusted && targetUser !== user) {\r\n\t\t\treturn this.errorReply(`/${cmd} - Access denied.`);\r\n\t\t}\r\n\r\n\t\tlet buf = Utils.html`${targetUser.tempGroup}${targetUser.name} `;\r\n\t\tconst ac = targetUser.autoconfirmed;\r\n\t\tif (ac && showAll) {\r\n\t\t\tbuf += ` (ac${targetUser.id === ac ? `` : `: ${ac}`})`;\r\n\t\t}\r\n\t\tconst trusted = targetUser.trusted;\r\n\t\tif (trusted && showAll) {\r\n\t\t\tbuf += ` (trusted${targetUser.id === trusted ? `` : `: ${trusted}`})`;\r\n\t\t}\r\n\t\tif (!targetUser.connected) buf += ` (offline)`;\r\n\t\tconst roomauth = room?.auth.getDirect(targetUser.id);\r\n\t\tif (roomauth && Config.groups[roomauth]?.name) {\r\n\t\t\tbuf += Utils.html`
${Config.groups[roomauth].name} (${roomauth})`;\r\n\t\t}\r\n\t\tif (Config.groups[targetUser.tempGroup]?.name) {\r\n\t\t\tbuf += Utils.html`
Global ${Config.groups[targetUser.tempGroup].name} (${targetUser.tempGroup})`;\r\n\t\t}\r\n\t\tif (Users.globalAuth.sectionLeaders.has(targetUser.id)) {\r\n\t\t\tbuf += Utils.html`
Section Leader (${RoomSections.sectionNames[Users.globalAuth.sectionLeaders.get(targetUser.id)!]})`;\r\n\t\t}\r\n\t\tif (targetUser.isSysop) {\r\n\t\t\tbuf += `
(Pokémon Showdown System Operator)`;\r\n\t\t}\r\n\t\tif (!targetUser.registered) {\r\n\t\t\tbuf += `
(Unregistered)`;\r\n\t\t}\r\n\t\tlet publicrooms = ``;\r\n\t\tlet hiddenrooms = ``;\r\n\t\tlet privaterooms = ``;\r\n\t\tfor (const roomid of targetUser.inRooms) {\r\n\t\t\tconst targetRoom = Rooms.get(roomid)!;\r\n\r\n\t\t\tconst authSymbol = targetRoom.auth.getDirect(targetUser.id).trim();\r\n\t\t\tconst battleTitle = (targetRoom.battle ? ` title=\"${targetRoom.title}\"` : '');\r\n\t\t\tconst output = `${authSymbol}${roomid}`;\r\n\t\t\tif (targetRoom.settings.isPrivate === true) {\r\n\t\t\t\tif (targetRoom.settings.modjoin === '~') continue;\r\n\t\t\t\tif (privaterooms) privaterooms += ` | `;\r\n\t\t\t\tprivaterooms += output;\r\n\t\t\t} else if (targetRoom.settings.isPrivate) {\r\n\t\t\t\tif (hiddenrooms) hiddenrooms += ` | `;\r\n\t\t\t\thiddenrooms += output;\r\n\t\t\t} else {\r\n\t\t\t\tif (publicrooms) publicrooms += ` | `;\r\n\t\t\t\tpublicrooms += output;\r\n\t\t\t}\r\n\t\t}\r\n\t\tbuf += `
Rooms: ${publicrooms || `(no public rooms)`}`;\r\n\r\n\t\tif (!showAll) {\r\n\t\t\treturn this.sendReplyBox(buf);\r\n\t\t}\r\n\t\tconst canViewAlts = (user === targetUser ? user.can('altsself') : user.can('alts', targetUser));\r\n\t\tconst canViewPunishments = canViewAlts ||\r\n\t\t\t(room && room.settings.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.id in room.users);\r\n\t\tconst canViewSecretRooms = user === targetUser || (canViewAlts && targetUser.locked) || user.can('makeroom');\r\n\t\tbuf += `
`;\r\n\r\n\t\tif (canViewAlts) {\r\n\t\t\tlet prevNames = targetUser.previousIDs.map(userid => {\r\n\t\t\t\tconst punishments = Punishments.userids.get(userid);\r\n\t\t\t\tif (!punishments || !user.can('alts')) return userid;\r\n\t\t\t\treturn punishments.map(\r\n\t\t\t\t\tpunishment => (\r\n\t\t\t\t\t\t`${userid}${punishment ? ` (${Punishments.punishmentTypes.get(punishment.type)?.desc || `punished`}` +\r\n\t\t\t\t\t\t`${punishment.id !== targetUser.id ? ` as ${punishment.id}` : ``})` : ``}`\r\n\t\t\t\t\t)\r\n\t\t\t\t).join(' | ');\r\n\t\t\t}).join(\", \");\r\n\t\t\tif (prevNames) buf += Utils.html`
Previous names: ${prevNames}`;\r\n\r\n\t\t\tfor (const targetAlt of targetUser.getAltUsers(true)) {\r\n\t\t\t\tif (!targetAlt.named && !targetAlt.connected) continue;\r\n\t\t\t\tif (targetAlt.tempGroup === '~' && user.tempGroup !== '~') continue;\r\n\r\n\t\t\t\tconst punishments = Punishments.userids.get(targetAlt.id) || [];\r\n\t\t\t\tconst punishMsg = !user.can('alts') ? '' : punishments.map(punishment => (\r\n\t\t\t\t\t` (${Punishments.punishmentTypes.get(punishment.type)?.desc || 'punished'}` +\r\n\t\t\t\t\t`${punishment.id !== targetAlt.id ? ` as ${punishment.id}` : ''})`\r\n\t\t\t\t)).join(' | ');\r\n\t\t\t\tbuf += Utils.html`
Alt: ${targetAlt.name}${punishMsg}`;\r\n\t\t\t\tif (!targetAlt.connected) buf += ` (offline)`;\r\n\t\t\t\tprevNames = targetAlt.previousIDs.map(userid => {\r\n\t\t\t\t\tconst p = Punishments.userids.get(userid);\r\n\t\t\t\t\tif (!p || !user.can('alts')) return userid;\r\n\t\t\t\t\treturn p.map(\r\n\t\t\t\t\t\tcur => `${userid} (${Punishments.punishmentTypes.get(cur.type)?.desc || 'punished'}` + `${cur.id !== targetAlt.id ? ` as ${cur.id}` : ``})`\r\n\t\t\t\t\t).join(' | ');\r\n\t\t\t\t}).join(\", \");\r\n\t\t\t\tif (prevNames) buf += `
Previous names: ${prevNames}`;\r\n\t\t\t}\r\n\t\t}\r\n\t\tif (canViewPunishments) {\r\n\t\t\tif (targetUser.namelocked) {\r\n\t\t\t\tbuf += `
NAMELOCKED: ${targetUser.namelocked}`;\r\n\t\t\t\tconst punishment = Punishments.userids.getByType(targetUser.locked!, 'NAMELOCK');\r\n\t\t\t\tif (punishment) {\r\n\t\t\t\t\tconst expiresIn = Punishments.checkLockExpiration(targetUser.locked);\r\n\t\t\t\t\tif (expiresIn) buf += expiresIn;\r\n\t\t\t\t\tif (punishment.reason) buf += Utils.html` (reason: ${punishment.reason})`;\r\n\t\t\t\t}\r\n\t\t\t} else if (targetUser.locked) {\r\n\t\t\t\tbuf += `
LOCKED: ${targetUser.locked}`;\r\n\t\t\t\tswitch (targetUser.locked) {\r\n\t\t\t\tcase '#rangelock':\r\n\t\t\t\t\tbuf += ` - IP or host is in a temporary range-lock`;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase '#hostfilter':\r\n\t\t\t\t\tbuf += ` - host is permanently locked for being a proxy`;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t\tconst punishment = Punishments.userids.getByType(targetUser.locked, 'LOCK');\r\n\t\t\t\tif (punishment) {\r\n\t\t\t\t\tconst expiresIn = Punishments.checkLockExpiration(targetUser.locked);\r\n\t\t\t\t\tif (expiresIn) buf += expiresIn;\r\n\t\t\t\t\tif (punishment.reason) buf += Utils.html` (reason: ${punishment.reason})`;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif (user.can('lock')) {\r\n\t\t\t\tconst battlebanned = Punishments.isBattleBanned(targetUser);\r\n\t\t\t\tif (battlebanned) {\r\n\t\t\t\t\tbuf += `
BATTLEBANNED: ${battlebanned.id}`;\r\n\t\t\t\t\tbuf += ` ${Punishments.checkPunishmentExpiration(battlebanned)}`;\r\n\t\t\t\t\tif (battlebanned.reason) buf += Utils.html` (reason: ${battlebanned.reason})`;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tconst groupchatbanned = Punishments.isGroupchatBanned(targetUser);\r\n\t\t\t\tif (groupchatbanned) {\r\n\t\t\t\t\tbuf += `
Banned from using groupchats${groupchatbanned.id !== targetUser.id ? `: ${groupchatbanned.id}` : ``}`;\r\n\t\t\t\t\tbuf += ` ${Punishments.checkPunishmentExpiration(groupchatbanned)}`;\r\n\t\t\t\t\tif (groupchatbanned.reason) buf += Utils.html` (reason: ${groupchatbanned.reason})`;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tconst ticketbanned = Punishments.isTicketBanned(targetUser.id);\r\n\t\t\t\tif (ticketbanned) {\r\n\t\t\t\t\tbuf += `
Banned from creating help tickets${ticketbanned.id !== targetUser.id ? `: ${ticketbanned.id}` : ``}`;\r\n\t\t\t\t\tbuf += ` ${Punishments.checkPunishmentExpiration(ticketbanned)}`;\r\n\t\t\t\t\tif (ticketbanned.reason) buf += Utils.html` (reason: ${ticketbanned.reason})`;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tif (targetUser.semilocked) {\r\n\t\t\t\tbuf += `
Semilocked: ${user.can('lock') ? targetUser.semilocked : \"(reason hidden)\"}`;\r\n\t\t\t}\r\n\t\t}\r\n\t\tif (user === targetUser ? user.can('ipself') : user.can('ip', targetUser)) {\r\n\t\t\tconst ips = targetUser.ips.map(ip => {\r\n\t\t\t\tconst status = [];\r\n\t\t\t\tconst punishments = Punishments.ips.get(ip);\r\n\t\t\t\tif (user.can('alts') && punishments) {\r\n\t\t\t\t\tfor (const punishment of punishments) {\r\n\t\t\t\t\t\tconst {type, id} = punishment;\r\n\t\t\t\t\t\tlet punishMsg = Punishments.punishmentTypes.get(type)?.desc || type;\r\n\t\t\t\t\t\tif (id !== targetUser.id) punishMsg += ` as ${id}`;\r\n\t\t\t\t\t\tstatus.push(punishMsg);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (Punishments.isSharedIp(ip)) {\r\n\t\t\t\t\tlet sharedStr = 'shared';\r\n\t\t\t\t\tif (Punishments.sharedIps.get(ip)) {\r\n\t\t\t\t\t\tsharedStr += `: ${Punishments.sharedIps.get(ip)}`;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tstatus.push(sharedStr);\r\n\t\t\t\t}\r\n\t\t\t\treturn `${ip}` + (status.length ? ` (${status.join('; ')})` : '');\r\n\t\t\t});\r\n\t\t\tbuf += `
IP${Chat.plural(ips)}: ${ips.join(\", \")}`;\r\n\t\t\tif (user.tempGroup !== ' ' && targetUser.latestHost) {\r\n\t\t\t\tbuf += Utils.html`
Host: ${targetUser.latestHost} [${targetUser.latestHostType}]`;\r\n\t\t\t}\r\n\t\t} else if (user === targetUser) {\r\n\t\t\tbuf += `
IP: ${connection.ip}`;\r\n\t\t}\r\n\t\tif ((user === targetUser || canViewAlts) && hiddenrooms) {\r\n\t\t\tbuf += `
Hidden rooms: ${hiddenrooms}`;\r\n\t\t}\r\n\t\tif (canViewSecretRooms && privaterooms) {\r\n\t\t\tbuf += `
Secret rooms: ${privaterooms}`;\r\n\t\t}\r\n\r\n\t\tconst gameRooms = [];\r\n\t\tfor (const curRoom of Rooms.rooms.values()) {\r\n\t\t\tif (!curRoom.game) continue;\r\n\t\t\tconst inPlayerTable = targetUser.id in curRoom.game.playerTable && !targetUser.inRooms.has(curRoom.roomid);\r\n\t\t\tconst hasPlayerSymbol = curRoom.auth.getDirect(targetUser.id) === Users.PLAYER_SYMBOL;\r\n\t\t\tconst canSeeRoom = canViewAlts || user === targetUser || !curRoom.settings.isPrivate;\r\n\r\n\t\t\tif ((inPlayerTable || hasPlayerSymbol) && canSeeRoom) {\r\n\t\t\t\tgameRooms.push(curRoom.roomid);\r\n\t\t\t}\r\n\t\t}\r\n\t\tif (gameRooms.length) {\r\n\t\t\tbuf += `
Recent games: ${gameRooms.map(id => {\r\n\t\t\t\tconst shortId = id.startsWith('battle-') ? id.slice(7) : id;\r\n\t\t\t\treturn Utils.html`${shortId}`;\r\n\t\t\t}).join(' | ')}`;\r\n\t\t}\r\n\r\n\t\tif (canViewPunishments) {\r\n\t\t\tconst punishments = Punishments.getRoomPunishments(targetUser, {checkIps: true});\r\n\r\n\t\t\tif (punishments.length) {\r\n\t\t\t\tbuf += `
Room punishments: `;\r\n\r\n\t\t\t\tbuf += punishments.map(([curRoom, curPunishment]) => {\r\n\t\t\t\t\tconst {type: punishType, id: punishUserid, expireTime, reason} = curPunishment;\r\n\t\t\t\t\tlet punishDesc = Punishments.roomPunishmentTypes.get(punishType)?.desc || punishType;\r\n\t\t\t\t\tif (punishUserid !== targetUser.id) punishDesc += ` as ${punishUserid}`;\r\n\t\t\t\t\tconst expiresIn = new Date(expireTime).getTime() - Date.now();\r\n\t\t\t\t\tconst expireString = Chat.toDurationString(expiresIn, {precision: 1});\r\n\t\t\t\t\tpunishDesc += ` for ${expireString}`;\r\n\r\n\t\t\t\t\tif (reason) punishDesc += `: ${reason}`;\r\n\t\t\t\t\treturn `${curRoom} (${punishDesc})`;\r\n\t\t\t\t}).join(', ');\r\n\t\t\t}\r\n\t\t}\r\n\t\tthis.sendReplyBox(buf);\r\n\r\n\t\tif (showRecursiveAlts && canViewAlts) {\r\n\t\t\tconst targetId = toID(target);\r\n\t\t\tfor (const alt of Users.users.values()) {\r\n\t\t\t\tif (alt !== targetUser && alt.previousIDs.includes(targetId)) {\r\n\t\t\t\t\tvoid this.parse(`/altsnorecurse ${alt.name}`);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t},\r\n\twhoishelp: [\r\n\t\t`/whois - Get details on yourself: alts, group, IP address, and rooms.`,\r\n\t\t`/whois [username] - Get details on a username: alts (Requires: % @ &), group, IP address (Requires: @ &), and rooms.`,\r\n\t],\r\n\r\n\t'chp': 'offlinewhois',\r\n\tcheckpunishment: 'offlinewhois',\r\n\tofflinewhois(target, room, user) {\r\n\t\tif (!user.trusted) {\r\n\t\t\treturn this.errorReply(\"/offlinewhois - Access denied.\");\r\n\t\t}\r\n\t\tconst userid = toID(target);\r\n\t\tif (!userid) return this.errorReply(\"Please enter a valid username.\");\r\n\t\tconst targetUser = Users.get(userid);\r\n\t\tlet buf = Utils.html`${target}`;\r\n\t\tif (!targetUser?.connected) buf += ` (offline)`;\r\n\r\n\t\tconst roomauth = room?.auth.getDirect(userid);\r\n\t\tif (roomauth && Config.groups[roomauth]?.name) {\r\n\t\t\tbuf += `
${Config.groups[roomauth].name} (${roomauth})`;\r\n\t\t}\r\n\t\tconst group = Users.globalAuth.get(userid);\r\n\t\tif (Config.groups[group]?.name) {\r\n\t\t\tbuf += `
Global ${Config.groups[group].name} (${group})`;\r\n\t\t}\r\n\t\tif (Users.globalAuth.sectionLeaders.has(userid)) {\r\n\t\t\tbuf += `
Section Leader (${RoomSections.sectionNames[Users.globalAuth.sectionLeaders.get(userid)!]})`;\r\n\t\t}\r\n\r\n\t\tbuf += `
`;\r\n\t\tlet atLeastOne = false;\r\n\r\n\t\tconst idPunishments = Punishments.userids.get(userid);\r\n\t\tif (idPunishments) {\r\n\t\t\tfor (const p of idPunishments) {\r\n\t\t\t\tconst {type: punishType, id: punishUserid, reason} = p;\r\n\t\t\t\tif (!user.can('alts') && !['LOCK', 'BAN'].includes(punishType)) continue;\r\n\t\t\t\tconst punishDesc = (Punishments.punishmentTypes.get(punishType)?.desc || punishType);\r\n\t\t\t\tbuf += `${punishDesc}: ${punishUserid}`;\r\n\t\t\t\tconst expiresIn = Punishments.checkLockExpiration(userid);\r\n\t\t\t\tif (expiresIn) buf += expiresIn;\r\n\t\t\t\tif (reason) buf += Utils.html` (reason: ${reason})`;\r\n\t\t\t\tbuf += '
';\r\n\t\t\t\tatLeastOne = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (!user.can('alts') && !atLeastOne) {\r\n\t\t\tconst hasJurisdiction = room && user.can('mute', null, room) && Punishments.roomUserids.nestedHas(room.roomid, userid);\r\n\t\t\tif (!hasJurisdiction) {\r\n\t\t\t\treturn this.errorReply(\"/checkpunishment - User not found.\");\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tconst punishments = Punishments.getRoomPunishments(targetUser || {id: userid} as User);\r\n\r\n\t\tif (punishments?.length) {\r\n\t\t\tbuf += `
Room punishments: `;\r\n\r\n\t\t\tbuf += punishments.map(([curRoom, curPunishment]) => {\r\n\t\t\t\tconst {type: punishType, id: punishUserid, expireTime, reason} = curPunishment;\r\n\t\t\t\tlet punishDesc = Punishments.roomPunishmentTypes.get(punishType)?.desc || punishType;\r\n\t\t\t\tif (punishUserid !== userid) punishDesc += ` as ${punishUserid}`;\r\n\t\t\t\tconst expiresIn = new Date(expireTime).getTime() - Date.now();\r\n\t\t\t\tconst expireString = Chat.toDurationString(expiresIn, {precision: 1});\r\n\t\t\t\tpunishDesc += ` for ${expireString}`;\r\n\r\n\t\t\t\tif (reason) punishDesc += `: ${reason}`;\r\n\t\t\t\treturn `${curRoom} (${punishDesc})`;\r\n\t\t\t}).join(', ');\r\n\t\t\tatLeastOne = true;\r\n\t\t}\r\n\t\tif (!atLeastOne) {\r\n\t\t\tbuf += `This username has no punishments associated with it.`;\r\n\t\t}\r\n\t\tthis.sendReplyBox(buf);\r\n\t},\r\n\tofflinewhoishelp: [\r\n\t\t`/offlinewhois [username] - Get details on a username without requiring them to be online.`,\r\n\t\t`Requires: trusted user. `,\r\n\t],\r\n\r\n\tsbtl: 'sharedbattles',\r\n\tsharedbattles(target, room) {\r\n\t\tthis.checkCan('lock');\r\n\r\n\t\tconst [targetUsername1, targetUsername2] = target.split(',');\r\n\t\tif (!targetUsername1 || !targetUsername2) return this.parse(`/help sharedbattles`);\r\n\t\tconst user1 = Users.get(targetUsername1);\r\n\t\tconst user2 = Users.get(targetUsername2);\r\n\t\tconst userID1 = toID(targetUsername1);\r\n\t\tconst userID2 = toID(targetUsername2);\r\n\r\n\t\tconst battles = getCommonBattles(userID1, user1, userID2, user2, this.connection);\r\n\r\n\t\tif (!battles.length) return this.sendReply(`${targetUsername1} and ${targetUsername2} have no common battles.`);\r\n\r\n\t\tthis.sendReplyBox(Utils.html`Common battles between ${targetUsername1} and ${targetUsername2}:
` + battles.map(id => {\r\n\t\t\tconst shortId = id.startsWith('battle-') ? id.slice(7) : id;\r\n\t\t\treturn Utils.html`${shortId}`;\r\n\t\t}).join(' | '));\r\n\t},\r\n\tsharedbattleshelp: [`/sharedbattles [user1], [user2] - Finds recent battles common to [user1] and [user2]. Requires % @ &`],\r\n\r\n\tsp: 'showpunishments',\r\n\tshowpunishments(target, room, user) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!room.persist) {\r\n\t\t\treturn this.errorReply(\"This command is unavailable in temporary rooms.\");\r\n\t\t}\r\n\t\treturn this.parse(`/join view-punishments-${room}`);\r\n\t},\r\n\tshowpunishmentshelp: [`/showpunishments - Shows the current punishments in the room. Requires: % @ # &`],\r\n\r\n\tsgp: 'showglobalpunishments',\r\n\tshowglobalpunishments(target, room, user) {\r\n\t\tthis.checkCan('lock');\r\n\t\treturn this.parse(`/join view-globalpunishments`);\r\n\t},\r\n\tshowglobalpunishmentshelp: [`/showpunishments - Shows the current global punishments. Requires: % @ # &`],\r\n\r\n\tasync host(target, room, user, connection, cmd) {\r\n\t\tif (!target) return this.parse('/help host');\r\n\t\tthis.checkCan('alts');\r\n\t\ttarget = target.trim();\r\n\t\tif (!net.isIPv4(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.');\r\n\t\tconst {dnsbl, host, hostType} = await IPTools.lookup(target);\r\n\t\tconst dnsblMessage = dnsbl ? ` [${dnsbl}]` : ``;\r\n\t\tthis.sendReply(`IP ${target}: ${host || \"ERROR\"} [${hostType}]${dnsblMessage}`);\r\n\t},\r\n\thosthelp: [`/host [ip] - Gets the host for a given IP. Requires: % @ &`],\r\n\r\n\tsearchip: 'ipsearch',\r\n\tipsearchall: 'ipsearch',\r\n\thostsearch: 'ipsearch',\r\n\tipsearch(target, room, user, connection, cmd) {\r\n\t\tif (!target.trim()) return this.parse(`/help ipsearch`);\r\n\t\tthis.checkCan('rangeban');\r\n\r\n\t\tconst [ipOrHost, roomid] = this.splitOne(target);\r\n\t\tconst targetRoom = roomid ? Rooms.get(roomid) : null;\r\n\t\tif (typeof targetRoom === 'undefined') {\r\n\t\t\treturn this.errorReply(`The room \"${roomid}\" does not exist.`);\r\n\t\t}\r\n\t\tconst results: string[] = [];\r\n\t\tconst isAll = (cmd === 'ipsearchall');\r\n\r\n\t\tif (/[a-z]/.test(ipOrHost)) {\r\n\t\t\t// host\r\n\t\t\tthis.sendReply(`Users with host ${ipOrHost}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);\r\n\t\t\tfor (const curUser of Users.users.values()) {\r\n\t\t\t\tif (results.length > 100 && !isAll) break;\r\n\t\t\t\tif (!curUser.latestHost?.endsWith(ipOrHost)) continue;\r\n\t\t\t\tif (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;\r\n\t\t\t\tresults.push(`${curUser.connected ? ONLINE_SYMBOL : OFFLINE_SYMBOL} ${curUser.name}`);\r\n\t\t\t}\r\n\t\t} else if (IPTools.ipRegex.test(ipOrHost)) {\r\n\t\t\t// ip\r\n\t\t\tthis.sendReply(`Users with IP ${ipOrHost}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);\r\n\t\t\tfor (const curUser of Users.users.values()) {\r\n\t\t\t\tif (!curUser.ips.some(ip => ip === ipOrHost)) continue;\r\n\t\t\t\tif (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;\r\n\t\t\t\tresults.push(`${curUser.connected ? ONLINE_SYMBOL : OFFLINE_SYMBOL} ${curUser.name}`);\r\n\t\t\t}\r\n\t\t} else if (IPTools.isValidRange(ipOrHost)) {\r\n\t\t\t// range\r\n\t\t\tthis.sendReply(`Users in IP range ${ipOrHost}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);\r\n\t\t\tconst checker = IPTools.checker(ipOrHost);\r\n\t\t\tfor (const curUser of Users.users.values()) {\r\n\t\t\t\tif (results.length > 100 && !isAll) continue;\r\n\t\t\t\tif (!curUser.ips.some(ip => checker(ip))) continue;\r\n\t\t\t\tif (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;\r\n\t\t\t\tresults.push(`${curUser.connected ? ONLINE_SYMBOL : OFFLINE_SYMBOL} ${curUser.name}`);\r\n\t\t\t}\r\n\t\t} else {\r\n\t\t\treturn this.errorReply(`${ipOrHost} is not a valid IP, IP range, or host.`);\r\n\t\t}\r\n\r\n\t\tif (!results.length) {\r\n\t\t\treturn this.sendReply(`No users found.`);\r\n\t\t}\r\n\t\tthis.sendReply(results.slice(0, 100).join('; '));\r\n\t\tif (results.length > 100 && !isAll) {\r\n\t\t\tthis.sendReply(`More than 100 users found. Use /ipsearchall for the full list.`);\r\n\t\t}\r\n\t},\r\n\tipsearchhelp: [`/ipsearch [ip|range|host], (room) - Find all users with specified IP, IP range, or host. If a room is provided only users in the room will be shown. Requires: &`],\r\n\r\n\tcheckchallenges(target, room, user) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!user.can('addhtml', null, room)) this.checkCan('ban', null, room);\r\n\t\tif (!this.runBroadcast(true)) return;\r\n\t\tif (!this.broadcasting) {\r\n\t\t\tthis.errorReply(`This command must be broadcast:`);\r\n\t\t\treturn this.parse(`/help checkchallenges`);\r\n\t\t}\r\n\t\tif (!target || !target.includes(',')) return this.parse(`/help checkchallenges`);\r\n\t\tconst {targetUser: user1, rest} = this.requireUser(target);\r\n\t\tconst {targetUser: user2, rest: rest2} = this.requireUser(rest);\r\n\t\tif (user1 === user2 || rest2) return this.parse(`/help checkchallenges`);\r\n\t\tif (!(user1.id in room.users) || !(user2.id in room.users)) {\r\n\t\t\treturn this.errorReply(`Both users must be in this room.`);\r\n\t\t}\r\n\t\tconst chall = Ladders.challenges.search(user1.id, user2.id);\r\n\r\n\t\tif (!chall) {\r\n\t\t\treturn this.sendReplyBox(Utils.html`${user1.name} and ${user2.name} are not challenging each other.`);\r\n\t\t}\r\n\t\tconst [from, to] = user1.id === chall.from ? [user1, user2] : [user2, user1];\r\n\t\tthis.sendReplyBox(Utils.html`${from.name} is challenging ${to.name} in ${Dex.formats.get(chall.format).name}.`);\r\n\t},\r\n\tcheckchallengeshelp: [`!checkchallenges [user1], [user2] - Check if the specified users are challenging each other. Requires: * @ # &`],\r\n\r\n\t/*********************************************************\r\n\t * Client fallback\r\n\t *********************************************************/\r\n\r\n\tunignore: 'ignore',\r\n\tignore(target, room, user) {\r\n\t\tif (!room) {\r\n\t\t\tthis.errorReply(`In PMs, this command can only be used by itself to ignore the person you're talking to: \"/${this.cmd}\", not \"/${this.cmd} ${target}\"`);\r\n\t\t}\r\n\t\tthis.errorReply(`You're using a custom client that doesn't support the ignore command.`);\r\n\t},\r\n\tignorehelp: [`/ignore [user] - Ignore the given [user].`],\r\n\r\n\t/*********************************************************\r\n\t * Data Search Dex\r\n\t *********************************************************/\r\n\r\n\tpstats: 'data',\r\n\tstats: 'data',\r\n\tdex: 'data',\r\n\tpokedex: 'data',\r\n\tdata(target, room, user, connection, cmd) {\r\n\t\tif (!this.runBroadcast()) return;\r\n\t\tconst gen = parseInt(cmd.substr(-1));\r\n\t\tif (gen) target += `, gen${gen}`;\r\n\r\n\t\tconst {dex, format, targets} = this.splitFormat(target, true);\r\n\r\n\t\tlet buffer = '';\r\n\t\ttarget = targets.join(',');\r\n\t\tconst targetId = toID(target);\r\n\t\tif (!targetId) return this.parse('/help data');\r\n\t\tconst targetNum = parseInt(target);\r\n\t\tif (!isNaN(targetNum) && `${targetNum}` === target) {\r\n\t\t\tfor (const pokemon of Dex.species.all()) {\r\n\t\t\t\tif (pokemon.num === targetNum) {\r\n\t\t\t\t\ttarget = pokemon.baseSpecies;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tconst newTargets = dex.dataSearch(target);\r\n\t\tconst showDetails = (cmd.startsWith('dt') || cmd === 'details');\r\n\t\tif (!newTargets || !newTargets.length) {\r\n\t\t\treturn this.errorReply(`No Pok\\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : \"\"}. (Check your spelling?)`);\r\n\t\t}\r\n\r\n\t\tfor (const [i, newTarget] of newTargets.entries()) {\r\n\t\t\tif (newTarget.isInexact && !i) {\r\n\t\t\t\tbuffer = `No Pok\\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : \"\"}. Showing the data of '${newTargets[0].name}' instead.\\n`;\r\n\t\t\t}\r\n\t\t\tlet details: {[k: string]: string} = {};\r\n\t\t\tswitch (newTarget.searchType) {\r\n\t\t\tcase 'nature':\r\n\t\t\t\tconst nature = Dex.natures.get(newTarget.name);\r\n\t\t\t\tbuffer += `${nature.name} nature: `;\r\n\t\t\t\tif (nature.plus) {\r\n\t\t\t\t\tbuffer += `+10% ${Dex.stats.names[nature.plus]}, -10% ${Dex.stats.names[nature.minus!]}.`;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tbuffer += `No effect.`;\r\n\t\t\t\t}\r\n\t\t\t\treturn this.sendReply(buffer);\r\n\t\t\tcase 'pokemon':\r\n\t\t\t\tlet pokemon = dex.species.get(newTarget.name);\r\n\t\t\t\tif (format?.onModifySpecies) {\r\n\t\t\t\t\tpokemon = format.onModifySpecies.call({dex, clampIntRange: Utils.clampIntRange, toID} as Battle, pokemon) || pokemon;\r\n\t\t\t\t}\r\n\t\t\t\tlet tierDisplay = room?.settings.dataCommandTierDisplay;\r\n\t\t\t\tif (!tierDisplay && room?.battle) {\r\n\t\t\t\t\tif (room.battle.format.includes('doubles') || room.battle.format.includes('vgc')) {\r\n\t\t\t\t\t\ttierDisplay = 'doubles tiers';\r\n\t\t\t\t\t} else if (room.battle.format.includes('nationaldex')) {\r\n\t\t\t\t\t\ttierDisplay = 'National Dex tiers';\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (!tierDisplay) tierDisplay = 'tiers';\r\n\t\t\t\tconst displayedTier = tierDisplay === 'tiers' ? pokemon.tier :\r\n\t\t\t\t\ttierDisplay === 'doubles tiers' ? pokemon.doublesTier :\r\n\t\t\t\t\ttierDisplay === 'National Dex tiers' ? pokemon.natDexTier :\r\n\t\t\t\t\tpokemon.num >= 0 ? String(pokemon.num) : pokemon.tier;\r\n\t\t\t\tbuffer += `|raw|${Chat.getDataPokemonHTML(pokemon, dex.gen, displayedTier)}\\n`;\r\n\t\t\t\tif (showDetails) {\r\n\t\t\t\t\tlet weighthit = 20;\r\n\t\t\t\t\tif (pokemon.weighthg >= 2000) {\r\n\t\t\t\t\t\tweighthit = 120;\r\n\t\t\t\t\t} else if (pokemon.weighthg >= 1000) {\r\n\t\t\t\t\t\tweighthit = 100;\r\n\t\t\t\t\t} else if (pokemon.weighthg >= 500) {\r\n\t\t\t\t\t\tweighthit = 80;\r\n\t\t\t\t\t} else if (pokemon.weighthg >= 250) {\r\n\t\t\t\t\t\tweighthit = 60;\r\n\t\t\t\t\t} else if (pokemon.weighthg >= 100) {\r\n\t\t\t\t\t\tweighthit = 40;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tdetails = {\r\n\t\t\t\t\t\t\"Dex#\": String(pokemon.num),\r\n\t\t\t\t\t\tGen: String(pokemon.gen) || 'CAP',\r\n\t\t\t\t\t\tHeight: `${pokemon.heightm} m`,\r\n\t\t\t\t\t};\r\n\t\t\t\t\tdetails[\"Weight\"] = `${pokemon.weighthg / 10} kg (${weighthit} BP)`;\r\n\t\t\t\t\tconst gmaxMove = pokemon.canGigantamax || dex.species.get(pokemon.changesFrom).canGigantamax;\r\n\t\t\t\t\tif (gmaxMove && dex.gen >= 8) details[\"G-Max Move\"] = gmaxMove;\r\n\t\t\t\t\tif (pokemon.color && dex.gen >= 5) details[\"Dex Colour\"] = pokemon.color;\r\n\t\t\t\t\tif (pokemon.eggGroups && dex.gen >= 2) details[\"Egg Group(s)\"] = pokemon.eggGroups.join(\", \");\r\n\t\t\t\t\tconst evos: string[] = [];\r\n\t\t\t\t\tfor (const evoName of pokemon.evos) {\r\n\t\t\t\t\t\tconst evo = dex.species.get(evoName);\r\n\t\t\t\t\t\tif (evo.gen <= dex.gen) {\r\n\t\t\t\t\t\t\tconst condition = evo.evoCondition ? ` ${evo.evoCondition}` : ``;\r\n\t\t\t\t\t\t\tswitch (evo.evoType) {\r\n\t\t\t\t\t\t\tcase 'levelExtra':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (level-up${condition})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tcase 'levelFriendship':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (level-up with high Friendship${condition})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tcase 'levelHold':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (level-up holding ${evo.evoItem}${condition})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tcase 'useItem':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (${evo.evoItem})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tcase 'levelMove':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (level-up with ${evo.evoMove}${condition})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tcase 'other':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (${evo.evoCondition})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tcase 'trade':\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (trade${evo.evoItem ? ` holding ${evo.evoItem}` : condition})`);\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\tdefault:\r\n\t\t\t\t\t\t\t\tevos.push(`${evo.name} (${evo.evoLevel}${condition})`);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (!evos.length) {\r\n\t\t\t\t\t\tdetails[`Does Not Evolve`] = \"\";\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tdetails[\"Evolution\"] = evos.join(\", \");\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tcase 'item':\r\n\t\t\t\tconst item = dex.items.get(newTarget.name);\r\n\t\t\t\tbuffer += `|raw|${Chat.getDataItemHTML(item)}\\n`;\r\n\t\t\t\tif (showDetails) {\r\n\t\t\t\t\tdetails = {\r\n\t\t\t\t\t\tGen: String(item.gen),\r\n\t\t\t\t\t};\r\n\r\n\t\t\t\t\tif (dex.gen >= 4) {\r\n\t\t\t\t\t\tif (item.fling) {\r\n\t\t\t\t\t\t\tdetails[\"Fling Base Power\"] = String(item.fling.basePower);\r\n\t\t\t\t\t\t\tif (item.fling.status) details[\"Fling Effect\"] = item.fling.status;\r\n\t\t\t\t\t\t\tif (item.fling.volatileStatus) details[\"Fling Effect\"] = item.fling.volatileStatus;\r\n\t\t\t\t\t\t\tif (item.isBerry) details[\"Fling Effect\"] = \"Activates the Berry's effect on the target.\";\r\n\t\t\t\t\t\t\tif (item.id === 'whiteherb') details[\"Fling Effect\"] = \"Restores the target's negative stat stages to 0.\";\r\n\t\t\t\t\t\t\tif (item.id === 'mentalherb') {\r\n\t\t\t\t\t\t\t\tconst flingEffect = \"Removes the effects of Attract, Disable, Encore, Heal Block, Taunt, and Torment from the target.\";\r\n\t\t\t\t\t\t\t\tdetails[\"Fling Effect\"] = flingEffect;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tdetails[\"Fling\"] = \"This item cannot be used with Fling.\";\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (item.naturalGift && dex.gen >= 3) {\r\n\t\t\t\t\t\tdetails[\"Natural Gift Type\"] = item.naturalGift.type;\r\n\t\t\t\t\t\tdetails[\"Natural Gift Base Power\"] = String(item.naturalGift.basePower);\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (item.isNonstandard) {\r\n\t\t\t\t\t\tdetails[`Unobtainable in Gen ${dex.gen}`] = \"\";\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tcase 'move':\r\n\t\t\t\tconst move = dex.moves.get(newTarget.name);\r\n\t\t\t\tbuffer += `|raw|${Chat.getDataMoveHTML(move)}\\n`;\r\n\t\t\t\tif (showDetails) {\r\n\t\t\t\t\tdetails = {\r\n\t\t\t\t\t\tPriority: String(move.priority),\r\n\t\t\t\t\t\tGen: String(move.gen) || 'CAP',\r\n\t\t\t\t\t};\r\n\r\n\t\t\t\t\tif (move.isNonstandard === \"Past\" && dex.gen >= 8) details[\"✗ Past Gens Only\"] = \"\";\r\n\t\t\t\t\tif (move.secondary || move.secondaries || move.hasSheerForce) details[\"✓ Boosted by Sheer Force\"] = \"\";\r\n\t\t\t\t\tif (move.flags['contact'] && dex.gen >= 3) details[\"✓ Contact\"] = \"\";\r\n\t\t\t\t\tif (move.flags['sound'] && dex.gen >= 3) details[\"✓ Sound\"] = \"\";\r\n\t\t\t\t\tif (move.flags['bullet'] && dex.gen >= 6) details[\"✓ Bullet\"] = \"\";\r\n\t\t\t\t\tif (move.flags['pulse'] && dex.gen >= 6) details[\"✓ Pulse\"] = \"\";\r\n\t\t\t\t\tif (!move.flags['protect'] && move.target !== 'self') details[\"✓ Bypasses Protect\"] = \"\";\r\n\t\t\t\t\tif (move.flags['bypasssub']) details[\"✓ Bypasses Substitutes\"] = \"\";\r\n\t\t\t\t\tif (move.flags['defrost']) details[\"✓ Thaws user\"] = \"\";\r\n\t\t\t\t\tif (move.flags['bite'] && dex.gen >= 6) details[\"✓ Bite\"] = \"\";\r\n\t\t\t\t\tif (move.flags['punch'] && dex.gen >= 4) details[\"✓ Punch\"] = \"\";\r\n\t\t\t\t\tif (move.flags['powder'] && dex.gen >= 6) details[\"✓ Powder\"] = \"\";\r\n\t\t\t\t\tif (move.flags['reflectable'] && dex.gen >= 3) details[\"✓ Bounceable\"] = \"\";\r\n\t\t\t\t\tif (move.flags['charge']) details[\"✓ Two-turn move\"] = \"\";\r\n\t\t\t\t\tif (move.flags['recharge']) details[\"✓ Has recharge turn\"] = \"\";\r\n\t\t\t\t\tif (move.flags['gravity'] && dex.gen >= 4) details[\"✗ Suppressed by Gravity\"] = \"\";\r\n\t\t\t\t\tif (move.flags['dance'] && dex.gen >= 7) details[\"✓ Dance move\"] = \"\";\r\n\t\t\t\t\tif (move.flags['slicing'] && dex.gen >= 9) details[\"✓ Slicing move\"] = \"\";\r\n\t\t\t\t\tif (move.flags['wind'] && dex.gen >= 9) details[\"✓ Wind move\"] = \"\";\r\n\r\n\t\t\t\t\tif (dex.gen >= 7) {\r\n\t\t\t\t\t\tif (move.gen >= 8 && move.isMax) {\r\n\t\t\t\t\t\t\t// Don't display Z-Power for Max/G-Max moves\r\n\t\t\t\t\t\t} else if (move.zMove?.basePower) {\r\n\t\t\t\t\t\t\tdetails[\"Z-Power\"] = String(move.zMove.basePower);\r\n\t\t\t\t\t\t} else if (move.zMove?.effect) {\r\n\t\t\t\t\t\t\tconst zEffects: {[k: string]: string} = {\r\n\t\t\t\t\t\t\t\tclearnegativeboost: \"Restores negative stat stages to 0\",\r\n\t\t\t\t\t\t\t\tcrit2: \"Crit ratio +2\",\r\n\t\t\t\t\t\t\t\theal: \"Restores HP 100%\",\r\n\t\t\t\t\t\t\t\tcurse: \"Restores HP 100% if user is Ghost type, otherwise Attack +1\",\r\n\t\t\t\t\t\t\t\tredirect: \"Redirects opposing attacks to user\",\r\n\t\t\t\t\t\t\t\thealreplacement: \"Restores replacement's HP 100%\",\r\n\t\t\t\t\t\t\t};\r\n\t\t\t\t\t\t\tdetails[\"Z-Effect\"] = zEffects[move.zMove.effect];\r\n\t\t\t\t\t\t} else if (move.zMove?.boost) {\r\n\t\t\t\t\t\t\tdetails[\"Z-Effect\"] = \"\";\r\n\t\t\t\t\t\t\tconst boost = move.zMove.boost;\r\n\t\t\t\t\t\t\tconst stats: {[k in BoostID]: string} = {\r\n\t\t\t\t\t\t\t\tatk: 'Attack', def: 'Defense', spa: 'Sp. Atk', spd: 'Sp. Def', spe: 'Speed', accuracy: 'Accuracy', evasion: 'Evasiveness',\r\n\t\t\t\t\t\t\t};\r\n\t\t\t\t\t\t\tlet h: BoostID;\r\n\t\t\t\t\t\t\tfor (h in boost) {\r\n\t\t\t\t\t\t\t\tdetails[\"Z-Effect\"] += ` ${stats[h]} +${boost[h]}`;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t} else if (move.isZ && typeof move.isZ === 'string') {\r\n\t\t\t\t\t\t\tdetails[\"✓ Z-Move\"] = \"\";\r\n\t\t\t\t\t\t\tconst zCrystal = dex.items.get(move.isZ);\r\n\t\t\t\t\t\t\tdetails[\"Z-Crystal\"] = zCrystal.name;\r\n\t\t\t\t\t\t\tif (zCrystal.itemUser) {\r\n\t\t\t\t\t\t\t\tdetails[\"User\"] = zCrystal.itemUser.join(\", \");\r\n\t\t\t\t\t\t\t\tdetails[\"Required Move\"] = dex.items.get(move.isZ).zMoveFrom!;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tdetails[\"Z-Effect\"] = \"None\";\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (dex.gen >= 8) {\r\n\t\t\t\t\t\tif (move.isMax) {\r\n\t\t\t\t\t\t\tdetails[\"✓ Max Move\"] = \"\";\r\n\t\t\t\t\t\t\tif (typeof move.isMax === \"string\") details[\"User\"] = `${move.isMax}`;\r\n\t\t\t\t\t\t} else if (move.maxMove?.basePower) {\r\n\t\t\t\t\t\t\tdetails[\"Dynamax Power\"] = String(move.maxMove.basePower);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tconst targetTypes: {[k: string]: string} = {\r\n\t\t\t\t\t\tnormal: \"One Adjacent Pok\\u00e9mon\",\r\n\t\t\t\t\t\tself: \"User\",\r\n\t\t\t\t\t\tadjacentAlly: \"One Ally\",\r\n\t\t\t\t\t\tadjacentAllyOrSelf: \"User or Ally\",\r\n\t\t\t\t\t\tadjacentFoe: \"One Adjacent Opposing Pok\\u00e9mon\",\r\n\t\t\t\t\t\tallAdjacentFoes: \"All Adjacent Opponents\",\r\n\t\t\t\t\t\tfoeSide: \"Opposing Side\",\r\n\t\t\t\t\t\tallySide: \"User's Side\",\r\n\t\t\t\t\t\tallyTeam: \"User's Side\",\r\n\t\t\t\t\t\tallAdjacent: \"All Adjacent Pok\\u00e9mon\",\r\n\t\t\t\t\t\tany: \"Any Pok\\u00e9mon\",\r\n\t\t\t\t\t\tall: \"All Pok\\u00e9mon\",\r\n\t\t\t\t\t\tscripted: \"Chosen Automatically\",\r\n\t\t\t\t\t\trandomNormal: \"Random Adjacent Opposing Pok\\u00e9mon\",\r\n\t\t\t\t\t\tallies: \"User and Allies\",\r\n\t\t\t\t\t};\r\n\t\t\t\t\tdetails[\"Target\"] = targetTypes[move.target] || \"Unknown\";\r\n\r\n\t\t\t\t\tif (move.id === 'snatch' && dex.gen >= 3) {\r\n\t\t\t\t\t\tdetails[`Non-Snatchable Moves`] = '';\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (move.id === 'mirrormove') {\r\n\t\t\t\t\t\tdetails[`Non-Mirrorable Moves`] = '';\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (move.isNonstandard === 'Unobtainable') {\r\n\t\t\t\t\t\tdetails[`Unobtainable in Gen ${dex.gen}`] = \"\";\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tcase 'ability':\r\n\t\t\t\tconst ability = dex.abilities.get(newTarget.name);\r\n\t\t\t\tbuffer += `|raw|${Chat.getDataAbilityHTML(ability)}\\n`;\r\n\t\t\t\tif (showDetails) {\r\n\t\t\t\t\tdetails = {\r\n\t\t\t\t\t\tGen: String(ability.gen) || 'CAP',\r\n\t\t\t\t\t};\r\n\t\t\t\t\tif (ability.isPermanent) details[\"✓ Not affected by Gastro Acid\"] = \"\";\r\n\t\t\t\t\tif (ability.isBreakable) details[\"✓ Ignored by Mold Breaker\"] = \"\";\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tdefault:\r\n\t\t\t\tthrow new Error(`Unrecognized searchType`);\r\n\t\t\t}\r\n\r\n\t\t\tif (showDetails) {\r\n\t\t\t\tbuffer += `|raw|${Object.entries(details).map(([detail, value]) => (\r\n\t\t\t\t\tvalue === '' ? detail : `${detail}: ${value}`\r\n\t\t\t\t)).join(\" |  \")}\\n`;\r\n\t\t\t}\r\n\t\t}\r\n\t\tthis.sendReply(buffer);\r\n\t},\r\n\tdatahelp: [\r\n\t\t`/data [pokemon/item/move/ability/nature] - Get details on this pokemon/item/move/ability/nature.`,\r\n\t\t`/data [pokemon/item/move/ability/nature], Gen [generation number/format name] - Get details on this pokemon/item/move/ability/nature for that generation/format.`,\r\n\t\t`!data [pokemon/item/move/ability/nature] - Show everyone these details. Requires: + % @ # &`,\r\n\t],\r\n\r\n\tdt: 'details',\r\n\tdt1: 'details',\r\n\tdt2: 'details',\r\n\tdt3: 'details',\r\n\tdt4: 'details',\r\n\tdt5: 'details',\r\n\tdt6: 'details',\r\n\tdt7: 'details',\r\n\tdt8: 'details',\r\n\tdt9: 'details',\r\n\tdetails(target) {\r\n\t\tif (!target) return this.parse('/help details');\r\n\t\tthis.run('data');\r\n\t},\r\n\tdetailshelp() {\r\n\t\tthis.sendReplyBox(\r\n\t\t\t`/details [Pok\\u00e9mon/item/move/ability/nature]: get additional details on this Pok\\u00e9mon/item/move/ability/nature.
` +\r\n\t\t\t`/details [Pok\\u00e9mon/item/move/ability/nature], Gen [generation number]: get details on this Pok\\u00e9mon/item/move/ability/nature in that generation.
` +\r\n\t\t\t`You can also append the generation number to /dt; for example, /dt1 Mewtwo gets details on Mewtwo in Gen 1.
` +\r\n\t\t\t`/details [Pok\\u00e9mon/item/move/ability/nature], [format]: get details on this Pok\\u00e9mon/item/move/ability/nature in that format.
` +\r\n\t\t\t`!details [Pok\\u00e9mon/item/move/ability/nature]: show everyone these details. Requires: + % @ # &`\r\n\t\t);\r\n\t},\r\n\r\n\tweaknesses: 'weakness',\r\n\tweak: 'weakness',\r\n\tresist: 'weakness',\r\n\tweakness(target, room, user) {\r\n\t\tif (!target) return this.parse('/help weakness');\r\n\t\tif (!this.runBroadcast()) return;\r\n\t\tconst {format, dex, targets} = this.splitFormat(target.split(/[,/]/).map(toID));\r\n\r\n\t\tlet isInverse = false;\r\n\t\tif (format && Dex.formats.getRuleTable(format).has('inversemod')) {\r\n\t\t\tisInverse = true;\r\n\t\t} else if (targets[targets.length - 1] === 'inverse') {\r\n\t\t\tisInverse = true;\r\n\t\t\ttargets.pop();\r\n\t\t}\r\n\r\n\t\tlet species: {types: string[], [k: string]: any} = dex.species.get(targets[0]);\r\n\t\tconst type1 = dex.types.get(targets[0]);\r\n\t\tconst type2 = dex.types.get(targets[1]);\r\n\t\tconst type3 = dex.types.get(targets[2]);\r\n\r\n\t\tif (species.exists) {\r\n\t\t\ttarget = species.name;\r\n\t\t} else {\r\n\t\t\tconst types = [];\r\n\t\t\tif (type1.exists) {\r\n\t\t\t\ttypes.push(type1.name);\r\n\t\t\t\tif (type2.exists && type2 !== type1) {\r\n\t\t\t\t\ttypes.push(type2.name);\r\n\t\t\t\t}\r\n\t\t\t\tif (type3.exists && type3 !== type1 && type3 !== type2) {\r\n\t\t\t\t\ttypes.push(type3.name);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif (types.length === 0) {\r\n\t\t\t\treturn this.sendReplyBox(Utils.html`${target} isn't a recognized type or Pokemon${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : \"\"}.`);\r\n\t\t\t}\r\n\t\t\tspecies = {types: types};\r\n\t\t\ttarget = types.join(\"/\");\r\n\t\t}\r\n\r\n\t\tconst weaknesses = [];\r\n\t\tconst resistances = [];\r\n\t\tconst immunities = [];\r\n\t\tfor (const type of dex.types.names()) {\r\n\t\t\tconst notImmune = dex.getImmunity(type, species);\r\n\t\t\tif (notImmune || isInverse) {\r\n\t\t\t\tlet typeMod = !notImmune && isInverse ? 1 : 0;\r\n\t\t\t\ttypeMod += (isInverse ? -1 : 1) * dex.getEffectiveness(type, species);\r\n\t\t\t\tswitch (typeMod) {\r\n\t\t\t\tcase 1:\r\n\t\t\t\t\tweaknesses.push(type);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase 2:\r\n\t\t\t\t\tweaknesses.push(`${type}`);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase 3:\r\n\t\t\t\t\tweaknesses.push(`${type}`);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase -1:\r\n\t\t\t\t\tresistances.push(type);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase -2:\r\n\t\t\t\t\tresistances.push(`${type}`);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase -3:\r\n\t\t\t\t\tresistances.push(`${type}`);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\timmunities.push(type);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tconst statuses: {[k: string]: string} = {\r\n\t\t\tbrn: \"Burn\",\r\n\t\t\tfrz: \"Frozen\",\r\n\t\t\thail: \"Hail damage\",\r\n\t\t\tpar: \"Paralysis\",\r\n\t\t\tpowder: \"Powder moves\",\r\n\t\t\tprankster: \"Prankster\",\r\n\t\t\tsandstorm: \"Sandstorm damage\",\r\n\t\t\ttox: \"Toxic\",\r\n\t\t\ttrapped: \"Trapping\",\r\n\t\t};\r\n\t\tfor (const status in statuses) {\r\n\t\t\tif (!dex.getImmunity(status, species)) {\r\n\t\t\t\timmunities.push(statuses[status]);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tconst buffer = [];\r\n\t\tbuffer.push(`${species.exists ? `${species.name} (ignoring abilities):` : `${target}:`}`);\r\n\t\tbuffer.push(`: ${weaknesses.join(', ') || 'None'}`);\r\n\t\tbuffer.push(`: ${resistances.join(', ') || 'None'}`);\r\n\t\tbuffer.push(`: ${immunities.join(', ') || 'None'}`);\r\n\t\tthis.sendReplyBox(buffer.join('
'));\r\n\t},\r\n\tweaknesshelp: [\r\n\t\t`/weakness [pokemon] - Provides a Pok\\u00e9mon's resistances, weaknesses, and immunities, ignoring abilities.`,\r\n\t\t`/weakness [type 1]/[type 2] - Provides a type or type combination's resistances, weaknesses, and immunities, ignoring abilities.`,\r\n\t\t`!weakness [pokemon] - Shows everyone a Pok\\u00e9mon's resistances, weaknesses, and immunities, ignoring abilities. Requires: + % @ # &`,\r\n\t\t`!weakness [type 1]/[type 2] - Shows everyone a type or type combination's resistances, weaknesses, and immunities, ignoring abilities. Requires: + % @ # &`,\r\n\t],\r\n\r\n\teff: 'effectiveness',\r\n\ttype: 'effectiveness',\r\n\tmatchup: 'effectiveness',\r\n\teffectiveness(target, room, user) {\r\n\t\tconst {dex, targets} = this.splitFormat(target.split(/[,/]/));\r\n\t\tif (targets.length !== 2) return this.errorReply(\"Attacker and defender must be separated with a comma.\");\r\n\r\n\t\tlet searchMethods = ['types', 'moves', 'species'];\r\n\t\tconst sourceMethods = ['types', 'moves'];\r\n\t\tconst targetMethods = ['types', 'species'];\r\n\t\tlet source;\r\n\t\tlet defender;\r\n\t\tlet foundData;\r\n\t\tlet atkName;\r\n\t\tlet defName;\r\n\r\n\t\tfor (let i = 0; i < 2; ++i) {\r\n\t\t\tlet method!: string;\r\n\t\t\tfor (const m of searchMethods) {\r\n\t\t\t\tfoundData = (dex as any)[m].get(targets[i]);\r\n\t\t\t\tif (foundData.exists) {\r\n\t\t\t\t\tmethod = m;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tif (!foundData.exists) return this.parse('/help effectiveness');\r\n\t\t\tif (!source && sourceMethods.includes(method)) {\r\n\t\t\t\tif (foundData.type) {\r\n\t\t\t\t\tsource = foundData;\r\n\t\t\t\t\tatkName = foundData.name;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tsource = foundData.name;\r\n\t\t\t\t\tatkName = foundData.name;\r\n\t\t\t\t}\r\n\t\t\t\tsearchMethods = targetMethods;\r\n\t\t\t} else if (!defender && targetMethods.includes(method)) {\r\n\t\t\t\tif (foundData.types) {\r\n\t\t\t\t\tdefender = foundData;\r\n\t\t\t\t\tdefName = `${foundData.name} (not counting abilities)`;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tdefender = {types: [foundData.name]};\r\n\t\t\t\t\tdefName = foundData.name;\r\n\t\t\t\t}\r\n\t\t\t\tsearchMethods = sourceMethods;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (!this.runBroadcast()) return;\r\n\r\n\t\tlet factor = 0;\r\n\t\tif (dex.getImmunity(source, defender) ||\r\n\t\t\tsource.ignoreImmunity && (source.ignoreImmunity === true || source.ignoreImmunity[source.type])) {\r\n\t\t\tlet totalTypeMod = 0;\r\n\t\t\tif (source.effectType !== 'Move' || source.category !== 'Status' && (source.basePower || source.basePowerCallback)) {\r\n\t\t\t\tfor (const type of defender.types) {\r\n\t\t\t\t\tconst baseMod = dex.getEffectiveness(source, type);\r\n\t\t\t\t\tconst moveMod = source.onEffectiveness?.call({dex: Dex} as Battle, baseMod, null, type, source);\r\n\t\t\t\t\ttotalTypeMod += typeof moveMod === 'number' ? moveMod : baseMod;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tfactor = Math.pow(2, totalTypeMod);\r\n\t\t}\r\n\r\n\t\tconst hasThousandArrows = source.id === 'thousandarrows' && defender.types.includes('Flying');\r\n\t\tconst additionalInfo = hasThousandArrows ? \"
However, Thousand Arrows will be 1x effective on the first hit.\" : \"\";\r\n\r\n\t\tthis.sendReplyBox(`${atkName} is ${factor}x effective against ${defName}.${additionalInfo}`);\r\n\t},\r\n\teffectivenesshelp: [\r\n\t\t`/effectiveness [attack], [defender] - Provides the effectiveness of a move or type on another type or a Pok\\u00e9mon.`,\r\n\t\t`!effectiveness [attack], [defender] - Shows everyone the effectiveness of a move or type on another type or a Pok\\u00e9mon.`,\r\n\t],\r\n\r\n\tcover: 'coverage',\r\n\tcoverage(target, room, user) {\r\n\t\tif (!this.runBroadcast()) return;\r\n\t\tif (!target) return this.parse(\"/help coverage\");\r\n\r\n\t\tconst {dex, targets} = this.splitFormat(target.split(/[,+/]/));\r\n\t\tconst sources: (string | Move)[] = [];\r\n\t\tlet dispTable = false;\r\n\t\tconst bestCoverage: {[k: string]: number} = {};\r\n\t\tlet hasThousandArrows = false;\r\n\r\n\t\tfor (const type of dex.types.names()) {\r\n\t\t\t// This command uses -5 to designate immunity\r\n\t\t\tbestCoverage[type] = -5;\r\n\t\t}\r\n\r\n\t\tfor (let arg of targets) {\r\n\t\t\targ = toID(arg);\r\n\r\n\t\t\t// arg is the gen?\r\n\t\t\tif (arg === dex.currentMod) continue;\r\n\r\n\t\t\t// arg is 'table' or 'all'?\r\n\t\t\tif (arg === 'table' || arg === 'all') {\r\n\t\t\t\tif (this.broadcasting) return this.sendReplyBox(\"The full table cannot be broadcast.\");\r\n\t\t\t\tdispTable = true;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// arg is a type?\r\n\t\t\tconst argType = arg.charAt(0).toUpperCase() + arg.slice(1);\r\n\t\t\tlet eff;\r\n\t\t\tif (dex.types.isName(argType)) {\r\n\t\t\t\tsources.push(argType);\r\n\t\t\t\tfor (const type in bestCoverage) {\r\n\t\t\t\t\tif (!dex.getImmunity(argType, type)) continue;\r\n\t\t\t\t\teff = dex.getEffectiveness(argType, type);\r\n\t\t\t\t\tif (eff > bestCoverage[type]) bestCoverage[type] = eff;\r\n\t\t\t\t}\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// arg is a move?\r\n\t\t\tconst move = dex.moves.get(arg);\r\n\t\t\tif (!move.exists) {\r\n\t\t\t\treturn this.errorReply(`Type or move '${arg}' not found.`);\r\n\t\t\t} else if (move.gen > dex.gen) {\r\n\t\t\t\treturn this.errorReply(`Move '${arg}' is not available in Gen ${dex.gen}.`);\r\n\t\t\t}\r\n\r\n\t\t\tif (!move.basePower && !move.basePowerCallback) continue;\r\n\t\t\tif (move.id === 'thousandarrows') hasThousandArrows = true;\r\n\t\t\tsources.push(move);\r\n\t\t\tfor (const type in bestCoverage) {\r\n\t\t\t\tif (move.id === \"struggle\") {\r\n\t\t\t\t\teff = 0;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tif (!dex.getImmunity(move.type, type) && !move.ignoreImmunity) continue;\r\n\t\t\t\t\tconst baseMod = dex.getEffectiveness(move, type);\r\n\t\t\t\t\tconst moveMod = move.onEffectiveness?.call({dex} as Battle, baseMod, null, type, move as ActiveMove);\r\n\t\t\t\t\teff = typeof moveMod === 'number' ? moveMod : baseMod;\r\n\t\t\t\t}\r\n\t\t\t\tif (eff > bestCoverage[type]) bestCoverage[type] = eff;\r\n\t\t\t}\r\n\t\t}\r\n\t\tif (sources.length === 0) return this.errorReply(\"No moves using a type table for determining damage were specified.\");\r\n\t\tif (sources.length > 4) return this.errorReply(\"Specify a maximum of 4 moves or types.\");\r\n\r\n\t\t// converts to fractional effectiveness, 0 for immune\r\n\t\tfor (const type in bestCoverage) {\r\n\t\t\tif (bestCoverage[type] === -5) {\r\n\t\t\t\tbestCoverage[type] = 0;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\tbestCoverage[type] = Math.pow(2, bestCoverage[type]);\r\n\t\t}\r\n\r\n\t\tif (!dispTable) {\r\n\t\t\tconst buffer: string[] = [];\r\n\t\t\tconst superEff: string[] = [];\r\n\t\t\tconst neutral: string[] = [];\r\n\t\t\tconst resists: string[] = [];\r\n\t\t\tconst immune: string[] = [];\r\n\r\n\t\t\tfor (const type in bestCoverage) {\r\n\t\t\t\tswitch (bestCoverage[type]) {\r\n\t\t\t\tcase 0:\r\n\t\t\t\t\timmune.push(type);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase 0.25:\r\n\t\t\t\tcase 0.5:\r\n\t\t\t\t\tresists.push(type);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase 1:\r\n\t\t\t\t\tneutral.push(type);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase 2:\r\n\t\t\t\tcase 4:\r\n\t\t\t\t\tsuperEff.push(type);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tthrow new Error(`/coverage effectiveness of ${bestCoverage[type]} from parameters: ${target}`);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tbuffer.push(`Coverage for ${sources.join(' + ')}:`);\r\n\t\t\tbuffer.push(`Super Effective: ${superEff.join(', ') || 'None'}`);\r\n\t\t\tbuffer.push(`: ${neutral.join(', ') || 'None'}`);\r\n\t\t\tbuffer.push(`: ${resists.join(', ') || 'None'}`);\r\n\t\t\tbuffer.push(`: ${immune.join(', ') || 'None'}`);\r\n\t\t\treturn this.sendReplyBox(buffer.join('
'));\r\n\t\t} else {\r\n\t\t\tlet buffer = '
| ';\r\n\t\t\tconst icon: {[k: string]: string} = {};\r\n\t\t\tfor (const type of dex.types.names()) {\r\n\t\t\t\ticon[type] = ` | ${icon[type]} | `;\r\n\t\t\t}\r\n\t\t\tbuffer += '
|---|---|
| ${icon[type1]} | `;\r\n\t\t\t\tfor (const type2 of dex.types.names()) {\r\n\t\t\t\t\tlet typing: string;\r\n\t\t\t\t\tlet cell = 'bestEff) bestEff = curEff;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tif (bestEff === -5) {\r\n\t\t\t\t\t\t\tbestEff = 0;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tbestEff = Math.pow(2, bestEff);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\tswitch (bestEff) {\r\n\t\t\t\t\tcase 0:\r\n\t\t\t\t\t\tcell += `bgcolor=#666666 title=\"${typing}\">${bestEff}`;\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase 0.25:\r\n\t\t\t\t\tcase 0.5:\r\n\t\t\t\t\t\tcell += `bgcolor=#AA5544 title=\"${typing}\">${bestEff}`;\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase 1:\r\n\t\t\t\t\t\tcell += `bgcolor=#6688AA title=\"${typing}\">${bestEff}`;\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase 2:\r\n\t\t\t\t\tcase 4:\r\n\t\t\t\t\t\tcell += `bgcolor=#559955 title=\"${typing}\">${bestEff}`;\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tdefault:\r\n\t\t\t\t\t\tthrow new Error(`/coverage effectiveness of ${bestEff} from parameters: ${target}`);\r\n\t\t\t\t\t}\r\n\t\t\t\t\tcell += ' | ';\r\n\t\t\t\t\tbuffer += cell;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tbuffer += '
| ${sections[sectionId].name} | `);\r\n\t\t\tfor (const section of sections[sectionId].formats) {\r\n\t\t\t\tconst subformat = Dex.formats.get(section);\r\n\t\t\t\tconst nameHTML = Utils.escapeHTML(subformat.name);\r\n\t\t\t\tconst desc = [...(subformat.desc ? [subformat.desc] : []), ...(subformat.threads || [])];\r\n\t\t\t\tconst descHTML = desc.length ? desc.join(\"|
|---|---|
| ${nameHTML} | ${descHTML} |
/tour join in the room's chat. You can check if your team is legal for the tournament by clicking the Validate button once you've joined and selected a team. To battle your opponent in the tournament, click the Ready! button when it appears. There are two different types of room tournaments: elimination (if a user loses more than a certain number of times, they are eliminated) and round robin (all users play against each other, and the user with the most wins is the winner).`);\r\n\t\t}\r\n\t\tif (showAll || ['vpn', 'proxy'].includes(target)) {\r\n\t\t\tbuffer.push(`${this.tr`Proxy lock help`}`);\r\n\t\t}\r\n\t\tif (showAll || ['ca', 'customavatar', 'customavatars'].includes(target)) {\r\n\t\t\tbuffer.push(this.tr`Custom avatars are given to Global Staff members, contributors (coders and spriters) to Pokemon Showdown, and Smogon badgeholders at the discretion of Zarel. They are also sometimes given out as prizes for major room events or Smogon tournaments.`);\r\n\t\t}\r\n\t\tif (showAll || ['privacy', 'private'].includes(target)) {\r\n\t\t\tbuffer.push(`${this.tr`Pok\u00E9mon Showdown privacy policy`}`);\r\n\t\t}\r\n\t\tif (showAll || ['lostpassword', 'password', 'lostpass'].includes(target)) {\r\n\t\t\tbuffer.push(`If you need your Pok\u00E9mon Showdown password reset, you can fill out a ${this.tr`Password Reset Form`}. You will need to make a Smogon account to be able to fill out the form, as password resets are processed through the Smogon forums.`);\r\n\t\t}\r\n\t\tif (!buffer.length && target) {\r\n\t\t\tthis.errorReply(`'${target}' is an invalid FAQ.`);\r\n\t\t\treturn this.parse(`/help faq`);\r\n\t\t}\r\n\t\tif (!target || showAll) {\r\n\t\t\tbuffer.unshift(`${this.tr`Frequently Asked Questions`}`);\r\n\t\t}\r\n\t\tthis.sendReplyBox(buffer.join(`${fullFormat}/challenge [user],${fullFormat} to challenge someone with it!`\r\n\t\t\t);\r\n\t\t}\r\n\t},\r\n\r\n\tadminhelp(target, room, user) {\r\n\t\tthis.checkCan('rangeban');\r\n\t\tlet cmds = Chat.allCommands();\r\n\t\tconst canExecute = (perm: string) => !(\r\n\t\t\t// gotta make sure no lower group has it\r\n\t\t\tObject.values(Config.groups).slice(1).some(f => (f as any)[perm])\r\n\t\t);\r\n\t\tcmds = cmds.filter(\r\n\t\t\tf => f.requiredPermission && canExecute(f.requiredPermission) && f.fullCmd !== this.handler?.fullCmd\r\n\t\t);\r\n\t\tcmds = Utils.sortBy(cmds, f => f.fullCmd);\r\n\t\tlet namespaces = new Map${text})` : `(no help found)`;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tbuf += `| Userid | ${showIPs ? `Latest IP | ` : \"\"}
|---|---|
| `;\r\n\t\t\tbuf += `${id} | `;\r\n\t\t\tconst ipStr = ips.map(f => `${f}`).join(', ');\r\n\t\t\tbuf += `${showIPs ? `${ipStr} | ` : \"\"}
| IP | Times Used |
|---|---|
| ${ip} | ${ipTable[ip]} |
Pokémon Showdown! supports custom rules in three ways:
`,\r\n\t\t\t`/challenge USERNAME, FORMAT @@@ RULES/tour rules RULES (see the Tournament command help)Bans are just a - followed by the thing you want to ban.
- Arceus: Ban a Pokémon (including all formes)- Arceus-Flying or - Giratina-Altered: Ban a Pokémon forme- Baton Pass: Ban a move/item/ability/etc- OU or - DUU: Ban a tier- Mega or - CAP: Ban a Pokémon category- Blaziken + Speed Boost: Ban a combination of things in a single Pokemon (you can have a Blaziken, and you can have Speed Boost on the same team, but the Blaziken can't have Speed Boost)- Drizzle ++ Swift Swim: Ban a combination of things in a team (if any Pok\u00E9mon on your team have Drizzle, no Pok\u00E9mon can have Swift Swim)Using a + instead of a - unbans that category.
+ Blaziken: Unban/unrestrict a Pokémon.The following rules can be added to challenges/tournaments to modify the style of play. Alternatively, already present rules can be removed from formats by preceding the rule name with !
However, some rules, like Obtainable, are made of subrules, that can be individually turned on and off.
| Rule Name | Description |
|---|---|
| ${rule.name} | ${desc} |
![rule name].!! [Name] = [new value]. For example, overriding the Min Source Gen on [Gen 8] VGC 2021 from 8 to 3 would look like !! Min Source Gen = 3.| Rule Name | Description |
|---|---|
| ${rule.name} | ${desc} |