/** * 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 = { ip: 'whois', rooms: 'whois', alt: 'whois', alts: 'whois', whoare: 'whois', whois: function (target, room, user, connection, cmd) { if (room.id === 'staff' && !this.runBroadcast()) return; let targetUser = this.targetUserOrSelf(target, user.group === ' '); if (!targetUser) { return this.errorReply("User " + this.targetUsername + " not found."); } let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts'); if (showAll && !user.confirmed && targetUser !== user) { return this.errorReply("/alts - Access denied."); } let buf = '' + targetUser.group + '' + Tools.escapeHTML(targetUser.name) + ' ' + (!targetUser.connected ? ' (offline)' : ''); if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) { buf += "
" + 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 (let i in targetUser.roomCount) { if (i === 'global') continue; let targetRoom = Rooms.get(i); let output = (targetRoom.auth && targetRoom.auth[targetUser.userid] ? targetRoom.auth[targetUser.userid] : '') + '' + i + ''; 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); } buf += '
'; if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) { let alts = targetUser.getAlts(true); let output = Object.keys(targetUser.prevNames).join(", "); if (output) buf += "
Previous names: " + Tools.escapeHTML(output); for (let j = 0; j < alts.length; ++j) { let targetAlt = Users.get(alts[j]); if (!targetAlt.named && !targetAlt.connected) continue; if (targetAlt.group === '~' && user.group !== '~') continue; buf += '
Alt: ' + Tools.escapeHTML(targetAlt.name) + '' + (!targetAlt.connected ? " (offline)" : ""); output = Object.keys(targetAlt.prevNames).join(", "); if (output) buf += "
Previous names: " + output; } 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; } } if (targetUser.semilocked) { buf += '
Semilocked: ' + targetUser.semilocked; } } if ((user.can('ip', targetUser) || user === targetUser)) { let ips = Object.keys(targetUser.ips); buf += "
IP" + ((ips.length > 1) ? "s" : "") + ": " + ips.join(", ") + (user.group !== ' ' && targetUser.latestHost ? "
Host: " + Tools.escapeHTML(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 bannedFrom = ""; for (let i = 0; i < Rooms.global.chatRooms.length; i++) { let thisRoom = Rooms.global.chatRooms[i]; if (!thisRoom || thisRoom.isPrivate === true) continue; let roomBanned = ((thisRoom.bannedIps && thisRoom.bannedIps[targetUser.latestIp]) || (thisRoom.bannedUsers && thisRoom.bannedUsers[targetUser.userid])); if (roomBanned) { if (bannedFrom) bannedFrom += ", "; bannedFrom += '' + thisRoom + ' (' + roomBanned + ')'; } } if (bannedFrom) buf += '
Banned from: ' + bannedFrom; } 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."], host: function (target, room, user, connection, cmd) { if (!target) return this.parse('/help host'); if (!this.can('rangeban')) return; if (!/[0-9.]+/.test(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.'); Dnsbl.reverse(target, (err, hosts) => { this.sendReply('IP ' + target + ': ' + (hosts ? hosts[0] : 'NULL')); }); }, hosthelp: ["/host [ip] - Gets the host for a given IP. Requires: & ~"], 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) return this.errorReply("No results found."); return this.sendReply(results.join('; ')); }, ipsearchhelp: ["/ipsearch [ip|range|host] - Find all users with specified IP, IP range, or host. Requires: & ~"], /********************************************************* * Shortcuts *********************************************************/ inv: 'invite', invite: function (target, room, user) { if (!target) return this.parse('/help invite'); target = this.splitTarget(target); if (!this.targetUser) { return this.errorReply("User " + this.targetUsername + " not found."); } let targetRoom = (target ? Rooms.search(target) : room); if (!targetRoom) { return this.errorReply("Room " + target + " not found."); } return this.parse('/msg ' + this.targetUsername + ', /invite ' + targetRoom.id); }, invitehelp: ["/invite [username], [roomname] - Invites the player [username] to join the room [roomname]."], /********************************************************* * Data Search Tools *********************************************************/ pstats: 'data', stats: 'data', dex: 'data', pokedex: 'data', data: function (target, room, user, connection, cmd) { if (!this.runBroadcast()) return; let buffer = ''; let targetId = toId(target); if (!targetId) return this.parse('/help data'); let targetNum = parseInt(targetId); if (!isNaN(targetNum)) { for (let p in Tools.data.Pokedex) { let pokemon = Tools.getTemplate(p); if (pokemon.num === targetNum) { target = pokemon.species; targetId = pokemon.id; break; } } } let newTargets = Tools.dataSearch(target); let showDetails = (cmd === 'dt' || cmd === 'details'); if (newTargets && newTargets.length) { for (let i = 0; i < newTargets.length; ++i) { if (!newTargets[i].exactMatch && !i) { buffer = "No Pok\u00e9mon, item, move, ability or nature named '" + target + "' was found. Showing the data of '" + newTargets[0].name + "' instead.\n"; } if (newTargets[i].searchType === 'nature') { let nature = Tools.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); } else { buffer += '|c|~|/data-' + newTargets[i].searchType + ' ' + newTargets[i].name + '\n'; } } } else { return this.errorReply("No Pok\u00e9mon, item, move, ability or nature named '" + target + "' was found. (Check your spelling?)"); } if (showDetails) { let details; let isSnatch = false; let isMirrorMove = false; if (newTargets[0].searchType === 'pokemon') { let pokemon = Tools.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, "Height": pokemon.heightm + " m", "Weight": pokemon.weightkg + " kg (" + weighthit + " BP)", "Dex Colour": pokemon.color, }; if (pokemon.eggGroups) details["Egg Group(s)"] = pokemon.eggGroups.join(", "); if (!pokemon.evos.length) { details['Does Not Evolve'] = ""; } else { details["Evolution"] = pokemon.evos.map(evo => { evo = Tools.getTemplate(evo); return evo.name + " (" + evo.evoLevel + ")"; }).join(", "); } } else if (newTargets[0].searchType === 'move') { let move = Tools.getMove(newTargets[0].name); details = { "Priority": move.priority, "Gen": move.gen, }; 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']) details["✗ Suppressed by Gravity"] = ""; if (move.id === 'snatch') isSnatch = true; if (move.id === 'mirrormove') isMirrorMove = true; 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"; } else if (newTargets[0].searchType === 'item') { let item = Tools.getItem(newTargets[0].name); details = { "Gen": item.gen, }; 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) { 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(" |  ") + ''; if (isSnatch) buffer += ' |  Snatchable Moves'; if (isMirrorMove) buffer += ' |  Mirrorable Moves'; } this.sendReply(buffer); }, datahelp: ["/data [pokemon/item/move/ability] - Get details on this pokemon/item/move/ability/nature.", "!data [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ # & ~"], dt: 'details', details: function (target) { if (!target) return this.parse('/help details'); this.run('data'); }, detailshelp: ["/details [pokemon] - Get additional details on this pokemon/item/move/ability/nature.", "!details [pokemon] - Show everyone these details. Requires: + % @ # & ~"], 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 = Tools.getTemplate(target); let type1 = Tools.getType(targets[0]); let type2 = Tools.getType(targets[1]); if (pokemon.exists) { target = pokemon.species; } else if (type1.exists && type2.exists && type1 !== type2) { pokemon = {types: [type1.id, type2.id]}; target = type1.id + "/" + type2.id; } else if (type1.exists) { pokemon = {types: [type1.id]}; target = type1.id; } else { return this.sendReplyBox("" + Tools.escapeHTML(target) + " isn't a recognized type or pokemon."); } let weaknesses = []; let resistances = []; let immunities = []; for (let type in Tools.data.TypeChart) { let notImmune = Tools.getImmunity(type, pokemon); if (notImmune) { let typeMod = Tools.getEffectiveness(type, pokemon); switch (typeMod) { case 1: weaknesses.push(type); break; case 2: weaknesses.push("" + type + ""); break; case -1: resistances.push(type); break; case -2: 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: + % @ # & ~"], 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 = Tools[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 (Tools.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 = Tools.getEffectiveness(source, defender.types[i]); let moveMod = source.onEffectiveness && source.onEffectiveness.call(Tools, baseMod, defender.types[i], source); totalTypeMod += typeof moveMod === 'number' ? moveMod : baseMod; } } factor = Math.pow(2, totalTypeMod); } let hasThousandArrows = source.id === 'thousandarrows' && defender.types.indexOf('Flying') >= 0; 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."], 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 Tools.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 Tools.data.TypeChart) { sources.push(move); for (let type in bestCoverage) { if (!Tools.getImmunity(move, type) && !move.ignoreImmunity) continue; eff = Tools.getEffectiveness(move, type); if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } move = Tools.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 (!Tools.getImmunity(move.type, type) && !move.ignoreImmunity) continue; let baseMod = Tools.getEffectiveness(move, type); let moveMod = move.onEffectiveness && move.onEffectiveness.call(Tools, baseMod, type, move); eff = typeof moveMod === 'number' ? moveMod : baseMod; } if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } return this.sendReply("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 Tools.data.TypeChart) { icon[type] = ''; // row of icons at top buffer += ''; } buffer += ''; for (let type1 in Tools.data.TypeChart) { // assembles the rest of the rows buffer += ''; for (let type2 in Tools.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: 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: ' + Tools.escapeHTML(targets[i])); } continue; } } if (!evSet) { if (lowercase === 'invested' || lowercase === 'max') { evSet = 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: ' + Tools.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].indexOf('+') > -1) { nature = 1.1; natureSet = true; } else if (targets[i].indexOf('-') > -1) { 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: ' + Tools.escapeHTML(modifier)); } if (modifier > 6) { return this.sendReplyBox('Modifier should be a number between -6 and +6'); } } if (!pokemon) { let testPoke = Tools.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: 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 = Tools.toDurationString(uptime * 1000); } this.sendReplyBox("Uptime: " + uptimeText + ""); }, groups: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox( "+ Voice - They can use ! commands like !groups, and talk during moderated chat
" + "% Driver - The above, and they can mute. Global % can also lock users and check for alts
" + "@ Moderator - The above, and they can ban users
" + "& Leader - The above, and they can promote to moderator and force ties
" + "# Room Owner - They are leaders of the room and can almost totally control it
" + "~ Administrator - They can do anything, like change what this message says" ); }, groupshelp: ["/groups - Explains what the + % @ # & next to people's names mean.", "!groups - Shows everyone that information. Requires: + % @ # & ~"], 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" ); }, opensourcehelp: ["/opensource - Links to PS's source code repository.", "!opensource - Show everyone that information. Requires: + % @ # & ~"], staff: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Pokémon Showdown Staff List"); }, forums: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Pokémon Showdown Forums"); }, suggestions: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Make a suggestion for Pokémon Showdown"); }, bugreport: 'bugs', bugs: function (target, room, user) { if (!this.runBroadcast()) return; if (room.battle) { this.sendReplyBox("
QuestionsBug Reports
"); } else { this.sendReplyBox( "Have a replay showcasing a bug on Pokémon Showdown?
" + "- Questions
" + "- Bug Reports" ); } }, 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: + % @ # & ~"], 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: + % @ # & ~"], 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
" ); }, calculator: '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: + % @ # & ~"], 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 XY CAP teams" ); }, caphelp: ["/cap - Provides an introduction to the Create-A-Pokémon project.", "!cap - Show everyone that information. Requires: + % @ # & ~"], 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" ); }, om: 'othermetas', othermetas: function (target, room, user) { if (!this.runBroadcast()) return; target = toId(target); let buffer = ""; if (target === 'all' && this.broadcasting) { return this.sendReplyBox("You cannot broadcast information about all Other Metagames at once."); } if (!target || target === 'all') { buffer += "- Other Metagames Forum
"; buffer += "- Other Metagames Analyses
"; if (!target) return this.sendReplyBox(buffer); } let showMonthly = (target === 'all' || target === 'omofthemonth' || target === 'omotm' || target === 'month'); let monthBuffer = "- Other Metagame of the Month"; if (target === 'all') { // Display OMotM formats, with forum thread links as caption this.parse('/formathelp omofthemonth'); if (showMonthly) this.sendReply('|raw|
' + monthBuffer + '
'); // Display the rest of OM formats, with OM hub/index forum links as caption this.parse('/formathelp othermetagames'); return this.sendReply('|raw|
' + buffer + '
'); } if (showMonthly) { this.target = 'omofthemonth'; this.run('formathelp'); this.sendReply('|raw|
' + monthBuffer + '
'); } else { this.run('formathelp'); } }, othermetashelp: ["/om - Provides links to information on the Other Metagames.", "!om - Show everyone that information. Requires: + % @ # & ~"], 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." ); } let targetId = toId(target); if (targetId === 'ladder') targetId = 'search'; if (targetId === 'all') targetId = ''; let formatList; let format = Tools.getFormat(targetId); if (format.effectType === 'Format') formatList = [targetId]; if (!formatList) { if (this.broadcasting && (cmd !== 'om' && cmd !== 'othermetas')) return this.sendReply("'" + target + "' is not a format. This command's search mode is too spammy to broadcast."); formatList = Object.keys(Tools.data.Formats).filter(formatid => Tools.data.Formats[formatid].effectType === 'Format'); } // Filter formats and group by section let exactMatch = ''; let sections = {}; let totalMatches = 0; for (let i = 0; i < formatList.length; i++) { let format = Tools.getFormat(formatList[i]); let sectionId = toId(format.section); if (targetId && !format[targetId + 'Show'] && sectionId !== targetId && format.id === formatList[i] && !format.id.startsWith(targetId)) 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.sendReply("No " + (target ? "matched " : "") + "formats found."); if (totalMatches === 1) { let format = Tools.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("
")); } // Build tables let buf = []; for (let sectionId in sections) { if (exactMatch && sectionId !== exactMatch) continue; buf.push("

