/** * 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'; const path = require('path'); exports.commands = { '!whois': true, ip: 'whois', rooms: 'whois', alt: 'whois', alts: 'whois', whoare: 'whois', whois: function (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} `; 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 = ""; targetUser.inRooms.forEach(roomid => { if (roomid === 'global') return; 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 === '~') return; 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); } buf += '
'; if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) { let alts = targetUser.getAltUsers(true); let prevNames = Object.keys(targetUser.prevNames).join(", "); if (prevNames) buf += Chat.html`
Previous names: ${prevNames}`; for (let j = 0; j < alts.length; ++j) { let targetAlt = alts[j]; if (!targetAlt.named && !targetAlt.connected) continue; if (targetAlt.group === '~' && user.group !== '~') continue; buf += Chat.html`
Alt: ${targetAlt.name}`; if (!targetAlt.connected) buf += ` (offline)`; prevNames = Object.keys(targetAlt.prevNames).join(", "); if (prevNames) buf += `
Previous names: ${prevNames}`; } 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 += ` (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 += ` (reason: ${punishment[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 => { if (Punishments.sharedIps.has(ip)) { let sharedStr = 'shared'; if (Punishments.sharedIps.get(ip)) { sharedStr += `: ${Punishments.sharedIps.get(ip)}`; } return ip + ` (${sharedStr})`; } return ip; }); buf += `
IP${Chat.plural(ips)}: ${ips.join(", ")}`; if (user.group !== ' ' && targetUser.latestHost) { buf += Chat.html`
Host: ${targetUser.latestHost}`; } } if ((user === targetUser || user.can('alts', targetUser)) && hiddenrooms) { buf += `
Hidden rooms: ${hiddenrooms}`; } if ((user === targetUser || user.can('makeroom')) && privaterooms) { buf += `
Private rooms: ${privaterooms}`; } if (user.can('alts', targetUser) || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.userid in room.users)) { 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: function (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 = {BAN: "BANNED", LOCK: "LOCKED", NAMELOCK: "NAMELOCKED"}[punishType] || punishType; buf += `${punishName}: ${punishUserid}`; let expiresIn = Punishments.checkLockExpiration(userid); if (expiresIn) buf += expiresIn; if (reason) buf += ` (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); 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 !== 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(', '); atLeastOne = true; } if (!atLeastOne) { buf += `This username has no punishments associated with it.`; } this.sendReplyBox(buf); }, '!host': true, host: function (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: function (target, room, user, connection, cmd) { if (!target.trim()) return this.parse('/help ipsearch'); if (!this.can('rangeban')) return; let results = []; let isAll = (cmd === 'ipsearchall'); if (/[a-z]/.test(target)) { // host this.sendReply("Users with host " + target + ":"); Users.users.forEach(curUser => { if (results.length > 100 && !isAll) return; if (!curUser.latestHost || !curUser.latestHost.endsWith(target)) return; 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 (target.slice(-1) === '*') { // IP range this.sendReply("Users in IP range " + target + ":"); target = target.slice(0, -1); Users.users.forEach(curUser => { if (results.length > 100 && !isAll) return; if (!curUser.latestIp.startsWith(target)) return; 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 " + target + ":"); Users.users.forEach(curUser => { if (curUser.latestIp === target) { results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name); } }); } if (!results.length) { if (!target.includes('.')) return this.errorReply("'" + target + "' is not a valid IP or host."); return this.sendReply("No results found."); } return this.sendReply(results.join('; ')); }, ipsearchhelp: ["/ipsearch [ip|range|host] - Find all users with specified IP, IP range, or host. Requires: & ~"], checkchallenges: function (target, room, user) { if (!this.can('ban', null, room)) return false; if (!this.runBroadcast()) 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 = []; if (user1.challengeTo && Users.get(user1.challengeTo.to) === user2) { challenges.push(Chat.html`${user1.name} is challenging ${user2.name} in ${Dex.getFormat(user1.challengeTo.format).name}.`); } if (user2.challengeTo && Users.get(user2.challengeTo.to) === user1) { challenges.push(Chat.html`${user2.name} is challenging ${user1.name} in ${Dex.getFormat(user2.challengeTo.format).name}.`); } 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: function (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: function (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(targetId); if (!isNaN(targetNum) && '' + targetNum === target) { for (let p in Dex.data.Pokedex) { let pokemon = Dex.getTemplate(p); if (pokemon.num === targetNum) { target = pokemon.species; targetId = pokemon.id; break; } } } let mod = Dex; if (sep[1] && toId(sep[1]) in Dex.dexes) { mod = Dex.mod(toId(sep[1])); } else if (sep[1] && Dex.getFormat(sep[1]).mod) { mod = Dex.mod(Dex.getFormat(sep[1]).mod); } let newTargets = mod.dataSearch(target); let showDetails = (cmd === 'dt' || cmd === 'details'); if (newTargets && newTargets.length) { for (let i = 0; i < newTargets.length; ++i) { if (newTargets[i].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`; } switch (newTargets[i].searchType) { case 'nature': let nature = Dex.getNature(newTargets[i].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 template = mod.getTemplate(newTargets[i].name); buffer += `|raw|${Chat.getDataPokemonHTML(template, mod.gen)}\n`; break; case 'item': let item = mod.getItem(newTargets[i].name); buffer += `|raw|${Chat.getDataItemHTML(item)}\n`; break; case 'move': let move = mod.getMove(newTargets[i].name); buffer += `|raw|${Chat.getDataMoveHTML(move)}\n`; break; case 'ability': let ability = mod.getAbility(newTargets[i].name); buffer += `|raw|${Chat.getDataAbilityHTML(ability)}\n`; break; default: throw new Error(`Unrecognized searchType`); } } } else { 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?)`); } if (showDetails) { let details; if (newTargets[0].searchType === 'pokemon') { let pokemon = mod.getTemplate(newTargets[0].name); 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 = []; pokemon.evos.forEach(evo => { evo = mod.getTemplate(evo); if (evo.gen <= mod.gen) { evos.push(evo.name + " (" + evo.evoLevel + ")"); } }); if (!evos.length) { details['Does Not Evolve'] = ""; } else { details["Evolution"] = evos.join(", "); } } else if (newTargets[0].searchType === 'move') { let move = mod.getMove(newTargets[0].name); 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 (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'] = ''; } } else if (newTargets[0].searchType === 'item') { let item = mod.getItem(newTargets[0].name); 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; } } else { details = {}; } buffer += '|raw|' + Object.keys(details).map(detail => { if (details[detail] === '') return detail; return '' + detail + ': ' + details[detail]; }).join(" |  ") + ''; } this.sendReply(buffer); }, datahelp: ["/data [pokemon/item/move/ability] - Get details on this pokemon/item/move/ability/nature.", "/data [pokemon/item/move/ability], Gen [generation number/format name] - Get details on this pokemon/item/move/ability/nature for that generation/format.", "!data [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ * # & ~"], '!details': true, dt: 'details', details: function (target) { if (!target) return this.parse('/help details'); this.run('data'); }, detailshelp: ["/details [pokemon/item/move/ability] - Get additional details on this pokemon/item/move/ability/nature.", "/details [pokemon/item/move/ability], Gen [generation number/format name] - Get details on this pokemon/item/move/ability/nature for that generation/format.", "!details [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ * # & ~"], '!weakness': true, weaknesses: 'weakness', weak: 'weakness', resist: 'weakness', weakness: function (target, room, user) { if (!target) return this.parse('/help weakness'); if (!this.runBroadcast()) return; target = target.trim(); let targets = target.split(/ ?[,/ ] ?/); let pokemon = Dex.getTemplate(target); let type1 = Dex.getType(targets[0]); let type2 = Dex.getType(targets[1]); let type3 = Dex.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."); } pokemon = {types: types}; target = types.join("/"); } let weaknesses = []; let resistances = []; let immunities = []; for (let type in Dex.data.TypeChart) { let notImmune = Dex.getImmunity(type, pokemon); if (notImmune) { let typeMod = Dex.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: function (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':1, 'getMove':1, 'getTemplate':1}; let sourceMethods = {'getType':1, 'getMove':1}; let targetMethods = {'getType':1, 'getTemplate':1}; let source, defender, foundData, atkName, defName; for (let i = 0; i < 2; ++i) { let method; for (method in searchMethods) { foundData = Dex[method](targets[i]); if (foundData.exists) break; } if (!foundData.exists) return this.parse('/help effectiveness'); if (!source && method in sourceMethods) { if (foundData.type) { source = foundData; atkName = foundData.name; } else { source = foundData.id; atkName = foundData.id; } searchMethods = targetMethods; } else if (!defender && method in targetMethods) { 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 (let i = 0; i < defender.types.length; i++) { let baseMod = Dex.getEffectiveness(source, defender.types[i]); let moveMod = source.onEffectiveness && source.onEffectiveness.call(Dex, baseMod, defender.types[i], 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: function (target, room, user) { if (!this.runBroadcast()) return; if (!target) return this.parse("/help coverage"); let targets = target.split(/[,+]/); let sources = []; let dispTable = false; let bestCoverage = {}; let hasThousandArrows = false; for (let type in Dex.data.TypeChart) { // This command uses -5 to designate immunity bestCoverage[type] = -5; } for (let i = 0; i < targets.length; i++) { let move = targets[i].trim(); move = move.charAt(0).toUpperCase() + move.slice(1).toLowerCase(); if (move === 'Table' || move === 'All') { if (this.broadcasting) return this.sendReplyBox("The full table cannot be broadcast."); dispTable = true; continue; } let eff; if (move in Dex.data.TypeChart) { sources.push(move); for (let type in bestCoverage) { if (!Dex.getImmunity(move, type) && !move.ignoreImmunity) continue; eff = Dex.getEffectiveness(move, type); if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } move = Dex.getMove(move); if (move.exists) { 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 (!Dex.getImmunity(move.type, type) && !move.ignoreImmunity) continue; let baseMod = Dex.getEffectiveness(move, type); let moveMod = move.onEffectiveness && move.onEffectiveness.call(Dex, baseMod, type, move); eff = typeof moveMod === 'number' ? moveMod : baseMod; } if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } return this.errorReply("No type or move '" + targets[i] + "' found."); } 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 Dex.data.TypeChart) { icon[type] = ''; // row of icons at top buffer += ''; } buffer += ''; for (let type1 in Dex.data.TypeChart) { // assembles the rest of the rows buffer += ''; for (let type2 in Dex.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: function (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 = false; let pokemon; let useStat = ''; let level = 100; let calcHP = false; let nature = 1.0; let iv = 31; let ev = 252; let statValue = -1; let modifier = 0; let positiveMod = true; for (let i = 0; i < targets.length; i++) { let lowercase = targets[i].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(targets[i].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(targets[i]); ivSet = true; if (isNaN(iv)) { return this.sendReplyBox('Invalid value for IVs: ' + Chat.escapeHTML(targets[i])); } 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(targets[i]); evSet = true; if (isNaN(ev)) { return this.sendReplyBox('Invalid value for EVs: ' + Chat.escapeHTML(targets[i])); } if (ev > 255 || ev < 0) { return this.sendReplyBox('The amount of EVs should be between 0 and 255.'); } if (!natureSet) { if (targets[i].includes('+')) { nature = 1.1; natureSet = true; } else if (targets[i].includes('-')) { nature = 0.9; natureSet = true; } } continue; } } if (!modSet) { if (targets[i] === 'scarf' || targets[i] === 'specs' || targets[i] === 'band') { modifier = 1; modSet = true; } else if (targets[i].charAt(0) === '+') { modifier = parseInt(targets[i].charAt(1)); modSet = true; } else if (targets[i].charAt(0) === '-') { positiveMod = false; modifier = parseInt(targets[i].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 (!pokemon) { let testPoke = Dex.getTemplate(targets[i]); if (testPoke.baseStats) { pokemon = testPoke.baseStats; baseSet = true; continue; } } let tempStat = parseInt(targets[i]); if (!isNaN(tempStat) && !baseSet && tempStat > 0 && tempStat < 256) { statValue = tempStat; baseSet = true; } } if (pokemon) { if (useStat) { statValue = pokemon[useStat]; } else { return this.sendReplyBox('No stat found.'); } } if (statValue < 0) { return this.sendReplyBox('No valid value for base stat found.'); } let output; if (calcHP) { output = (((iv + (2 * statValue) + (ev / 4) + 100) * level) / 100) + 10; } else { output = Math.floor(nature * Math.floor((((iv + (2 * statValue) + (ev / 4)) * level) / 100) + 5)); if (positiveMod) { output *= (2 + modifier) / 2; } else { output *= 2 / (2 + modifier); } } return this.sendReplyBox('Base ' + statValue + (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."], /********************************************************* * Informational commands *********************************************************/ '!uptime': true, uptime: function (target, room, user) { 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: function (target, room, user) { if (!this.runBroadcast()) return; let servertime = new Date(); this.sendReplyBox(`Server time: ${servertime.toLocaleString()}`); }, '!groups': true, groups: function (target, room, user) { if (!this.runBroadcast()) return; const showRoom = (target !== 'global'); const showGlobal = (target !== 'room' && target !== 'rooms'); this.sendReplyBox( (showRoom ? `Room ranks
` + `+ Voice - They can use ! commands like !groups, and talk during moderated chat
` + `% 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
` : ``) + (showRoom && showGlobal ? `
` : ``) + (showGlobal ? `Global ranks
` + `+ Global Voice - They can use ! commands like !groups, and talk during moderated chat
` + `% 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` : ``) ); }, 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: function (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: function (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: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Pokémon Showdown Staff List"); }, '!forums': true, forums: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Pokémon Showdown Forums"); }, '!suggestions': true, suggestions: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Make a suggestion for Pokémon Showdown"); }, '!bugs': true, bugreport: 'bugs', bugs: function (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: function (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: function (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: function (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: function (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: function (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', calc: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( "Pokémon Showdown! damage calculator. (Courtesy of Honko)
" + "- Damage Calculator" ); }, calchelp: ["/calc - Provides a link to a damage calculator", "!calc - Shows everyone a link to a damage calculator. Requires: + % @ * # & ~"], '!cap': true, capintro: 'cap', cap: function (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: function (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', formathelp: function (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') formatList = [targetId]; if (!formatList) { formatList = Object.keys(Dex.formats); } // Filter formats and group by section let exactMatch = ''; let sections = {}; let totalMatches = 0; for (let i = 0; i < formatList.length; i++) { let format = Dex.getFormat(formatList[i]); 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 === formatList[i] && !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 " + (target ? "matched " : "") + "formats found."); if (totalMatches === 1) { let format = Dex.getFormat(Object.values(sections)[0].formats[0]); let formatType = (format.gameType || "singles"); formatType = formatType.charAt(0).toUpperCase() + formatType.slice(1).toLowerCase(); if (!format.desc) return this.sendReplyBox("No description found for this " + formatType + " " + format.section + " format."); return this.sendReplyBox(format.desc.join("
")); } 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 (let i = 0; i < sections[sectionId].formats.length; i++) { let format = Dex.getFormat(sections[sectionId].formats[i]); let nameHTML = Chat.escapeHTML(format.name); let descHTML = format.desc ? format.desc.join("
") : "—"; buf.push(``); } } buf.push(`
${sections[sectionId].name}
${nameHTML}${descHTML}
`); return this.sendReply("|raw|" + buf.join("") + ""); }, '!roomhelp': true, roomhelp: function (target, room, user) { if (!this.canBroadcast('!htmlbox')) return; if (this.broadcastMessage && !this.can('declare', null, room)) return false; if (!this.runBroadcast('!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: adds a moderator note that can be read through modlog
" + "
" + "Room moderators (@) can also use:
" + "- /roomban OR /rb username: bans user from the room
" + "- /roomunban username: unbans user from the room
" + "- /roomvoice username: appoint a room voice
" + "- /roomdevoice username: remove a room voice
" + "- /staffintro intro: sets 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: sets 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: broadcasts a box of HTML code to the room
" + "- !showimage [url], [width], [height]: shows 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: Creates a new single elimination tournament in the current room.
" + "- /tour create format, roundrobin: Creates a new round robin tournament in the current room.
" + "- /tour end: Forcibly ends the tournament in the current room
" + "- /tour start: Starts the tournament in the current room
" + "- /tour banlist [pokemon], [talent], [...]: Bans 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: function (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: function (target, room, user) { if (!this.can('lockdown')) return false; let buf = `${process.pid} - Main
`; Sockets.workers.forEach(worker => { buf += `${worker.pid || worker.process.pid} - Sockets ${worker.id}
`; }); const ProcessManager = require('../process-manager'); ProcessManager.cache.forEach((execFile, processManager) => { let i = 0; processManager.processes.forEach(process => { buf += `${process.process.pid} - ${path.basename(execFile)} ${i++}
`; }); }); this.sendReplyBox(buf); }, '!rules': true, rule: 'rules', rules: function (target, room, user) { if (!target) { if (!this.runBroadcast()) return; this.sendReplyBox("Please follow the rules:
" + (room && room.rulesLink ? "- " + Chat.escapeHTML(room.title) + " room rules
" : "") + "- " + (room && room.rulesLink ? "Global rules" : "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 > 100) { return this.errorReply("Error: Room rules link is too long (must be under 100 characters). You can use a URL shortener to shorten the link."); } if (target === 'delete' || target === 'remove') { if (!room.rulesLink) return this.errorReply("This room does not have rules set to remove."); delete room.rulesLink; this.privateModCommand(`(${user.name} has removed the room rules link.)`); } else { room.rulesLink = target.trim(); this.privateModCommand(`(${user.name} changed the room rules 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: function (target, room, user) { if (!this.runBroadcast()) return; target = target.toLowerCase(); 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 === 'tiering' || target === 'tiers' || target === 'tier') { buffer.push("Tiering FAQ"); } if (showAll || target === 'badge' || target === 'badges') { buffer.push("Badge FAQ"); } if (showAll || !buffer.length) { buffer.unshift("Frequently Asked Questions"); } this.sendReplyBox(buffer.join("
")); }, faqhelp: ["/faq [theme] - Provides a link to the FAQ. Add deviation, doubles, randomcap, restart, or staff for a link to these questions. Add all for all of them.", "!faq [theme] - Shows everyone a link to the FAQ. Add deviation, doubles, randomcap, restart, or staff for a link to these questions. Add all for all of them. Requires: + % @ * # & ~"], '!smogdex': true, analysis: 'smogdex', strategy: 'smogdex', smogdex: function (target, room, user) { 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 (generation === 'sm' || generation === 'sumo' || generation === '7' || generation === 'seven') { generation = 'sm'; } else if (generation === 'xy' || generation === 'oras' || generation === '6' || generation === 'six') { generation = 'xy'; genNumber = 6; } else if (generation === 'bw' || generation === 'bw2' || generation === '5' || generation === 'five') { generation = 'bw'; genNumber = 5; } else if (generation === 'dp' || generation === 'dpp' || generation === '4' || generation === 'four') { generation = 'dp'; genNumber = 4; } else if (generation === 'adv' || generation === 'rse' || generation === 'rs' || generation === '3' || generation === 'three') { generation = 'rs'; genNumber = 3; } else if (generation === 'gsc' || generation === 'gs' || generation === '2' || generation === 'two') { generation = 'gs'; genNumber = 2; } else if (generation === 'rby' || generation === 'rb' || generation === '1' || generation === 'one') { 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.tier === 'CAP') { generation = 'cap'; this.errorReply("CAP is not currently supported by Smogon Strategic Pokedex."); } 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 (formatId === 'battlespotdoubles') { formatId = 'battle_spot_doubles'; } else if (formatId === 'battlespottriples') { formatId = 'battle_spot_triples'; if (genNumber > 6) { 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 = ''; } let speciesid = pokemon.speciesid; // Special case for Meowstic-M if (speciesid === 'meowstic') speciesid = 'meowsticm'; if (pokemon.tier === 'CAP') { this.sendReplyBox("" + generation.toUpperCase() + " " + Chat.escapeHTML(formatName) + " " + pokemon.name + " analysis preview, brought to you by Smogon University CAP Project"); } 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) { 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: function (target, broadcast, user) { if (!this.runBroadcast()) return; if (!target) return this.parse('/help veekun'); 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 naming for this gender difference forme of Meowstic. if (baseSpecies === 'Meowstic' && forme === 'F') { forme = 'Female'; } let link = baseLink + 'pokemon/' + baseSpecies.toLowerCase(); if (forme) { link += '?form=' + forme.toLowerCase(); } this.sendReplyBox("" + pokemon.species + " description by Veekun"); } // Item if (item.exists) { atLeastOne = true; 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.sendReply(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: function () { 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: function (target, room, user) { if (!this.can('potd')) return false; Config.potd = target; Rooms.SimulatorProcess.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.logModCommand("The Pok\u00e9mon of the Day was changed to " + target + " by " + user.name + "."); } else { if (Rooms.lobby) Rooms.lobby.addRaw("
The Pokémon of the Day was removed!
No pokemon will be guaranteed in random battles.
"); this.logModCommand("The Pok\u00e9mon of the Day was removed by " + user.name + "."); } }, '!dice': true, roll: 'dice', dice: function (target, room, user) { if (!target || target.match(/[^\d\sdHL+-]/i)) return this.parse('/help dice'); if (!this.runBroadcast()) 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("Roll (1 - " + diceFaces + ")" + offsetFragment + ": " + rollSum); let sumFragment = "
Sum" + offsetFragment + (removeOutlier ? " except " + (removeOutlier > 0 ? "highest" : "lowest") : ""); return this.sendReplyBox("" + diceQuantity + " rolls (1 - " + diceFaces + ")" + (trackRolls ? ": " + rolls.join(", ") : "") + sumFragment + ": " + 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: function (target, room, user) { let options = target.split(','); if (options.length < 2) return this.parse('/help pick'); if (!this.runBroadcast()) return false; const pickedOption = options[Math.floor(Math.random() * options.length)]; return this.sendReplyBox('We randomly picked: ' + Chat.escapeHTML(pickedOption).trim()); }, pickrandomhelp: ["/pick [option], [option], ... - Randomly selects an item from a list containing 2 or more elements."], showimage: function (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 !== 3) { // Width and height are required because most browsers insert the // element before width and height are known, and when the // image is loaded, this changes the height of the chat area, which // messes up autoscrolling. 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; 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!'); } this.sendReply('|raw|'); }, showimagehelp: ["/showimage [url], [width], [height] - Show an image. " + "Any CSS units may be used for the width or height (default: px)." + "Requires: # & ~"], htmlbox: function (target, room, user, connection, cmd, message) { if (!target) return this.parse('/help htmlbox'); target = this.canHTML(target); if (!target) return; if (!this.canBroadcast('!htmlbox')) return; if (this.broadcastMessage && !this.can('declare', null, room)) return false; if (!this.runBroadcast('!htmlbox')) return; this.sendReplyBox(target); }, addhtmlbox: function (target, room, user, connection, cmd, message) { if (!target) return this.parse('/help htmlbox'); if (!this.canTalk()) return; target = this.canHTML(target); if (!target) return; if (!this.can('addhtml', null, room)) return; if (!user.can('addhtml')) { target += '
[' + Chat.escapeHTML(user.name) + ']
'; } this.addBox(target); }, htmlboxhelp: [ "/htmlbox [message] - Displays a message, parsing HTML code contained.", "!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #", ], }; process.nextTick(() => { Dex.includeData(); });