/** * Informational Commands * Pokemon Showdown - https://pokemonshowdown.com/ * * These are informational commands. For instance, you can define the command * 'whois' here, then use it by typing /whois into Pokemon Showdown. * * For the API, see chat-plugins/COMMANDS.md * * @license MIT license */ 'use strict'; /** @type {ChatCommands} */ const commands = { '!whois': true, ip: 'whois', rooms: 'whois', alt: 'whois', alts: 'whois', whoare: 'whois', whois(target, room, user, connection, cmd) { if (room && room.id === 'staff' && !this.runBroadcast()) return; if (!room) room = Rooms.global; let targetUser = this.targetUserOrSelf(target, user.group === ' '); let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts'); if (!targetUser) { if (showAll) return this.parse('/offlinewhois ' + target); return this.errorReply("User " + this.targetUsername + " not found."); } if (showAll && !user.trusted && targetUser !== user) { return this.errorReply(`/${cmd} - Access denied.`); } let buf = Chat.html`${targetUser.group}${targetUser.name} `; const ac = targetUser.autoconfirmed; if (ac && showAll) buf += ` (ac${targetUser.userid === ac ? `` : `: ${ac}`})`; if (!targetUser.connected) buf += ` (offline)`; let roomauth = ''; if (room.auth && targetUser.userid in room.auth) roomauth = room.auth[targetUser.userid]; if (Config.groups[roomauth] && Config.groups[roomauth].name) { buf += `
${Config.groups[roomauth].name} (${roomauth})`; } if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) { buf += `
Global ${Config.groups[targetUser.group].name} (${targetUser.group})`; } if (targetUser.isSysop) { buf += `
(Pokémon Showdown System Operator)`; } if (!targetUser.registered) { buf += `
(Unregistered)`; } let publicrooms = ""; let hiddenrooms = ""; let privaterooms = ""; for (const roomid of targetUser.inRooms) { if (roomid === 'global') continue; let targetRoom = Rooms.get(roomid); let authSymbol = (targetRoom.auth && targetRoom.auth[targetUser.userid] ? targetRoom.auth[targetUser.userid] : ''); let battleTitle = (roomid.battle ? ` title="${roomid.title}"` : ''); let output = `${authSymbol}${roomid}`; if (targetRoom.isPrivate === true) { if (targetRoom.modjoin === '~') continue; if (privaterooms) privaterooms += " | "; privaterooms += output; } else if (targetRoom.isPrivate) { if (hiddenrooms) hiddenrooms += " | "; hiddenrooms += output; } else { if (publicrooms) publicrooms += " | "; publicrooms += output; } } buf += '
Rooms: ' + (publicrooms || '(no public rooms)'); if (!showAll) { return this.sendReplyBox(buf); } const canViewAlts = (user === targetUser || user.can('alts', targetUser)); const canViewPunishments = canViewAlts || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.userid in room.users); const canViewSecretRooms = user === targetUser || (canViewAlts && targetUser.locked) || user.can('makeroom'); buf += '
'; if (canViewAlts) { let prevNames = Object.keys(targetUser.prevNames).map(userid => { const punishment = Punishments.userids.get(userid); return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.userid ? ` as ${punishment[1]}` : ''})` : ''); }).join(", "); if (prevNames) buf += Chat.html`
Previous names: ${prevNames}`; for (const targetAlt of targetUser.getAltUsers(true)) { if (!targetAlt.named && !targetAlt.connected) continue; if (targetAlt.group === '~' && user.group !== '~') continue; const punishment = Punishments.userids.get(targetAlt.userid); const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''; buf += Chat.html`
Alt: ${targetAlt.name}${punishMsg}`; if (!targetAlt.connected) buf += ` (offline)`; prevNames = Object.keys(targetAlt.prevNames).map(userid => { const punishment = Punishments.userids.get(userid); return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''); }).join(", "); if (prevNames) buf += `
Previous names: ${prevNames}`; } } if (canViewPunishments) { if (targetUser.namelocked) { buf += `
NAMELOCKED: ${targetUser.namelocked}`; let punishment = Punishments.userids.get(targetUser.locked); if (punishment) { let expiresIn = Punishments.checkLockExpiration(targetUser.locked); if (expiresIn) buf += expiresIn; if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`; } } else if (targetUser.locked) { buf += `
LOCKED: ${targetUser.locked}`; switch (targetUser.locked) { case '#dnsbl': buf += ` - IP is in a DNS-based blacklist`; break; case '#range': buf += ` - IP or host is in a temporary range-lock`; break; case '#hostfilter': buf += ` - host is permanently locked for being a proxy`; break; } let punishment = Punishments.userids.get(targetUser.locked); if (punishment) { let expiresIn = Punishments.checkLockExpiration(targetUser.locked); if (expiresIn) buf += expiresIn; if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`; } } let battlebanned = Punishments.isBattleBanned(targetUser); if (battlebanned) { buf += `
BATTLEBANNED: ${battlebanned[1]}`; let expiresIn = new Date(battlebanned[2]).getTime() - Date.now(); let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24); let expiresText = ''; if (expiresDays >= 1) { expiresText = `in around ${Chat.count(expiresDays, "days")}`; } else { expiresText = `soon`; } if (expiresIn > 1) buf += ` (expires ${expiresText})`; if (battlebanned[3]) buf += Chat.html` (reason: ${battlebanned[3]})`; } if (targetUser.semilocked) { buf += `
Semilocked: ${targetUser.semilocked}`; } } if ((user.can('ip', targetUser) || user === targetUser)) { let ips = Object.keys(targetUser.ips); ips = ips.map(ip => { let status = []; let punishment = Punishments.ips.get(ip); if (user.can('ip') && punishment) { let [punishType, userid] = punishment; let punishMsg = Punishments.punishmentTypes.get(punishType) || 'punished'; if (userid !== targetUser.userid) punishMsg += ` as ${userid}`; status.push(punishMsg); } if (Punishments.sharedIps.has(ip)) { let sharedStr = 'shared'; if (Punishments.sharedIps.get(ip)) { sharedStr += `: ${Punishments.sharedIps.get(ip)}`; } status.push(sharedStr); } return ip + (status.length ? ` (${status.join('; ')})` : ''); }); buf += `
IP${Chat.plural(ips)}: ${ips.join(", ")}`; if (user.group !== ' ' && targetUser.latestHost) { buf += Chat.html`
Host: ${targetUser.latestHost}`; } } if (canViewAlts && hiddenrooms) { buf += `
Hidden rooms: ${hiddenrooms}`; } if (canViewSecretRooms && privaterooms) { buf += `
Secret rooms: ${privaterooms}`; } let gameRooms = []; for (const room of Rooms.rooms.values()) { if (!room.game) continue; if ((targetUser.userid in room.game.players && !targetUser.inRooms.has(room.id)) || room.auth[targetUser.userid] === Users.PLAYER_SYMBOL) { if (room.isPrivate && !canViewAlts) { continue; } gameRooms.push(room.id); } } if (gameRooms.length) { buf += '
Recent games: ' + gameRooms.map(id => { let shortId = id.startsWith('battle-') ? id.slice(7) : id; return Chat.html`${shortId}`; }).join(' | '); } if (canViewPunishments) { let punishments = Punishments.getRoomPunishments(targetUser, {checkIps: true}); if (punishments.length) { buf += `
Room punishments: `; buf += punishments.map(([room, punishment]) => { const [punishType, punishUserid, expireTime, reason] = punishment; let punishDesc = Punishments.roomPunishmentTypes.get(punishType); if (!punishDesc) punishDesc = `punished`; if (punishUserid !== targetUser.userid) punishDesc += ` as ${punishUserid}`; let expiresIn = new Date(expireTime).getTime() - Date.now(); let expireString = Chat.toDurationString(expiresIn, {precision: 1}); punishDesc += ` for ${expireString}`; if (reason) punishDesc += `: ${reason}`; return `${room} (${punishDesc})`; }).join(', '); } } this.sendReplyBox(buf); }, whoishelp: [ `/whois - Get details on yourself: alts, group, IP address, and rooms.`, `/whois [username] - Get details on a username: alts (Requires: % @ & ~), group, IP address (Requires: @ & ~), and rooms.`, ], '!offlinewhois': true, checkpunishment: 'offlinewhois', offlinewhois(target, room, user) { if (!user.trusted) { return this.errorReply("/offlinewhois - Access denied."); } let userid = toId(target); if (!userid) return this.errorReply("Please enter a valid username."); let targetUser = Users(userid); let buf = Chat.html`${target}`; if (!targetUser || !targetUser.connected) buf += ` (offline)`; let roomauth = ''; if (room && room.auth && userid in room.auth) roomauth = room.auth[userid]; if (Config.groups[roomauth] && Config.groups[roomauth].name) { buf += `
${Config.groups[roomauth].name} (${roomauth})`; } let group = (Users.usergroups[userid] || '').charAt(0); if (Config.groups[group] && Config.groups[group].name) { buf += `
Global ${Config.groups[group].name} (${group})`; } buf += `

`; let atLeastOne = false; let punishment = Punishments.userids.get(userid); if (punishment) { const [punishType, punishUserid, , reason] = punishment; const punishName = (Punishments.punishmentTypes.get(punishType) || punishType).toUpperCase(); buf += `${punishName}: ${punishUserid}`; let expiresIn = Punishments.checkLockExpiration(userid); if (expiresIn) buf += expiresIn; if (reason) buf += Chat.html` (reason: ${reason})`; buf += '
'; atLeastOne = true; } if (!user.can('alts') && !atLeastOne) { let hasJurisdiction = room && user.can('mute', null, room) && Punishments.roomUserids.nestedHas(room.id, userid); if (!hasJurisdiction) { return this.errorReply("/checkpunishment - User not found."); } } let punishments = Punishments.getRoomPunishments(targetUser || {userid}); if (punishments && punishments.length) { buf += `
Room punishments: `; buf += punishments.map(([room, punishment]) => { const [punishType, punishUserid, expireTime, reason] = punishment; let punishDesc = Punishments.roomPunishmentTypes.get(punishType); if (!punishDesc) punishDesc = `punished`; if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`; let expiresIn = new Date(expireTime).getTime() - Date.now(); let expireString = Chat.toDurationString(expiresIn, {precision: 1}); punishDesc += ` for ${expireString}`; if (reason) punishDesc += `: ${reason}`; return `${room} (${punishDesc})`; }).join(', '); atLeastOne = true; } if (!atLeastOne) { buf += `This username has no punishments associated with it.`; } this.sendReplyBox(buf); }, sp: 'showpunishments', showpunishments(target, room, user) { if (!room.chatRoomData) return this.errorReply("This command is unavailable in temporary rooms."); return this.parse(`/join view-punishments-${room}`); }, showpunishmentshelp: [`/showpunishments - Shows the current punishments in the room. Requires: % @ # & ~`], '!host': true, host(target, room, user, connection, cmd) { if (!target) return this.parse('/help host'); if (!this.can('rangeban')) return; target = target.trim(); if (!/^[0-9.]+$/.test(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.'); Dnsbl.reverse(target).then(host => { this.sendReply('IP ' + target + ': ' + (host || "ERROR")); }); }, hosthelp: [`/host [ip] - Gets the host for a given IP. Requires: & ~`], '!ipsearch': true, searchip: 'ipsearch', ipsearchall: 'ipsearch', hostsearch: 'ipsearch', ipsearch(target, room, user, connection, cmd) { if (!target.trim()) return this.parse(`/help ipsearch`); if (!this.can('rangeban')) return; let [ip, roomid] = this.splitOne(target); let targetRoom = roomid ? Rooms(roomid) : null; if (!targetRoom && targetRoom !== null) return this.errorReply(`The room "${roomid}" does not exist.`); let results = /** @type {string[]} */ ([]); let isAll = (cmd === 'ipsearchall'); if (/[a-z]/.test(ip)) { // host this.sendReply(`Users with host ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`); for (const curUser of Users.users.values()) { if (results.length > 100 && !isAll) continue; if (!curUser.latestHost || !curUser.latestHost.endsWith(ip)) continue; if (targetRoom && !curUser.inRooms.has(targetRoom.id)) continue; results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name); } if (results.length > 100 && !isAll) { return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`); } } else if (ip.slice(-1) === '*') { // IP range this.sendReply(`Users in IP range ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`); ip = ip.slice(0, -1); for (const curUser of Users.users.values()) { if (results.length > 100 && !isAll) continue; if (!curUser.latestIp.startsWith(ip)) continue; if (targetRoom && !curUser.inRooms.has(targetRoom.id)) continue; results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name); } if (results.length > 100 && !isAll) { return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`); } } else { this.sendReply(`Users with IP ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`); for (const curUser of Users.users.values()) { if (curUser.latestIp !== ip) continue; if (targetRoom && !curUser.inRooms.has(targetRoom.id)) continue; results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name); } } if (!results.length) { if (!ip.includes('.')) return this.errorReply(`${ip} is not a valid IP or host.`); return this.sendReply(`No results found.`); } return this.sendReply(results.join('; ')); }, ipsearchhelp: [`/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: & ~`], checkchallenges(target, room, user) { if (!this.can('ban', null, room)) return false; if (!this.runBroadcast(true)) return; if (!this.broadcasting) { this.errorReply(`This command must be broadcast:`); return this.parse(`/help checkchallenges`); } target = this.splitTarget(target); const user1 = this.targetUser; const user2 = Users.get(target); if (!user1 || !user2 || user1 === user2) return this.parse(`/help checkchallenges`); if (!(user1 in room.users) || !(user2 in room.users)) { return this.errorReply(`Both users must be in this room.`); } let challenges = []; const user1Challs = Ladders.challenges.get(user1.userid); if (user1Challs) { for (const chall of user1Challs) { if (chall.from === user1.userid && Users.get(chall.to) === user2) { challenges.push(Chat.html`${user1.name} is challenging ${user2.name} in ${Dex.getFormat(chall.formatid).name}.`); break; } } } const user2Challs = Ladders.challenges.get(user2.userid); if (user2Challs) { for (const chall of user2Challs) { if (chall.from === user2.userid && Users.get(chall.to) === user1) { challenges.push(Chat.html`${user2.name} is challenging ${user1.name} in ${Dex.getFormat(chall.formatid).name}.`); break; } } } if (!challenges.length) { return this.sendReplyBox(Chat.html`${user1.name} and ${user2.name} are not challenging each other.`); } this.sendReplyBox(challenges.join(`
`)); }, checkchallengeshelp: [`!checkchallenges [user1], [user2] - Check if the specified users are challenging each other. Requires: @ # & ~`], /********************************************************* * Client fallback *********************************************************/ unignore: 'ignore', ignore(target, room, user) { if (!room) this.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}"`); this.errorReply(`You're using a custom client that doesn't support the ignore command.`); }, /********************************************************* * Data Search Dex *********************************************************/ '!data': true, pstats: 'data', stats: 'data', dex: 'data', pokedex: 'data', data(target, room, user, connection, cmd) { if (!this.runBroadcast()) return; let buffer = ''; let sep = target.split(','); if (sep.length !== 2) sep = [target]; target = sep[0].trim(); let targetId = toId(target); if (!targetId) return this.parse('/help data'); let targetNum = parseInt(target); if (!isNaN(targetNum) && '' + targetNum === target) { for (let p in Dex.data.Pokedex) { let pokemon = Dex.getTemplate(p); if (pokemon.num === targetNum) { target = pokemon.species; break; } } } let mod = Dex; /** @type {Format?} */ let format = null; if (sep[1] && toId(sep[1]) in Dex.dexes) { mod = Dex.mod(toId(sep[1])); } else if (sep[1]) { format = Dex.getFormat(sep[1]); if (!format.exists) { return this.errorReply(`Unrecognized format or mod "${format.name}"`); } mod = Dex.mod(format.mod); } else if (room && room.battle) { format = Dex.getFormat(room.battle.format); mod = Dex.mod(format.mod); } let newTargets = mod.dataSearch(target); let showDetails = (cmd === 'dt' || cmd === 'details'); if (!newTargets || !newTargets.length) { return this.errorReply(`No Pok\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > mod.gen ? ` in Gen ${mod.gen}` : ""}. (Check your spelling?)`); } for (const [i, newTarget] of newTargets.entries()) { if (newTarget.isInexact && !i) { buffer = `No Pok\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > mod.gen ? ` in Gen ${mod.gen}` : ""}. Showing the data of '${newTargets[0].name}' instead.\n`; } /** @type {AnyObject} */ let details = null; switch (newTarget.searchType) { case 'nature': let nature = Dex.getNature(newTarget.name); buffer += "" + nature.name + " nature: "; if (nature.plus) { let statNames = {'atk': "Attack", 'def': "Defense", 'spa': "Special Attack", 'spd': "Special Defense", 'spe': "Speed"}; buffer += "+10% " + statNames[nature.plus] + ", -10% " + statNames[nature.minus] + "."; } else { buffer += "No effect."; } return this.sendReply(buffer); case 'pokemon': let pokemon = mod.getTemplate(newTarget.name); if (format && format.onModifyTemplate) { pokemon = format.onModifyTemplate.call(Dex, pokemon) || pokemon; } let tier = pokemon.tier; if (room && (room.id === 'smogondoubles' || ['gen7doublesou', 'gen7doublesubers', 'gen7doublesuu'].includes(room.battle && room.battle.format))) { tier = pokemon.doublesTier; } buffer += `|raw|${Chat.getDataPokemonHTML(pokemon, mod.gen, tier)}\n`; if (showDetails) { let weighthit = 20; if (pokemon.weightkg >= 200) { weighthit = 120; } else if (pokemon.weightkg >= 100) { weighthit = 100; } else if (pokemon.weightkg >= 50) { weighthit = 80; } else if (pokemon.weightkg >= 25) { weighthit = 60; } else if (pokemon.weightkg >= 10) { weighthit = 40; } details = { "Dex#": pokemon.num, "Gen": pokemon.gen || 'CAP', "Height": pokemon.heightm + " m", "Weight": pokemon.weightkg + " kg (" + weighthit + " BP)", }; if (pokemon.color && mod.gen >= 5) details["Dex Colour"] = pokemon.color; if (pokemon.eggGroups && mod.gen >= 2) details["Egg Group(s)"] = pokemon.eggGroups.join(", "); let evos = /** @type {string[]} */ ([]); for (const evoName of pokemon.evos) { const evo = mod.getTemplate(evoName); if (evo.gen <= mod.gen) { let condition = evo.evoCondition ? ` ${evo.evoCondition}` : ``; switch (evo.evoType) { case 'levelExtra': evos.push(`${evo.name} (level-up${condition})`); break; case 'levelFriendship': evos.push(`${evo.name} (level-up with high Friendship${condition})`); break; case 'levelHold': evos.push(`${evo.name} (level-up holding ${evo.evoItem}${condition})`); break; case 'stone': evos.push(`${evo.name} (${evo.evoItem})`); break; case 'levelMove': evos.push(`${evo.name} (level-up with ${evo.evoMove}${condition})`); break; case 'trade': evos.push(`${evo.name} (trade)`); break; default: evos.push(`${evo.name} (${evo.evoLevel})`); } } } if (!evos.length) { details['Does Not Evolve'] = ""; } else { details["Evolution"] = evos.join(", "); } } break; case 'item': let item = mod.getItem(newTarget.name); buffer += `|raw|${Chat.getDataItemHTML(item)}\n`; if (showDetails) { details = { "Gen": item.gen, }; if (mod.gen >= 4) { if (item.fling) { details["Fling Base Power"] = item.fling.basePower; if (item.fling.status) details["Fling Effect"] = item.fling.status; if (item.fling.volatileStatus) details["Fling Effect"] = item.fling.volatileStatus; if (item.isBerry) details["Fling Effect"] = "Activates the Berry's effect on the target."; if (item.id === 'whiteherb') details["Fling Effect"] = "Restores the target's negative stat stages to 0."; if (item.id === 'mentalherb') details["Fling Effect"] = "Removes the effects of Attract, Disable, Encore, Heal Block, Taunt, and Torment from the target."; } else { details["Fling"] = "This item cannot be used with Fling."; } } if (item.naturalGift && mod.gen >= 3) { details["Natural Gift Type"] = item.naturalGift.type; details["Natural Gift Base Power"] = item.naturalGift.basePower; } if (item.isUnreleased) { details["Unreleased in Gen " + mod.gen] = ""; } } break; case 'move': let move = mod.getMove(newTarget.name); buffer += `|raw|${Chat.getDataMoveHTML(move)}\n`; if (showDetails) { details = { "Priority": move.priority, "Gen": move.gen || 'CAP', }; if (move.secondary || move.secondaries) details["✓ Secondary effect"] = ""; if (move.flags['contact']) details["✓ Contact"] = ""; if (move.flags['sound']) details["✓ Sound"] = ""; if (move.flags['bullet']) details["✓ Bullet"] = ""; if (move.flags['pulse']) details["✓ Pulse"] = ""; if (!move.flags['protect'] && !/(ally|self)/i.test(move.target)) details["✓ Bypasses Protect"] = ""; if (move.flags['authentic']) details["✓ Bypasses Substitutes"] = ""; if (move.flags['defrost']) details["✓ Thaws user"] = ""; if (move.flags['bite']) details["✓ Bite"] = ""; if (move.flags['punch']) details["✓ Punch"] = ""; if (move.flags['powder']) details["✓ Powder"] = ""; if (move.flags['reflectable']) details["✓ Bounceable"] = ""; if (move.flags['gravity'] && mod.gen >= 4) details["✗ Suppressed by Gravity"] = ""; if (move.flags['dance'] && mod.gen >= 7) details["✓ Dance move"] = ""; if (mod.gen >= 7) { if (move.zMovePower) { details["Z-Power"] = move.zMovePower; } else if (move.zMoveEffect) { details["Z-Effect"] = { 'clearnegativeboost': "Restores negative stat stages to 0", 'crit2': "Crit ratio +2", 'heal': "Restores HP 100%", 'curse': "Restores HP 100% if user is Ghost type, otherwise Attack +1", 'redirect': "Redirects opposing attacks to user", 'healreplacement': "Restores replacement's HP 100%", }[move.zMoveEffect]; } else if (move.zMoveBoost) { details["Z-Effect"] = ""; let boost = move.zMoveBoost; let stats = {atk: 'Attack', def: 'Defense', spa: 'Sp. Atk', spd: 'Sp. Def', spe: 'Speed', accuracy: 'Accuracy', evasion: 'Evasiveness'}; for (let i in boost) { details["Z-Effect"] += " " + stats[i] + " +" + boost[i]; } } else if (move.isZ) { details["✓ Z-Move"] = ""; details["Z-Crystal"] = mod.getItem(move.isZ).name; if (move.basePower !== 1) { details["User"] = mod.getItem(move.isZ).zMoveUser.join(", "); details["Required Move"] = mod.getItem(move.isZ).zMoveFrom; } } else { details["Z-Effect"] = "None"; } } details["Target"] = { 'normal': "One Adjacent Pok\u00e9mon", 'self': "User", 'adjacentAlly': "One Ally", 'adjacentAllyOrSelf': "User or Ally", 'adjacentFoe': "One Adjacent Opposing Pok\u00e9mon", 'allAdjacentFoes': "All Adjacent Opponents", 'foeSide': "Opposing Side", 'allySide': "User's Side", 'allyTeam': "User's Side", 'allAdjacent': "All Adjacent Pok\u00e9mon", 'any': "Any Pok\u00e9mon", 'all': "All Pok\u00e9mon", }[move.target] || "Unknown"; if (move.id === 'snatch' && mod.gen >= 3) { details['Snatchable Moves'] = ''; } if (move.id === 'mirrormove') { details['Mirrorable Moves'] = ''; } if (move.isUnreleased) { details["Unreleased in Gen " + mod.gen] = ""; } } break; case 'ability': let ability = mod.getAbility(newTarget.name); buffer += `|raw|${Chat.getDataAbilityHTML(ability)}\n`; break; default: throw new Error(`Unrecognized searchType`); } if (details) { buffer += '|raw|' + Object.keys(details).map(detail => { if (details[detail] === '') return detail; return '' + detail + ': ' + details[detail]; }).join(" |  ") + '\n'; } } this.sendReply(buffer); }, datahelp: [ `/data [pokemon/item/move/ability/nature] - Get details on this pokemon/item/move/ability/nature.`, `/data [pokemon/item/move/ability/nature], Gen [generation number/format name] - Get details on this pokemon/item/move/ability/nature for that generation/format.`, `!data [pokemon/item/move/ability/nature] - Show everyone these details. Requires: + % @ # & ~`, ], '!details': true, dt: 'details', details(target) { if (!target) return this.parse('/help details'); this.run('data'); }, detailshelp: [ `/details [pokemon/item/move/ability/nature] - Get additional details on this pokemon/item/move/ability/nature.`, `/details [pokemon/item/move/ability/nature], Gen [generation number/format name] - Get details on this pokemon/item/move/ability/nature for that generation/format.`, `!details [pokemon/item/move/ability/nature] - Show everyone these details. Requires: + % @ # & ~`, ], '!weakness': true, weaknesses: 'weakness', weak: 'weakness', resist: 'weakness', weakness(target, room, user) { if (!target) return this.parse('/help weakness'); if (!this.runBroadcast()) return; target = target.trim(); let modName = target.split(','); let mod = Dex; /** @type {Format?} */ let format = null; if (modName[modName.length - 1] && toId(modName[modName.length - 1]) in Dex.dexes) { mod = Dex.mod(toId(modName[modName.length - 1])); } else if (room && room.battle) { format = Dex.getFormat(room.battle.format); mod = Dex.mod(format.mod); } let targets = target.split(/ ?[,/] ?/); /** @type {{types: string[], [k: string]: any}} */ let pokemon = mod.getTemplate(targets[0]); let type1 = mod.getType(targets[0]); let type2 = mod.getType(targets[1]); let type3 = mod.getType(targets[2]); if (pokemon.exists) { target = pokemon.species; } else { let types = []; if (type1.exists) { types.push(type1.id); if (type2.exists && type2 !== type1) { types.push(type2.id); } if (type3.exists && type3 !== type1 && type3 !== type2) { types.push(type3.id); } } if (types.length === 0) { return this.sendReplyBox(`${Chat.escapeHTML(target)} isn't a recognized type or pokemon${Dex.gen > mod.gen ? ` in Gen ${mod.gen}` : ""}.`); } pokemon = {types: types}; target = types.join("/"); } let weaknesses = []; let resistances = []; let immunities = []; for (let type in mod.data.TypeChart) { let notImmune = mod.getImmunity(type, pokemon); if (notImmune) { let typeMod = mod.getEffectiveness(type, pokemon); switch (typeMod) { case 1: weaknesses.push(type); break; case 2: weaknesses.push("" + type + ""); break; case 3: weaknesses.push("" + type + ""); break; case -1: resistances.push(type); break; case -2: resistances.push("" + type + ""); break; case -3: resistances.push("" + type + ""); break; } } else { immunities.push(type); } } let buffer = []; buffer.push(pokemon.exists ? "" + target + ' (ignoring abilities):' : '' + target + ':'); buffer.push('Weaknesses: ' + (weaknesses.join(', ') || 'None')); buffer.push('Resistances: ' + (resistances.join(', ') || 'None')); buffer.push('Immunities: ' + (immunities.join(', ') || 'None')); this.sendReplyBox(buffer.join('
')); }, weaknesshelp: [ `/weakness [pokemon] - Provides a Pok\u00e9mon's resistances, weaknesses, and immunities, ignoring abilities.`, `/weakness [type 1]/[type 2] - Provides a type or type combination's resistances, weaknesses, and immunities, ignoring abilities.`, `!weakness [pokemon] - Shows everyone a Pok\u00e9mon's resistances, weaknesses, and immunities, ignoring abilities. Requires: + % @ # & ~`, `!weakness [type 1]/[type 2] - Shows everyone a type or type combination's resistances, weaknesses, and immunities, ignoring abilities. Requires: + % @ # & ~`, ], '!effectiveness': true, eff: 'effectiveness', type: 'effectiveness', matchup: 'effectiveness', effectiveness(target, room, user) { let targets = target.split(/[,/]/).slice(0, 2); if (targets.length !== 2) return this.errorReply("Attacker and defender must be separated with a comma."); let searchMethods = ['getType', 'getMove', 'getTemplate']; let sourceMethods = ['getType', 'getMove']; let targetMethods = ['getType', 'getTemplate']; let source, defender, foundData, atkName, defName; for (let i = 0; i < 2; ++i) { let method; for (method of searchMethods) { foundData = Dex[method](targets[i]); if (foundData.exists) break; } if (!foundData.exists) return this.parse('/help effectiveness'); if (!source && sourceMethods.includes(method)) { if (foundData.type) { source = foundData; atkName = foundData.name; } else { source = foundData.id; atkName = foundData.id; } searchMethods = targetMethods; } else if (!defender && targetMethods.includes(method)) { if (foundData.types) { defender = foundData; defName = foundData.species + " (not counting abilities)"; } else { defender = {types: [foundData.id]}; defName = foundData.id; } searchMethods = sourceMethods; } } if (!this.runBroadcast()) return; let factor = 0; if (Dex.getImmunity(source, defender) || source.ignoreImmunity && (source.ignoreImmunity === true || source.ignoreImmunity[source.type])) { let totalTypeMod = 0; if (source.effectType !== 'Move' || source.category !== 'Status' && (source.basePower || source.basePowerCallback)) { for (const type of defender.types) { let baseMod = Dex.getEffectiveness(source, type); let moveMod = source.onEffectiveness && source.onEffectiveness.call(Dex, baseMod, null, type, source); totalTypeMod += typeof moveMod === 'number' ? moveMod : baseMod; } } factor = Math.pow(2, totalTypeMod); } let hasThousandArrows = source.id === 'thousandarrows' && defender.types.includes('Flying'); let additionalInfo = hasThousandArrows ? "
However, Thousand Arrows will be 1x effective on the first hit." : ""; this.sendReplyBox("" + atkName + " is " + factor + "x effective against " + defName + "." + additionalInfo); }, effectivenesshelp: [ `/effectiveness [attack], [defender] - Provides the effectiveness of a move or type on another type or a Pok\u00e9mon.`, `!effectiveness [attack], [defender] - Shows everyone the effectiveness of a move or type on another type or a Pok\u00e9mon.`, ], '!coverage': true, cover: 'coverage', coverage(target, room, user) { if (!this.runBroadcast()) return; if (!target) return this.parse("/help coverage"); let targets = target.split(/[,+]/); let sources = []; let mod = Dex; if (room && room.battle) { let format = Dex.getFormat(room.battle.format); mod = Dex.mod(format.mod); } if (targets[targets.length - 1] && toId(targets[targets.length - 1]) in Dex.dexes) { mod = Dex.mod(toId(targets[targets.length - 1])); } let dispTable = false; let bestCoverage = {}; let hasThousandArrows = false; for (let type in mod.data.TypeChart) { // This command uses -5 to designate immunity bestCoverage[type] = -5; } for (let arg of targets) { arg = toId(arg); // arg is the gen? if (arg === mod.currentMod) continue; // arg is 'table' or 'all'? if (arg === 'table' || arg === 'all') { if (this.broadcasting) return this.sendReplyBox("The full table cannot be broadcast."); dispTable = true; continue; } // arg is a type? let argType = arg.charAt(0).toUpperCase() + arg.slice(1); let eff; if (argType in mod.data.TypeChart) { sources.push(argType); for (let type in bestCoverage) { if (!mod.getImmunity(argType, type)) continue; eff = mod.getEffectiveness(argType, type); if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } // arg is a move? let move = mod.getMove(arg); if (!move.exists) { return this.errorReply(`Type or move '${arg}' not found.`); } else if (move.gen > mod.gen) { return this.errorReply(`Move '${arg}' is not available in Gen ${mod.gen}.`); } if (!move.basePower && !move.basePowerCallback) continue; if (move.id === 'thousandarrows') hasThousandArrows = true; sources.push(move); for (let type in bestCoverage) { if (move.id === "struggle") { eff = 0; } else { if (!mod.getImmunity(move.type, type) && !move.ignoreImmunity) continue; let baseMod = mod.getEffectiveness(move, type); let moveMod = move.onEffectiveness && move.onEffectiveness.call(mod, baseMod, null, type, move); eff = typeof moveMod === 'number' ? moveMod : baseMod; } if (eff > bestCoverage[type]) bestCoverage[type] = eff; } } if (sources.length === 0) return this.errorReply("No moves using a type table for determining damage were specified."); if (sources.length > 4) return this.errorReply("Specify a maximum of 4 moves or types."); // converts to fractional effectiveness, 0 for immune for (let type in bestCoverage) { if (bestCoverage[type] === -5) { bestCoverage[type] = 0; continue; } bestCoverage[type] = Math.pow(2, bestCoverage[type]); } if (!dispTable) { let buffer = []; let superEff = []; let neutral = []; let resists = []; let immune = []; for (let type in bestCoverage) { switch (bestCoverage[type]) { case 0: immune.push(type); break; case 0.25: case 0.5: resists.push(type); break; case 1: neutral.push(type); break; case 2: case 4: superEff.push(type); break; default: throw new Error("/coverage effectiveness of " + bestCoverage[type] + " from parameters: " + target); } } buffer.push('Coverage for ' + sources.join(' + ') + ':'); buffer.push('Super Effective: ' + (superEff.join(', ') || 'None')); buffer.push('Neutral: ' + (neutral.join(', ') || 'None')); buffer.push('Resists: ' + (resists.join(', ') || 'None')); buffer.push('Immunities: ' + (immune.join(', ') || 'None')); return this.sendReplyBox(buffer.join('
')); } else { let buffer = '
'; let icon = {}; for (let type in mod.data.TypeChart) { icon[type] = ''; // row of icons at top buffer += ''; } buffer += ''; for (let type1 in mod.data.TypeChart) { // assembles the rest of the rows buffer += ''; for (let type2 in mod.data.TypeChart) { let typing; let cell = ''; buffer += cell; } } buffer += '
' + icon[type] + '
' + icon[type1] + ' bestEff) bestEff = curEff; } if (bestEff === -5) { bestEff = 0; } else { bestEff = Math.pow(2, bestEff); } } switch (bestEff) { case 0: cell += 'bgcolor=#666666 title="' + typing + '">' + bestEff + ''; break; case 0.25: case 0.5: cell += 'bgcolor=#AA5544 title="' + typing + '">' + bestEff + ''; break; case 1: cell += 'bgcolor=#6688AA title="' + typing + '">' + bestEff + ''; break; case 2: case 4: cell += 'bgcolor=#559955 title="' + typing + '">' + bestEff + ''; break; default: throw new Error("/coverage effectiveness of " + bestEff + " from parameters: " + target); } cell += '
'; if (hasThousandArrows) { buffer += "
Thousand Arrows has neutral type effectiveness on Flying-type Pok\u00e9mon if not already smacked down."; } this.sendReplyBox('Coverage for ' + sources.join(' + ') + ':
' + buffer); } }, coveragehelp: [ `/coverage [move 1], [move 2] ... - Provides the best effectiveness match-up against all defending types for given moves or attacking types`, `!coverage [move 1], [move 2] ... - Shows this information to everyone.`, `Adding the parameter 'all' or 'table' will display the information with a table of all type combinations.`, ], '!statcalc': true, statcalc(target, room, user) { if (!target) return this.parse("/help statcalc"); if (!this.runBroadcast()) return; let targets = target.split(' '); let lvlSet, natureSet, ivSet, evSet, baseSet, modSet, realSet = false; let pokemon; let useStat = ''; let level = 100; let calcHP = false; let nature = 1.0; let iv = 31; let ev = 252; let baseStat = -1; let modifier = 0; let positiveMod = true; let realStat; for (const arg of targets) { let lowercase = arg.toLowerCase(); if (!lvlSet) { if (lowercase === 'lc') { level = 5; lvlSet = true; continue; } else if (lowercase === 'vgc') { level = 50; lvlSet = true; continue; } else if (lowercase.startsWith('lv') || lowercase.startsWith('level')) { level = parseInt(arg.replace(/\D/g, '')); lvlSet = true; if (level < 1 || level > 9999) { return this.sendReplyBox('Invalid value for level: ' + level); } continue; } } if (!useStat) { switch (lowercase) { case 'hp': case 'hitpoints': calcHP = true; useStat = 'hp'; continue; case 'atk': case 'attack': useStat = 'atk'; continue; case 'def': case 'defense': useStat = 'def'; continue; case 'spa': useStat = 'spa'; continue; case 'spd': case 'sdef': useStat = 'spd'; continue; case 'spe': case 'speed': useStat = 'spe'; continue; } } if (!natureSet) { if (lowercase === 'boosting' || lowercase === 'positive') { nature = 1.1; natureSet = true; continue; } else if (lowercase === 'negative' || lowercase === 'inhibiting') { nature = 0.9; natureSet = true; continue; } else if (lowercase === 'neutral') { continue; } } if (!ivSet) { if (lowercase.endsWith('iv') || lowercase.endsWith('ivs')) { iv = parseInt(arg); ivSet = true; if (isNaN(iv)) { return this.sendReplyBox('Invalid value for IVs: ' + Chat.escapeHTML(arg)); } continue; } } if (!evSet) { if (lowercase === 'invested' || lowercase === 'max') { evSet = true; if (lowercase === 'max' && !natureSet) { nature = 1.1; natureSet = true; } } else if (lowercase === 'uninvested') { ev = 0; evSet = true; } else if (lowercase.endsWith('ev') || lowercase.endsWith('evs') || lowercase.endsWith('+') || lowercase.endsWith('-')) { ev = parseInt(arg); evSet = true; if (isNaN(ev)) { return this.sendReplyBox('Invalid value for EVs: ' + Chat.escapeHTML(arg)); } if (ev > 255 || ev < 0) { return this.sendReplyBox('The amount of EVs should be between 0 and 255.'); } if (!natureSet) { if (arg.includes('+')) { nature = 1.1; natureSet = true; } else if (arg.includes('-')) { nature = 0.9; natureSet = true; } } continue; } } if (!modSet) { if (['band', 'scarf', 'specs'].includes(arg)) { modifier = 1; modSet = true; } else if (arg.charAt(0) === '+') { modifier = parseInt(arg.charAt(1)); modSet = true; } else if (arg.charAt(0) === '-') { positiveMod = false; modifier = parseInt(arg.charAt(1)); modSet = true; } if (isNaN(modifier)) { return this.sendReplyBox('Invalid value for modifier: ' + Chat.escapeHTML(modifier)); } if (modifier > 6) { return this.sendReplyBox('Modifier should be a number between -6 and +6'); } if (modSet) continue; } if (!pokemon) { let testPoke = Dex.getTemplate(arg); if (testPoke.baseStats) { pokemon = testPoke.baseStats; baseSet = true; continue; } } let tempStat = parseInt(arg); if (!realSet) { if (lowercase.endsWith('real')) { realStat = tempStat; realSet = true; if (isNaN(realStat)) { return this.sendReplyBox('Invalid value for target real stat: ' + Chat.escapeHTML(arg)); } if (realStat < 0) { return this.sendReplyBox('The target real stat must be greater than 0.'); } continue; } } if (!isNaN(tempStat) && !baseSet && tempStat > 0 && tempStat < 256) { baseStat = tempStat; baseSet = true; } } if (pokemon) { if (useStat) { baseStat = pokemon[useStat]; } else { return this.sendReplyBox('No stat found.'); } } if (realSet) { if (!baseSet) { if (calcHP) { baseStat = Math.ceil((100 * realStat - 10 - level * (ev / 4 + iv + 100)) / (2 * level)); } else { if (!positiveMod) { realStat *= (2 + modifier) / 2; } else { realStat *= 2 / (2 + modifier); } baseStat = Math.ceil((100 * Math.ceil(realStat) - nature * (level * (ev / 4 + iv) + 500)) / (2 * level * nature)); } if (baseStat < 0) { return this.sendReplyBox('No valid value for base stat possible with given parameters.'); } } else if (!evSet) { if (calcHP) { ev = Math.ceil(100 * (realStat - 10) / level - 2 * (baseStat + 50)); } else { if (!positiveMod) { realStat *= (2 + modifier) / 2; } else { realStat *= 2 / (2 + modifier); } ev = Math.ceil(-1 * (2 * (nature * (baseStat * level + 250) - 50 * Math.ceil(realStat))) / (level * nature)); } ev -= 31; if (ev < 0) iv += ev; ev *= 4; if (iv < 0 || ev > 255) { return this.sendReplyBox('No valid EV/IV combination possible with given parameters. Maybe try a different nature?' + ev); } } else { return this.sendReplyBox('Too many parameters given; nothing to calculate.'); } } else if (baseStat < 0) { return this.sendReplyBox('No valid value for base stat found.'); } let output; if (calcHP) { output = (((iv + (2 * baseStat) + (ev / 4) + 100) * level) / 100) + 10; } else { output = Math.floor(nature * Math.floor((((iv + (2 * baseStat) + (ev / 4)) * level) / 100) + 5)); if (positiveMod) { output *= (2 + modifier) / 2; } else { output *= 2 / (2 + modifier); } } return this.sendReplyBox('Base ' + baseStat + (calcHP ? ' HP ' : ' ') + 'at level ' + level + ' with ' + iv + ' IVs, ' + ev + (nature === 1.1 ? '+' : (nature === 0.9 ? '-' : '')) + ' EVs' + (modifier > 0 && !calcHP ? ' at ' + (positiveMod ? '+' : '-') + modifier : '') + ': ' + Math.floor(output) + '.'); }, statcalchelp: [ `/statcalc [level] [base stat] [IVs] [nature] [EVs] [modifier] (only base stat is required) - Calculates what the actual stat of a Pokémon is with the given parameters. For example, '/statcalc lv50 100 30iv positive 252ev scarf' calculates the speed of a base 100 scarfer with HP Ice in Battle Spot, and '/statcalc uninvested 90 neutral' calculates the attack of an uninvested Crobat.`, `!statcalc [level] [base stat] [IVs] [nature] [EVs] [modifier] (only base stat is required) - Shows this information to everyone.`, `Inputing 'hp' as an argument makes it use the formula for HP. Instead of giving nature, '+' and '-' can be appended to the EV amount (e.g. 252+ev) to signify a boosting or inhibiting nature.`, `An actual stat can be given in place of a base stat or EVs. In this case, the minumum base stat or EVs necessary to have that real stat with the given parameters will be determined. For example, '/statcalc 502real 252+ +1' calculates the minimum base speed necessary for a positive natured fully invested scarfer to outspeed`, ], /********************************************************* * Informational commands *********************************************************/ '!uptime': true, uptime(target, room, user) { if (!this.can('broadcast')) return false; if (!this.runBroadcast()) return; let uptime = process.uptime(); let uptimeText; if (uptime > 24 * 60 * 60) { let uptimeDays = Math.floor(uptime / (24 * 60 * 60)); uptimeText = uptimeDays + " " + (uptimeDays === 1 ? "day" : "days"); let uptimeHours = Math.floor(uptime / (60 * 60)) - uptimeDays * 24; if (uptimeHours) uptimeText += ", " + uptimeHours + " " + (uptimeHours === 1 ? "hour" : "hours"); } else { uptimeText = Chat.toDurationString(uptime * 1000); } this.sendReplyBox("Uptime: " + uptimeText + ""); }, '!servertime': true, servertime(target, room, user) { if (!this.runBroadcast()) return; let servertime = new Date(); this.sendReplyBox(`Server time: ${servertime.toLocaleString()}`); }, '!groups': true, groups(target, room, user) { if (!this.runBroadcast()) return; const showRoom = (target !== 'global'); const showGlobal = (target !== 'room' && target !== 'rooms'); const roomRanks = [ `Room ranks`, `+ Voice - They can use ! commands like !groups`, `% Driver - The above, and they can mute and warn`, `@ Moderator - The above, and they can room ban users`, `* Bot - Like Moderator, but makes it clear that this user is a bot`, `# Room Owner - They are leaders of the room and can almost totally control it`, ]; const globalRanks = [ `Global ranks`, `+ Global Voice - They can use ! commands like !groups`, `% Global Driver - The above, and they can also lock users and check for alts`, `@ Global Moderator - The above, and they can globally ban users`, `* Global Bot - Like Moderator, but makes it clear that this user is a bot`, `& Global Leader - The above, and they can promote to global moderator and force ties`, `~ Global Administrator - They can do anything, like change what this message says`, ]; this.sendReplyBox( (showRoom ? roomRanks.map(str => this.tr(str)).join('
') : ``) + (showRoom && showGlobal ? `