" + Tools.escapeHTML(sections[sectionId].name) + "

"); buf.push(""); for (let i = 0; i < sections[sectionId].formats.length; i++) { let format = Tools.getFormat(sections[sectionId].formats[i]); buf.push(""); } buf.push("
NameDescription
" + Tools.escapeHTML(format.name) + "" + (format.desc ? format.desc.join("
") : "—") + "
"); } return this.sendReply("|raw|
" + buf.join("") + "
"); }, roomhelp: function (target, room, user) { if (room.id === 'lobby' || room.battle) return this.sendReply("This command is too spammy for lobby/battles."); if (!this.runBroadcast()) 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
" + "- /modchat [off/autoconfirmed/+]: set modchat level
" + "- /staffintro intro: sets the staff introduction that will be displayed for all staff joining the room
" + "
" + "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
" + "- /modchat [%/@/#]: set modchat level
" + "- /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
" + "
" + "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
" + "
" + "More detailed help can be found here
" + "" ); }, restarthelp: function (target, room, user) { if (room.id === 'lobby' && !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: function (target, room, user) { if (!this.can('lockdown')) return false; let buf = "" + process.pid + " - Main
"; for (let i in Sockets.workers) { let worker = Sockets.workers[i]; buf += "" + (worker.pid || worker.process.pid) + " - Sockets " + i + "
"; } const ProcessManager = require('./../process-manager'); for (let managerData of ProcessManager.cache) { let i = 0; let processType = path.basename(managerData[1]); for (let process of managerData[0].processes) { buf += "" + process.process.pid + " - " + processType + " " + (i++) + "
"; } } this.sendReplyBox(buf); }, rule: 'rules', rules: function (target, room, user) { if (!target) { if (!this.runBroadcast()) return; this.sendReplyBox("Please follow the rules:
" + (room.rulesLink ? "- " + Tools.escapeHTML(room.title) + " room rules
" : "") + "- " + (room.rulesLink ? "Global rules" : "Rules") + ""); return; } if (!this.can('roommod', 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."); } room.rulesLink = target.trim(); this.sendReply("(The room rules link is now: " + 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: # & ~"], faq: function (target, room, user) { if (!this.runBroadcast()) return; target = target.toLowerCase(); let buffer = ""; let matched = false; if (target === 'all' && this.broadcasting) { return this.sendReplyBox("You cannot broadcast all FAQs at once."); } if (!target || target === 'all') { matched = true; buffer += "Frequently Asked Questions
"; } if (target === 'all' || target === 'elo') { matched = true; buffer += "Why did this user gain or lose so many points?
"; } if (target === 'all' || target === 'doubles' || target === 'triples' || target === 'rotation') { matched = true; buffer += "Can I play doubles/triples/rotation battles here?
"; } if (target === 'all' || target === 'restarts') { matched = true; buffer += "Why is the server restarting?
"; } if (target === 'all' || target === 'star' || target === 'player') { matched = true; buffer += 'Why is there this star (★) in front of my username?
'; } if (target === 'all' || target === 'staff') { matched = true; buffer += "Staff FAQ
"; } if (target === 'all' || target === 'autoconfirmed' || target === 'ac') { matched = true; buffer += "A user is autoconfirmed when they have won at least one rated battle and have been registered for a week or longer.
"; } if (target === 'all' || target === 'customavatar' || target === 'ca') { matched = true; buffer += "How can I get a custom avatar?
"; } if (target === 'all' || target === 'pm' || target === 'msg' || target === 'w') { matched = true; buffer += "How can I send a user a private message?
"; } if (target === 'all' || target === 'challenge' || target === 'chall') { matched = true; buffer += "How can I battle a specific user?
"; } if (target === 'all' || target === 'gxe') { matched = true; buffer += "What does GXE mean?
"; } if (target === 'all' || target === 'coil') { matched = true; buffer += "What is COIL?
"; } if (target === 'all' || target === 'tiering' || target === 'tiers' || target === 'tier') { matched = true; buffer += "Tiering FAQ
"; } if (!matched) { return this.sendReply("The FAQ entry '" + target + "' was not found. Try /faq for general help."); } this.sendReplyBox(buffer); }, 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: + % @ # & ~"], analysis: 'smogdex', strategy: 'smogdex', smogdex: function (target, room, user) { if (!this.runBroadcast()) return; let targets = target.split(','); let pokemon = Tools.getTemplate(targets[0]); let item = Tools.getItem(targets[0]); let move = Tools.getMove(targets[0]); let ability = Tools.getAbility(targets[0]); let format = Tools.getFormat(targets[0]); let atLeastOne = false; let generation = (targets[1] || 'xy').trim().toLowerCase(); let genNumber = 6; let extraFormat = Tools.getFormat(targets[2]); if (generation === 'xy' || generation === 'oras' || generation === '6' || generation === 'six') { generation = 'xy'; } 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 = 'xy'; } // 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 === 'Keldeo' || pokemon.baseSpecies === 'Genesect') { pokemon = Tools.getTemplate(pokemon.baseSpecies); } let formatName = extraFormat.name; let formatId = extraFormat.id; if (formatId === 'doublesou') { formatId = 'doubles'; } 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 and Hoopa-Unbound if (speciesid === 'meowstic') speciesid = 'meowsticm'; if (pokemon.tier === 'CAP') { this.sendReplyBox("" + generation.toUpperCase() + " " + Tools.escapeHTML(formatName) + " " + pokemon.name + " analysis preview, brought to you by Smogon University CAP Project"); } else { this.sendReplyBox("" + generation.toUpperCase() + " " + Tools.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 === 'doublesou') { formatId = 'doubles'; } 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() + " " + Tools.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] - Links to the Smogon University analysis for this Pok\u00e9mon in the given generation.", "!analysis [pokemon], [generation] - Shows everyone this link. Requires: + % @ # & ~"], veekun: function (target, broadcast, user) { if (!this.runBroadcast()) return; let baseLink = 'http://veekun.com/dex/'; let pokemon = Tools.getTemplate(target); let item = Tools.getItem(target); let move = Tools.getMove(target); let ability = Tools.getAbility(target); let nature = Tools.getNature(target); let atLeastOne = false; // Pokemon if (pokemon.exists) { atLeastOne = true; if (pokemon.isNonstandard) return this.sendReply(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.sendReply(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: 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; Simulator.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 + "."); } }, roll: 'dice', dice: function (target, room, user) { if (!target || target.match(/[^d\d\s\-\+HL]/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."], 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: ' + Tools.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 (user.userid === 'github') { if (!this.can('announce', null, room)) return; if (message.charAt(0) === '!') this.broadcasting = true; } else { if (!this.can('declare', null, room)) return; if (!this.runBroadcast('!htmlbox')) return; } this.sendReplyBox(target); }, htmlboxhelp: ["/htmlbox [message] - Displays a message, parsing HTML code contained. Requires: ~ # with global authority"], }; process.nextTick(() => Tools.includeData());