` : ``) + (showGlobal ? globalRanks.map(str => this.tr(str)).join('
') : ``) ); }, groupshelp: [ `/groups - Explains what the symbols (like % and @) before people's names mean.`, `/groups [global|room] - Explains only global or room symbols.`, `!groups - Shows everyone that information. Requires: + % @ # & ~`, ], '!punishments': true, punishments(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( `Room punishments:
` + `warn - Displays a popup with the rules.
` + `mute - Mutes a user (makes them unable to talk) for 7 minutes.
` + `hourmute - Mutes a user for 60 minutes.
` + `ban - Bans a user (makes them unable to join the room) for 2 days.
` + `blacklist - Bans a user for a year.
` + `
` + `Global punishments:
` + `lock - Locks a user (makes them unable to talk in any rooms or PM non-staff) for 2 days.
` + `weeklock - Locks a user for a week.
` + `namelock - Locks a user and prevents them from having a username for 2 days.
` + `globalban - Globally bans (makes them unable to connect and play games) for a week.` ); }, punishmentshelp: [ `/punishments - Explains punishments.`, `!punishments - Show everyone that information. Requires: + % @ # & ~`, ], '!opensource': true, repo: 'opensource', repository: 'opensource', git: 'opensource', opensource(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( `Pokémon Showdown is open source:
` + `- Language: JavaScript (Node.js)
` + `- What's new?
` + `- Server source code
` + `- Client source code
` + `- Dex source code` ); }, opensourcehelp: [ `/opensource - Links to PS's source code repository.`, `!opensource - Show everyone that information. Requires: + % @ # & ~`, ], '!staff': true, staff(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox(`Pokémon Showdown Staff List`); }, '!forums': true, forums(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox(`Pokémon Showdown Forums`); }, '!privacypolicy': true, privacypolicy(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( `- We log PMs so you can report them - staff can't look at them without permission unless there's a law enforcement reason.
` + `- We log IPs to enforce bans and mutes.
` + `- We use cookies to save your login info and teams, and for Google Analytics and AdSense.
` + `- For more information, you can read our full privacy policy.` ); }, '!suggestions': true, suggestions(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox(`Make a suggestion for Pokémon Showdown`); }, '!bugs': true, bugreport: 'bugs', bugreports: 'bugs', bugs(target, room, user) { if (!this.runBroadcast()) return; if (room && room.battle) { this.sendReplyBox(`
QuestionsBug Reports
`); } else { this.sendReplyBox( `Have a replay showcasing a bug on Pokémon Showdown?
` + `- Questions
` + `- Bug Reports (ask in Help before posting in the thread if you're unsure)` ); } }, '!avatars': true, avatars(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox(`You can by clicking on it in the menu in the upper right. Custom avatars are only obtainable by staff.`); }, avatarshelp: [ `/avatars - Explains how to change avatars.`, `!avatars - Show everyone that information. Requires: + % @ # & ~`, ], '!optionsbutton': true, optionbutton: 'optionsbutton', optionsbutton(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox(` (The Sound and Options buttons are at the top right, next to your username)`); }, '!soundbutton': true, soundsbutton: 'soundbutton', volumebutton: 'soundbutton', soundbutton(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox(` (The Sound and Options buttons are at the top right, next to your username)`); }, '!intro': true, introduction: 'intro', intro(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( `New to competitive Pokémon?
` + `- Beginner's Guide to Pokémon Showdown
` + `- An introduction to competitive Pokémon
` + `- What do 'OU', 'UU', etc mean?
` + `- What are the rules for each format? What is 'Sleep Clause'?` ); }, introhelp: [ `/intro - Provides an introduction to competitive Pok\u00e9mon.`, `!intro - Show everyone that information. Requires: + % @ # & ~`, ], '!smogintro': true, mentoring: 'smogintro', smogonintro: 'smogintro', smogintro(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( `Welcome to Smogon's official simulator! The Smogon Info / Intro Hub can help you get integrated into the community.
` + `- Useful Smogon Info
` + `- Tiering FAQ
` ); }, '!calc': true, calculator: 'calc', damagecalculator: 'calc', damagecalc: 'calc', randomscalc: 'calc', randbatscalc: 'calc', rcalc: 'calc', calc(target, room, user, connection, cmd) { if (!this.runBroadcast()) return; let isRandomBattle = (room && room.battle && room.battle.format === 'gen7randombattle'); if (['randomscalc', 'randbatscalc', 'rcalc'].includes(cmd) || isRandomBattle) { return this.sendReplyBox( `Random Battles damage calculator. (Courtesy of LegoFigure11 & Smoochyena)
` + `- Random Battles Damage Calcuator` ); } this.sendReplyBox( `Pokémon Showdown! damage calculator. (Courtesy of Honko)
` + `- Damage Calculator` ); }, calchelp: [ `/calc - Provides a link to a damage calculator`, `/rcalc - Provides a link to the random battles damage calculator`, `!calc - Shows everyone a link to a damage calculator. Requires: + % @ # & ~`, ], '!cap': true, capintro: 'cap', cap(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( `An introduction to the Create-A-Pokémon project:
` + `- CAP project website and description
` + `- What Pokémon have been made?
` + `- Talk about the metagame here
` + `- Sample SM CAP teams` ); }, caphelp: [ `/cap - Provides an introduction to the Create-A-Pok\u00e9mon project.`, `!cap - Show everyone that information. Requires: + % @ # & ~`, ], '!gennext': true, gennext(target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( "NEXT (also called Gen-NEXT) is a mod that makes changes to the game:
" + `- README: overview of NEXT
` + "Example replays:
" + `- Zergo vs Mr Weegle Snarf
` + `- NickMP vs Khalogie` ); }, '!formathelp': true, banlists: 'formathelp', tier: 'formathelp', tiers: 'formathelp', formats: 'formathelp', tiershelp: 'formathelp', formatshelp: 'formathelp', viewbanlist: 'formathelp', formathelp(target, room, user, connection, cmd) { if (!this.runBroadcast()) return; if (!target) { return this.sendReplyBox( `- Smogon Tiers
` + `- Tiering FAQ
` + `- The banlists for each tier
` + "
Type /formatshelp [format|section] to get details about an available format or group of formats." ); } const isOMSearch = (cmd === 'om' || cmd === 'othermetas'); let targetId = toId(target); if (targetId === 'ladder') targetId = 'search'; if (targetId === 'all') targetId = ''; let formatList; let format = Dex.getFormat(targetId); if (format.effectType === 'Format' || format.effectType === 'ValidatorRule' || format.effectType === 'Rule') formatList = [targetId]; if (!formatList) { formatList = Object.keys(Dex.formats); } // Filter formats and group by section let exactMatch = ''; let sections = {}; let totalMatches = 0; for (const mode of formatList) { let format = Dex.getFormat(mode); let sectionId = toId(format.section); let formatId = format.id; if (!/^gen\d+/.test(targetId)) formatId = formatId.replace(/^gen\d+/, ''); // skip generation prefix if it wasn't provided if (targetId && !format[targetId + 'Show'] && sectionId !== targetId && format.id === mode && !formatId.startsWith(targetId)) continue; if (isOMSearch && format.id.startsWith('gen') && ['ou', 'uu', 'ru', 'ubers', 'lc', 'customgame', 'doublescustomgame', 'gbusingles', 'gbudoubles'].includes(format.id.slice(4))) continue; if (isOMSearch && (format.id === 'gen5nu')) continue; totalMatches++; if (!sections[sectionId]) sections[sectionId] = {name: format.section, formats: []}; sections[sectionId].formats.push(format.id); if (format.id !== targetId) continue; exactMatch = sectionId; break; } if (!totalMatches) return this.errorReply("No matched formats found."); if (totalMatches === 1) { let rules = []; let rulesetHtml = ''; let format = Dex.getFormat(Object.values(sections)[0].formats[0]); if (format.effectType === 'ValidatorRule' || format.effectType === 'Rule' || format.effectType === 'Format') { if (format.ruleset && format.ruleset.length) rules.push("Ruleset - " + Chat.escapeHTML(format.ruleset.join(", "))); if (format.removedRules && format.removedRules.length) rules.push("Removed rules - " + Chat.escapeHTML(format.removedRules.join(", "))); if (format.banlist && format.banlist.length) rules.push("Bans - " + Chat.escapeHTML(format.banlist.join(", "))); if (format.unbanlist && format.unbanlist.length) rules.push("Unbans - " + Chat.escapeHTML(format.unbanlist.join(", "))); if (format.restrictedStones && format.restrictedStones.length) rules.push("Restricted Mega Stones - " + Chat.escapeHTML(format.restrictedStones.join(", "))); if (format.cannotMega && format.cannotMega.length) rules.push("Can't Mega Evolve non-natively - " + Chat.escapeHTML(format.cannotMega.join(", "))); if (format.restrictedAbilities && format.restrictedAbilities.length) rules.push("Restricted abilities - " + Chat.escapeHTML(format.restrictedAbilities.join(", "))); if (format.restrictedMoves && format.restrictedMoves.length) rules.push("Restricted moves - " + Chat.escapeHTML(format.restrictedMoves.join(", "))); if (rules.length > 0) { rulesetHtml = `
Banlist/Ruleset${rules.join("
")}
`; } else { rulesetHtml = "No ruleset found for " + format.name; } } let formatType = (format.gameType || "singles"); formatType = formatType.charAt(0).toUpperCase() + formatType.slice(1).toLowerCase(); if (!format.desc && !format.threads) { if (format.effectType === 'Format') { return this.sendReplyBox("No description found for this " + formatType + " " + format.section + " format." + "
" + rulesetHtml); } else { return this.sendReplyBox("No description found for this rule." + "
" + rulesetHtml); } } let descHtml = format.desc ? [format.desc] : []; if (format.threads) descHtml = descHtml.concat(format.threads); return this.sendReplyBox(descHtml.join("
") + "
" + rulesetHtml); } let tableStyle = `border:1px solid gray; border-collapse:collapse`; if (this.broadcasting) { tableStyle += `; display:inline-block; max-height:240px;" class="scrollable`; } // Build tables let buf = [``]; for (let sectionId in sections) { if (exactMatch && sectionId !== exactMatch) continue; buf.push(Chat.html``); for (const section of sections[sectionId].formats) { let format = Dex.getFormat(section); let nameHTML = Chat.escapeHTML(format.name); let desc = format.desc ? [format.desc] : []; if (format.threads) desc = desc.concat(format.threads); let descHTML = desc.length ? desc.join("
") : "—"; buf.push(``); } } buf.push(`
${sections[sectionId].name}
${nameHTML}${descHTML}
`); return this.sendReply("|raw|" + buf.join("") + ""); }, '!roomhelp': true, roomhelp(target, room, user) { if (!this.canBroadcast(false, '!htmlbox')) return; if (this.broadcastMessage && !this.can('declare', null, room)) return false; if (!this.runBroadcast(false, '!htmlbox')) return; this.sendReplyBox( `Room drivers (%) can use:
` + `- /warn OR /k username: warn a user and show the Pokémon Showdown rules
` + `- /mute OR /m username: 7 minute mute
` + `- /hourmute OR /hm username: 60 minute mute
` + `- /unmute username: unmute
` + `- /announce OR /wall message: make an announcement
` + `- /modlog username: search the moderator log of the room
` + `- /modnote note: add a moderator note that can be read through modlog
` + `
` + `Room moderators (@) can also use:
` + `- /roomban OR /rb username: ban user from the room
` + `- /roomunban username: unban user from the room
` + `- /roomvoice username: appoint a room voice
` + `- /roomdevoice username: remove a room voice
` + `- /staffintro intro: set the staff introduction that will be displayed for all staff joining the room
` + `- /roomsettings: change a variety of room settings, namely modchat
` + `
` + `Room owners (#) can also use:
` + `- /roomintro intro: set the room introduction that will be displayed for all users joining the room
` + `- /rules rules link: set the room rules link seen when using /rules
` + `- /roommod, /roomdriver username: appoint a room moderator/driver
` + `- /roomdemod, /roomdedriver username: remove a room moderator/driver
` + `- /roomdeauth username: remove all room auth from a user
` + `- /declare message: make a large blue declaration to the room
` + `- !htmlbox HTML code: broadcast a box of HTML code to the room
` + `- !showimage [url], [width], [height]: show an image to the room
` + `- /roomsettings: change a variety of room settings, including modchat, capsfilter, etc
` + `
` + `More detailed help can be found in the roomauth guide
` + `
` + `Tournament Help:
` + `- /tour create format, elimination: create a new single elimination tournament in the current room.
` + `- /tour create format, roundrobin: create a new round robin tournament in the current room.
` + `- /tour end: forcibly end the tournament in the current room
` + `- /tour start: start the tournament in the current room
` + `- /tour banlist [pokemon], [talent], [...]: ban moves, abilities, Pokémon or items from being used in a tournament (it must be created first)
` + `
` + `More detailed help can be found in the tournaments guide
` + `` ); }, '!restarthelp': true, restarthelp(target, room, user) { if (!Rooms.global.lockdown && !this.can('lockdown')) return false; if (!this.runBroadcast()) return; this.sendReplyBox( `The server is restarting. Things to know:
` + `- We wait a few minutes before restarting so people can finish up their battles
` + `- The restart itself will take around 0.6 seconds
` + `- Your ladder ranking and teams will not change
` + `- We are restarting to update Pokémon Showdown to a newer version` ); }, '!processes': true, processes(target, room, user) { if (!this.can('lockdown')) return false; let buf = `${process.pid} - Main
`; for (const worker of Sockets.workers.values()) { buf += `${worker.pid || worker.process.pid} - Sockets ${worker.id}
`; } /** @type {typeof import('../../lib/process-manager').processManagers} */ const processManagers = require(/** @type {any} */('../../.lib-dist/process-manager')).processManagers; for (const manager of processManagers) { for (const [i, process] of manager.processes.entries()) { buf += `${process.process.pid} - ${manager.basename} ${i} (load ${process.load})
`; } for (const [i, process] of manager.releasingProcesses.entries()) { buf += `${process.process.pid} - PENDING RELEASE ${manager.basename} ${i} (load ${process.load})
`; } } this.sendReplyBox(buf); }, '!rules': true, rule: 'rules', rules(target, room, user) { if (!target) { if (!this.runBroadcast()) return; this.sendReplyBox( `${room ? this.tr("Please follow the rules:") + '
' : ``}` + (room && room.rulesLink ? Chat.html`- ${this.tr `${room.title} room rules`}
` : ``) + `- ${this.tr("Global Rules")}` ); return; } if (!room) { return this.errorReply(`This is not a room you can set the rules of.`); } if (!this.can('editroom', null, room)) return; if (target.length > 150) { return this.errorReply(`Error: Room rules link is too long (must be under 150 characters). You can use a URL shortener to shorten the link.`); } target = target.trim(); if (target === 'delete' || target === 'remove') { if (!room.rulesLink) return this.errorReply(`This room does not have rules set to remove.`); delete room.rulesLink; this.privateModAction(`(${user.name} has removed the room rules link.)`); this.modlog('RULES', null, `removed room rules link`); } else { room.rulesLink = target; this.privateModAction(`(${user.name} changed the room rules link to: ${target})`); this.modlog('RULES', null, `changed link to: ${target}`); } if (room.chatRoomData) { room.chatRoomData.rulesLink = room.rulesLink; Rooms.global.writeChatRoomData(); } }, ruleshelp: [ `/rules - Show links to room rules and global rules.`, `!rules - Show everyone links to room rules and global rules. Requires: + % @ # & ~`, `/rules [url] - Change the room rules URL. Requires: # & ~`, `/rules remove - Removes a room rules URL. Requires: # & ~`, ], '!faq': true, faq(target, room, user) { if (!this.runBroadcast()) return; target = target.toLowerCase().trim(); let showAll = target === 'all'; if (showAll && this.broadcasting) { return this.sendReplyBox("You cannot broadcast all FAQs at once."); } let buffer = []; if (showAll || target === 'staff') { buffer.push(`Staff FAQ`); } if (showAll || target === 'autoconfirmed' || target === 'ac') { buffer.push(`A user is autoconfirmed when they have won at least one rated battle and have been registered for one week or longer.`); } if (showAll || target === 'coil') { buffer.push(`What is COIL?`); } if (showAll || target === 'ladder' || target === 'ladderhelp' || target === 'decay') { buffer.push(`How the ladder works`); } if (showAll || target === 'tiering' || target === 'tiers' || target === 'tier') { buffer.push(`Tiering FAQ`); } if (showAll || target === 'badge' || target === 'badges') { buffer.push(`Badge FAQ`); } if (showAll || target === 'rng') { buffer.push(`How Pokémon Showdown's RNG works`); } if (showAll || !buffer.length) { buffer.unshift(`Frequently Asked Questions`); } this.sendReplyBox(buffer.join(`
`)); }, faqhelp: [ `/faq [theme] - Provides a link to the FAQ. Add autoconfirmed, badges, coil, ladder, staff, or tiers for a link to these questions. Add all for all of them.`, `!faq [theme] - Shows everyone a link to the FAQ. Add autoconfirmed, badges, coil, ladder, staff, or tiers for a link to these questions. Add all for all of them. Requires: + % @ # & ~`, ], '!smogdex': true, analysis: 'smogdex', strategy: 'smogdex', smogdex(target, room, user) { if (!target) return this.parse('/help smogdex'); if (!this.runBroadcast()) return; let targets = target.split(','); let pokemon = Dex.getTemplate(targets[0]); let item = Dex.getItem(targets[0]); let move = Dex.getMove(targets[0]); let ability = Dex.getAbility(targets[0]); let format = Dex.getFormat(targets[0]); let atLeastOne = false; let generation = (targets[1] || 'sm').trim().toLowerCase(); let genNumber = 7; let extraFormat = Dex.getFormat(targets[2]); if (['7', 'gen7', 'seven', 'sm', 'sumo', 'usm', 'usum'].includes(generation)) { generation = 'sm'; } else if (['6', 'gen6', 'oras', 'six', 'xy'].includes(generation)) { generation = 'xy'; genNumber = 6; } else if (['5', 'b2w2', 'bw', 'bw2', 'five', 'gen5'].includes(generation)) { generation = 'bw'; genNumber = 5; } else if (['4', 'dp', 'dpp', 'four', 'gen4', 'hgss'].includes(generation)) { generation = 'dp'; genNumber = 4; } else if (['3', 'adv', 'frlg', 'gen3', 'rs', 'rse', 'three'].includes(generation)) { generation = 'rs'; genNumber = 3; } else if (['2', 'gen2', 'gs', 'gsc', 'two'].includes(generation)) { generation = 'gs'; genNumber = 2; } else if (['1', 'gen1', 'one', 'rb', 'rby', 'rgy'].includes(generation)) { generation = 'rb'; genNumber = 1; } else { generation = 'sm'; } // Pokemon if (pokemon.exists) { atLeastOne = true; if (genNumber < pokemon.gen) { return this.sendReplyBox(`${pokemon.name} did not exist in ${generation.toUpperCase()}!`); } if ((pokemon.battleOnly && pokemon.baseSpecies !== 'Greninja') || pokemon.baseSpecies === 'Keldeo' || pokemon.baseSpecies === 'Genesect') { pokemon = Dex.getTemplate(pokemon.baseSpecies); } let formatName = extraFormat.name; let formatId = extraFormat.id; if (formatName.startsWith('[Gen ')) { formatName = formatName.replace('[Gen ' + formatName[formatName.indexOf('[') + 5] + '] ', ''); formatId = toId(formatName); } if (formatId === 'battlespotdoubles') { formatId = 'battle_spot_doubles'; } else if (formatId === 'battlespottriples') { formatId = 'battle_spot_triples'; if (genNumber > 6) { return this.sendReplyBox(`Triples formats are not an available format in Pokémon generation ${generation.toUpperCase()}.`); } } else if (formatId === 'doublesou') { formatId = 'doubles'; } else if (formatId === 'balancedhackmons') { formatId = 'bh'; } else if (formatId === 'battlespotsingles') { formatId = 'battle_spot_singles'; } else if (formatId === 'ubers') { formatId = 'uber'; } else if (formatId.includes('vgc')) { formatId = 'vgc' + formatId.slice(-2); formatName = 'VGC20' + formatId.slice(-2); } else if (extraFormat.effectType !== 'Format') { formatName = formatId = ''; } const supportedLanguages = { spanish: 'es', french: 'fr', italian: 'it', german: 'de', portuguese: 'pt', }; let speciesid = pokemon.speciesid; // Special case for Meowstic-M if (speciesid === 'meowstic') speciesid = 'meowsticm'; if (['ou', 'uu'].includes(formatId) && generation === 'sm' && room && room.language in supportedLanguages) { // Limited support for translated analysis // Translated analysis do not support automatic redirects from a speciesid to the proper page this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } else if (['ou', 'uu'].includes(formatId) && generation === 'sm') { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University
` + `Other languages: Español, Français, Italiano, ` + `Deutsch, Português` ); } else { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } } // Item if (item.exists && genNumber > 1 && item.gen <= genNumber) { atLeastOne = true; this.sendReplyBox(`${generation.toUpperCase()} ${item.name} item analysis, brought to you by Smogon University`); } // Ability if (ability.exists && genNumber > 2 && ability.gen <= genNumber) { atLeastOne = true; this.sendReplyBox(`${generation.toUpperCase()} ${ability.name} ability analysis, brought to you by Smogon University`); } // Move if (move.exists && move.gen <= genNumber) { atLeastOne = true; this.sendReplyBox(`${generation.toUpperCase()} ${move.name} move analysis, brought to you by Smogon University`); } // Format if (format.id) { let formatName = format.name; let formatId = format.id; if (formatId === 'battlespotdoubles') { formatId = 'battle_spot_doubles'; } else if (formatId === 'battlespottriples') { formatId = 'battle_spot_triples'; if (genNumber > 6) { return this.sendReplyBox(`Triples formats are not an available format in Pokémon generation ${generation.toUpperCase()}.`); } } else if (formatId === 'doublesou') { formatId = 'doubles'; } else if (formatId === 'balancedhackmons') { formatId = 'bh'; } else if (formatId === 'battlespotsingles') { formatId = 'battle_spot_singles'; } else if (formatId === 'ubers') { formatId = 'uber'; } else if (formatId.includes('vgc')) { formatId = 'vgc' + formatId.slice(-2); formatName = 'VGC20' + formatId.slice(-2); } else if (format.effectType !== 'Format') { formatName = formatId = ''; } if (formatName) { atLeastOne = true; this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} format analysis, brought to you by Smogon University`); } } if (!atLeastOne) { return this.sendReplyBox(`Pokémon, item, move, ability, or format not found for generation ${generation.toUpperCase()}.`); } }, smogdexhelp: [ `/analysis [pokemon], [generation], [format] - Links to the Smogon University analysis for this Pok\u00e9mon in the given generation.`, `!analysis [pokemon], [generation], [format] - Shows everyone this link. Requires: + % @ # & ~`, ], '!veekun': true, veekun(target, broadcast, user) { if (!target) return this.parse('/help veekun'); if (!this.runBroadcast()) return; let baseLink = 'http://veekun.com/dex/'; let pokemon = Dex.getTemplate(target); let item = Dex.getItem(target); let move = Dex.getMove(target); let ability = Dex.getAbility(target); let nature = Dex.getNature(target); let atLeastOne = false; // Pokemon if (pokemon.exists) { atLeastOne = true; if (pokemon.isNonstandard) return this.errorReply(`${pokemon.species} is not a real Pok\u00e9mon.`); let baseSpecies = pokemon.baseSpecies || pokemon.species; let forme = pokemon.forme; // Showdown and Veekun have different names for various formes if (baseSpecies === 'Meowstic' && forme === 'F') forme = 'Female'; if (baseSpecies === 'Zygarde' && forme === '10%') forme = '10'; if (baseSpecies === 'Necrozma' && !Dex.getTemplate(baseSpecies + forme).battleOnly) forme = forme.substr(0, 4); if (baseSpecies === 'Pikachu' && Dex.getTemplate(baseSpecies + forme).gen === 7) forme += '-Cap'; if (forme.endsWith('Totem')) { if (baseSpecies === 'Raticate') forme = 'Totem-Alola'; if (baseSpecies === 'Marowak') forme = 'Totem'; if (baseSpecies === 'Mimikyu') forme += forme === 'Busted-Totem' ? '-Busted' : '-Disguised'; } let link = baseLink + 'pokemon/' + baseSpecies.toLowerCase(); if (forme) { if (baseSpecies === 'Arceus' || baseSpecies === 'Silvally') link += '/flavor'; link += '?form=' + forme.toLowerCase(); } this.sendReplyBox(`${pokemon.species} description by Veekun`); } // Item if (item.exists) { atLeastOne = true; if (item.isNonstandard) return this.errorReply(`${item.name} is not a real item.`); let link = baseLink + 'items/' + item.name.toLowerCase(); this.sendReplyBox(`${item.name} item description by Veekun`); } // Ability if (ability.exists) { atLeastOne = true; if (ability.isNonstandard) return this.errorReply(`${ability.name} is not a real ability.`); let link = baseLink + 'abilities/' + ability.name.toLowerCase(); this.sendReplyBox(`${ability.name} ability description by Veekun`); } // Move if (move.exists) { atLeastOne = true; if (move.isNonstandard) return this.errorReply(`${move.name} is not a real move.`); let link = baseLink + 'moves/' + move.name.toLowerCase(); this.sendReplyBox(`${move.name} move description by Veekun`); } // Nature if (nature.exists) { atLeastOne = true; let link = baseLink + 'natures/' + nature.name.toLowerCase(); this.sendReplyBox(`${nature.name} nature description by Veekun`); } if (!atLeastOne) { return this.sendReplyBox(`Pokémon, item, move, ability, or nature not found.`); } }, veekunhelp: [ `/veekun [pokemon] - Links to Veekun website for this pokemon/item/move/ability/nature.`, `!veekun [pokemon] - Shows everyone this link. Requires: + % @ # & ~`, ], '!register': true, register() { if (!this.runBroadcast()) return; this.sendReplyBox(`You will be prompted to register upon winning a rated battle. Alternatively, there is a register button in the menu in the upper right.`); }, /********************************************************* * Miscellaneous commands *********************************************************/ potd(target, room, user) { if (!this.can('potd')) return false; Config.potd = target; // TODO: support eval in new PM Rooms.PM.eval('Config.potd = \'' + toId(target) + '\''); if (target) { if (Rooms.lobby) Rooms.lobby.addRaw(`
The Pokémon of the Day is now ${target}!
This Pokemon will be guaranteed to show up in random battles.
`); this.modlog('POTD', null, target); } else { if (Rooms.lobby) Rooms.lobby.addRaw(`
The Pokémon of the Day was removed!
No pokemon will be guaranteed in random battles.
`); this.modlog('POTD', null, 'removed'); } }, '!dice': true, roll: 'dice', dice(target, room, user) { if (!target || target.match(/[^\d\sdHL+-]/i)) return this.parse('/help dice'); if (!this.runBroadcast(true)) return; // ~30 is widely regarded as the sample size required for sum to be a Gaussian distribution. // This also sets a computation time constraint for safety. let maxDice = 40; let diceQuantity = 1; let diceDataStart = target.indexOf('d'); if (diceDataStart >= 0) { if (diceDataStart) diceQuantity = Number(target.slice(0, diceDataStart)); target = target.slice(diceDataStart + 1); if (!Number.isInteger(diceQuantity) || diceQuantity <= 0 || diceQuantity > maxDice) return this.sendReply(`The amount of dice rolled should be a natural number up to ${maxDice}.`); } let offset = 0; let removeOutlier = 0; let modifierData = target.match(/[+-]/); if (modifierData) { switch (target.slice(modifierData.index).trim().toLowerCase()) { case '-l': removeOutlier = -1; break; case '-h': removeOutlier = +1; break; default: offset = Number(target.slice(modifierData.index)); if (isNaN(offset)) return this.parse('/help dice'); if (!Number.isSafeInteger(offset)) return this.errorReply(`The specified offset must be an integer up to ${Number.MAX_SAFE_INTEGER}.`); } if (removeOutlier && diceQuantity <= 1) return this.errorReply(`More than one dice should be rolled before removing outliers.`); target = target.slice(0, modifierData.index); } let diceFaces = 6; if (target.length) { diceFaces = Number(target); if (!Number.isSafeInteger(diceFaces) || diceFaces <= 0) { return this.errorReply(`Rolled dice must have a natural amount of faces up to ${Number.MAX_SAFE_INTEGER}.`); } } if (diceQuantity > 1) { // Make sure that we can deal with high rolls if (!Number.isSafeInteger(offset < 0 ? diceQuantity * diceFaces : diceQuantity * diceFaces + offset)) { return this.errorReply(`The maximum sum of rolled dice must be lower or equal than ${Number.MAX_SAFE_INTEGER}.`); } } let maxRoll = 0; let minRoll = Number.MAX_SAFE_INTEGER; let trackRolls = diceQuantity * (('' + diceFaces).length + 1) <= 60; let rolls = []; let rollSum = 0; for (let i = 0; i < diceQuantity; ++i) { let curRoll = Math.floor(Math.random() * diceFaces) + 1; rollSum += curRoll; if (curRoll > maxRoll) maxRoll = curRoll; if (curRoll < minRoll) minRoll = curRoll; if (trackRolls) rolls.push(curRoll); } // Apply modifiers if (removeOutlier > 0) { rollSum -= maxRoll; } else if (removeOutlier < 0) { rollSum -= minRoll; } if (offset) rollSum += offset; // Reply with relevant information let offsetFragment = ""; if (offset) offsetFragment += (offset > 0 ? " + " + offset : offset); if (diceQuantity === 1) return this.sendReplyBox(`Rolling (1 to ${diceFaces})${offsetFragment}: ${rollSum}`); const outlierFragment = removeOutlier ? ` except ${removeOutlier > 0 ? "highest" : "lowest"}` : ``; const rollsFragment = trackRolls ? ": " + rolls.join(", ") : ""; return this.sendReplyBox( `${diceQuantity} rolls (1 to ${diceFaces})${rollsFragment}
` + `Sum${offsetFragment}${outlierFragment}: ${rollSum}` ); }, dicehelp: [ `/dice [max number] - Randomly picks a number between 1 and the number you choose.`, `/dice [number of dice]d[number of sides] - Simulates rolling a number of dice, e.g., /dice 2d4 simulates rolling two 4-sided dice.`, `/dice [number of dice]d[number of sides][+/-][offset] - Simulates rolling a number of dice and adding an offset to the sum, e.g., /dice 2d6+10: two standard dice are rolled; the result lies between 12 and 22.`, `/dice [number of dice]d[number of sides]-[H/L] - Simulates rolling a number of dice with removal of extreme values, e.g., /dice 3d8-L: rolls three 8-sided dice; the result ignores the lowest value.`, ], '!pickrandom': true, pr: 'pickrandom', pick: 'pickrandom', pickrandom(target, room, user) { if (!target) return false; if (!target.includes(',')) return this.parse('/help pick'); if (!this.runBroadcast(true)) return false; if (this.broadcasting) { [, target] = Chat.splitFirst(this.message, ' '); } let options = target.split(','); const pickedOption = options[Math.floor(Math.random() * options.length)].trim(); return this.sendReplyBox(Chat.html`We randomly picked: ${pickedOption}`); }, pickrandomhelp: [`/pick [option], [option], ... - Randomly selects an item from a list containing 2 or more elements.`], showimage(target, room, user) { if (!target) return this.parse('/help showimage'); if (!this.can('declare', null, room)) return false; if (!this.runBroadcast()) return; if (this.room.isPersonal && !this.user.can('announce')) { return this.errorReply(`Images are not allowed in personal rooms.`); } let targets = target.split(','); if (targets.length !== 1 && targets.length !== 3) { return this.parse('/help showimage'); } let image = targets[0].trim(); if (!image) return this.errorReply(`No image URL was provided!`); image = this.canEmbedURI(image); if (!image) return false; if (targets.length === 3) { let width = targets[1].trim(); if (!width) return this.errorReply(`No width for the image was provided!`); if (!isNaN(width)) width += `px`; let height = targets[2].trim(); if (!height) return this.errorReply(`No height for the image was provided!`); if (!isNaN(height)) height += `px`; let unitRegex = /^\d+(?:p[xtc]|%|[ecm]m|ex|in)$/; if (!unitRegex.test(width)) { return this.errorReply(`"${width}" is not a valid width value!`); } if (!unitRegex.test(height)) { return this.errorReply(`"${height}" is not a valid height value!`); } return this.sendReply(Chat.html`|raw|`); } Chat.fitImage(image).then(([width, height]) => { this.sendReply(Chat.html`|raw|`); room.update(); }); }, showimagehelp: [`/showimage [url], [width], [height] - Show an image. Any CSS units may be used for the width or height (default: px). If width and height aren't provided, automatically scale the image to fit in chat. Requires: # & ~`], htmlbox(target, room, user) { if (!target) return this.parse('/help htmlbox'); target = this.canHTML(target); if (!target) return; target = Chat.collapseLineBreaksHTML(target); if (!this.canBroadcast(true, '!htmlbox')) return; if (this.broadcastMessage && !this.can('declare', null, room)) return false; if (!this.runBroadcast(true, '!htmlbox')) return; this.sendReplyBox(target); }, htmlboxhelp: [ `/htmlbox [message] - Displays a message, parsing HTML code contained.`, `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: * # & ~`, ], addhtmlbox(target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; target = this.canHTML(target); if (!target) return; if (!this.can('addhtml', null, room)) return; target = Chat.collapseLineBreaksHTML(target); if (!user.can('addhtml')) { target += Chat.html`
[${user.name}]
`; } this.addBox(target); }, addhtmlboxhelp: [ `/addhtmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: * & ~`, ], addrankhtmlbox(target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; let [rank, html] = this.splitOne(target); if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); html = this.canHTML(html); if (!html) return; if (!this.can('addhtml', null, room)) return; html = Chat.collapseLineBreaksHTML(html); if (!user.can('addhtml')) { html += Chat.html`
[${user.name}]
`; } this.room.sendRankedUsers(`|html|
${html}
`, rank); }, addrankhtmlboxhelp: [ `/addrankhtmlbox [rank], [message] - Shows everyone with the specified rank or higher a message, parsing HTML code contained. Requires: * & ~`, ], changeuhtml: 'adduhtml', adduhtml(target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; let [name, html] = this.splitOne(target); name = toId(name); html = this.canHTML(html); if (!html) return; if (!this.can('addhtml', null, room)) return; html = Chat.collapseLineBreaksHTML(html); if (!user.can('addhtml')) { html += Chat.html`
[${user.name}]
`; } html = `|uhtml${(cmd === 'changeuhtml' ? 'change' : '')}|${name}|${html}`; this.add(html); }, adduhtmlhelp: [ `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained. Requires: * & ~`, ], changeuhtmlhelp: [ `/changeuhtml [name], [message] - Changes the message previously shown with /adduhtml [name]. Requires: * & ~`, ], changerankuhtml: 'addrankuhtml', addrankuhtml(target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; let [rank, uhtml] = this.splitOne(target); if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); let [name, html] = this.splitOne(uhtml); name = toId(name); html = this.canHTML(html); if (!html) return; if (!this.can('addhtml', null, room)) return; html = Chat.collapseLineBreaksHTML(html); if (!user.can('addhtml')) { html += Chat.html`
[${user.name}]
`; } html = `|uhtml${(cmd === 'changerankuhtml' ? 'change' : '')}|${name}|${html}`; this.room.sendRankedUsers(html, rank); }, addrankuhtmlhelp: [ `/addrankuhtml [rank], [name], [message] - Shows everyone with the specified rank or higher a message that can change, parsing HTML code contained. Requires: * & ~`, ], changerankuhtmlhelp: [ `/changerankuhtml [rank], [name], [message] - Changes the message previously shown with /addrankuhtml [rank], [name]. Requires: * & ~`, ], }; /** @type {PageTable} */ const pages = { punishments(query, user, connection) { this.title = 'Punishments'; let buf = ""; this.extractRoom(); if (!user.named) return Rooms.RETRY_AFTER_LOGIN; buf += `

List of active punishments:

`; if (!this.can('mute', null, this.room)) return; if (!this.room.chatRoomData) { return buf + `
This page is unavailable in temporary rooms / non-existent rooms.
`; } const store = new Map(); const possessive = (word) => { const suffix = word.endsWith('s') ? `'` : `'s`; return `${word}${suffix}`; }; if (Punishments.roomUserids.get(this.room.id)) { for (let [key, value] of Punishments.roomUserids.get(this.room.id)) { if (!store.has(value)) store.set(value, [new Set([value.id]), new Set()]); store.get(value)[0].add(key); } } if (Punishments.roomIps.get(this.room.id)) { for (let [key, value] of Punishments.roomIps.get(this.room.id)) { if (!store.has(value)) store.set(value, [new Set([value.id]), new Set()]); store.get(value)[1].add(key); } } for (const [punishment, data] of store) { let [punishType, id, expireTime, reason] = punishment; let alts = [...data[0]].filter(user => user !== id); let ip = [...data[1]]; let expiresIn = new Date(expireTime).getTime() - Date.now(); let expireString = Chat.toDurationString(expiresIn, {precision: 1}); let punishDesc = ""; if (reason) punishDesc += ` Reason: ${reason}.`; if (alts.length) punishDesc += ` Alts: ${alts.join(", ")}.`; if (user.can('ban') && ip.length) { punishDesc += ` IPs: ${ip.join(", ")}.`; } buf += `

- ${possessive(id)} ${punishType.toLowerCase()} expires in ${expireString}.${punishDesc}

`; } if (this.room.muteQueue) { for (const entry of this.room.muteQueue) { let expiresIn = new Date(entry.time).getTime() - Date.now(); if (expiresIn < 0) continue; let expireString = Chat.toDurationString(expiresIn, {precision: 1}); buf += `

- ${possessive(entry.userid)} mute expires in ${expireString}.

`; } } buf += `
`; return buf; }, }; exports.pages = pages; exports.commands = commands; process.nextTick(() => { Dex.includeData(); Chat.multiLinePattern.register( '/htmlbox', '!htmlbox', '/addhtmlbox', '/addrankhtmlbox', '/adduthml', '/changeuhtml', '/addrankuhtmlbox', '/changerankuhtmlbox' ); });