mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-06 13:47:24 -05:00
2372 lines
97 KiB
JavaScript
2372 lines
97 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
const RESULTS_MAX_LENGTH = 10;
|
|
|
|
var commands = exports.commands = {
|
|
|
|
ip: 'whois',
|
|
rooms: 'whois',
|
|
alt: 'whois',
|
|
alts: 'whois',
|
|
whoare: 'whois',
|
|
whois: function (target, room, user, connection, cmd) {
|
|
if (room.id === 'staff' && !this.canBroadcast()) return;
|
|
var targetUser = this.targetUserOrSelf(target, user.group === ' ');
|
|
if (!targetUser) {
|
|
return this.sendReply("User " + this.targetUsername + " not found.");
|
|
}
|
|
var showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
|
|
if (showAll && !user.can('lock') && targetUser !== user) {
|
|
return this.errorReply("/alts - Access denied.");
|
|
}
|
|
|
|
var buf = '<strong class="username"><small style="display:none">' + targetUser.group + '</small>' + Tools.escapeHTML(targetUser.name) + '</strong> ' + (!targetUser.connected ? ' <em style="color:gray">(offline)</em>' : '');
|
|
if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) {
|
|
buf += "<br />" + Config.groups[targetUser.group].name + " (" + targetUser.group + ")";
|
|
}
|
|
if (targetUser.isSysop) {
|
|
buf += "<br />(Pokémon Showdown System Operator)";
|
|
}
|
|
if (!targetUser.registered) {
|
|
buf += "<br />(Unregistered)";
|
|
}
|
|
var publicrooms = "";
|
|
var hiddenrooms = "";
|
|
var privaterooms = "";
|
|
for (var i in targetUser.roomCount) {
|
|
if (i === 'global') continue;
|
|
var targetRoom = Rooms.get(i);
|
|
|
|
var output = (targetRoom.auth && targetRoom.auth[targetUser.userid] ? targetRoom.auth[targetUser.userid] : '') + '<a href="/' + i + '" room="' + i + '">' + i + '</a>';
|
|
if (targetRoom.isPrivate === true) {
|
|
if (privaterooms) privaterooms += " | ";
|
|
privaterooms += output;
|
|
} else if (targetRoom.isPrivate) {
|
|
if (hiddenrooms) hiddenrooms += " | ";
|
|
hiddenrooms += output;
|
|
} else {
|
|
if (publicrooms) publicrooms += " | ";
|
|
publicrooms += output;
|
|
}
|
|
}
|
|
buf += '<br />Rooms: ' + (publicrooms || '<em>(no public rooms)</em>');
|
|
|
|
if (!showAll) {
|
|
return this.sendReplyBox(buf);
|
|
}
|
|
buf += '<br />';
|
|
if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) {
|
|
var alts = targetUser.getAlts(true);
|
|
var output = Object.keys(targetUser.prevNames).join(", ");
|
|
if (output) buf += "<br />Previous names: " + Tools.escapeHTML(output);
|
|
|
|
for (var j = 0; j < alts.length; ++j) {
|
|
var targetAlt = Users.get(alts[j]);
|
|
if (!targetAlt.named && !targetAlt.connected) continue;
|
|
if (targetAlt.group === '~' && user.group !== '~') continue;
|
|
|
|
buf += '<br />Alt: <span class="username">' + Tools.escapeHTML(targetAlt.name) + '</span>' + (!targetAlt.connected ? " <em style=\"color:gray\">(offline)</em>" : "");
|
|
output = Object.keys(targetAlt.prevNames).join(", ");
|
|
if (output) buf += "<br />Previous names: " + output;
|
|
}
|
|
if (targetUser.locked) {
|
|
buf += '<br />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 += '<br />Semilocked: ' + targetUser.semilocked;
|
|
}
|
|
}
|
|
if ((user.can('ip', targetUser) || user === targetUser)) {
|
|
var ips = Object.keys(targetUser.ips);
|
|
buf += "<br /> IP" + ((ips.length > 1) ? "s" : "") + ": " + ips.join(", ") +
|
|
(user.group !== ' ' && targetUser.latestHost ? "<br />Host: " + Tools.escapeHTML(targetUser.latestHost) : "");
|
|
}
|
|
if ((user === targetUser || user.can('alts')) && hiddenrooms) {
|
|
buf += '<br />Hidden rooms: ' + hiddenrooms;
|
|
}
|
|
if ((user === targetUser || user.hasConsoleAccess(connection)) && privaterooms) {
|
|
buf += '<br />Private rooms: ' + privaterooms;
|
|
}
|
|
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.sendReply('You must pass a valid IPv4 IP to /host.');
|
|
var self = this;
|
|
Dnsbl.reverse(target, function (err, hosts) {
|
|
self.sendReply('IP ' + target + ': ' + (hosts ? hosts[0] : 'NULL'));
|
|
});
|
|
},
|
|
hosthelp: ["/host [ip] - Gets the host for a given IP. Requires: & ~"],
|
|
|
|
ipsearchall: 'ipsearch',
|
|
hostsearch: 'ipsearch',
|
|
ipsearch: function (target, room, user, connection, cmd) {
|
|
if (!target.trim()) return this.parse('/help ipsearch');
|
|
if (!this.can('rangeban')) return;
|
|
var results = [];
|
|
|
|
var isAll = (cmd === 'ipsearchall');
|
|
|
|
if (/[a-z]/.test(target)) {
|
|
// host
|
|
this.sendReply("Users with host " + target + ":");
|
|
for (var userid in Users.users) {
|
|
var curUser = Users.users[userid];
|
|
if (!curUser.latestHost || !curUser.latestHost.endsWith(target)) continue;
|
|
if (results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name) > 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);
|
|
for (var userid in Users.users) {
|
|
var curUser = Users.users[userid];
|
|
if (!curUser.latestIp.startsWith(target)) continue;
|
|
if (results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name) > 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 + ":");
|
|
for (var userid in Users.users) {
|
|
var curUser = Users.users[userid];
|
|
if (curUser.latestIp === target) {
|
|
results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
|
|
}
|
|
}
|
|
}
|
|
if (!results.length) 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: & ~"],
|
|
|
|
/*********************************************************
|
|
* Shortcuts
|
|
*********************************************************/
|
|
|
|
inv: 'invite',
|
|
invite: function (target, room, user) {
|
|
if (!target) return this.parse('/help invite');
|
|
target = this.splitTarget(target);
|
|
if (!this.targetUser) {
|
|
return this.sendReply("User " + this.targetUsername + " not found.");
|
|
}
|
|
var targetRoom = (target ? Rooms.search(target) : room);
|
|
if (!targetRoom) {
|
|
return this.sendReply("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.canBroadcast()) return;
|
|
|
|
var buffer = '';
|
|
var targetId = toId(target);
|
|
if (!targetId) return this.parse('/help data');
|
|
if (targetId === '' + parseInt(targetId)) {
|
|
for (var p in Tools.data.Pokedex) {
|
|
var pokemon = Tools.getTemplate(p);
|
|
if (pokemon.num === parseInt(target)) {
|
|
target = pokemon.species;
|
|
targetId = pokemon.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var newTargets = Tools.dataSearch(target);
|
|
var showDetails = (cmd === 'dt' || cmd === 'details');
|
|
if (newTargets && newTargets.length) {
|
|
for (var i = 0; i < newTargets.length; ++i) {
|
|
if (newTargets[i].id !== targetId && !Tools.data.Aliases[targetId] && !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') {
|
|
buffer += "" + newTargets[i].name + " nature: ";
|
|
if (newTargets[i].plus) {
|
|
var statNames = {'atk': "Attack", 'def': "Defense", 'spa': "Special Attack", 'spd': "Special Defense", 'spe': "Speed"};
|
|
buffer += "+10% " + statNames[newTargets[i].plus] + ", -10% " + statNames[newTargets[i].minus] + ".";
|
|
} else {
|
|
buffer += "No effect.";
|
|
}
|
|
return this.sendReply(buffer);
|
|
} else {
|
|
buffer += '|c|~|/data-' + newTargets[i].searchType + ' ' + newTargets[i].name + '\n';
|
|
}
|
|
}
|
|
} else {
|
|
return this.sendReply("No Pok\u00e9mon, item, move, ability or nature named '" + target + "' was found. (Check your spelling?)");
|
|
}
|
|
|
|
if (showDetails) {
|
|
var details;
|
|
var isSnatch = false;
|
|
var isMirrorMove = false;
|
|
if (newTargets[0].searchType === 'pokemon') {
|
|
var pokemon = Tools.getTemplate(newTargets[0].name);
|
|
var 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 <em>(" + weighthit + " BP)</em>",
|
|
"Dex Colour": pokemon.color,
|
|
"Egg Group(s)": pokemon.eggGroups.join(", ")
|
|
};
|
|
if (!pokemon.evos.length) {
|
|
details["<font color=#585858>Does Not Evolve</font>"] = "";
|
|
} else {
|
|
details["Evolution"] = pokemon.evos.map(function (evo) {
|
|
evo = Tools.getTemplate(evo);
|
|
return evo.name + " (" + evo.evoLevel + ")";
|
|
}).join(", ");
|
|
}
|
|
} else if (newTargets[0].searchType === 'move') {
|
|
var move = Tools.getMove(newTargets[0].name);
|
|
details = {
|
|
"Priority": move.priority,
|
|
"Gen": move.gen
|
|
};
|
|
|
|
if (move.secondary || move.secondaries) details["<font color=black>✓ Secondary effect</font>"] = "";
|
|
if (move.flags['contact']) details["<font color=black>✓ Contact</font>"] = "";
|
|
if (move.flags['sound']) details["<font color=black>✓ Sound</font>"] = "";
|
|
if (move.flags['bullet']) details["<font color=black>✓ Bullet</font>"] = "";
|
|
if (move.flags['pulse']) details["<font color=black>✓ Pulse</font>"] = "";
|
|
if (!move.flags['protect'] && !/(ally|self)/i.test(move.target)) details["<font color=black>✓ Bypasses Protect</font>"] = "";
|
|
if (move.flags['authentic']) details["<font color=black>✓ Bypasses Substitutes</font>"] = "";
|
|
if (move.flags['defrost']) details["<font color=black>✓ Thaws user</font>"] = "";
|
|
if (move.flags['bite']) details["<font color=black>✓ Bite</font>"] = "";
|
|
if (move.flags['punch']) details["<font color=black>✓ Punch</font>"] = "";
|
|
if (move.flags['powder']) details["<font color=black>✓ Powder</font>"] = "";
|
|
if (move.flags['reflectable']) details["<font color=black>✓ Bounceable</font>"] = "";
|
|
if (move.flags['gravity']) details["<font color=black>✗ Suppressed by Gravity</font>"] = "";
|
|
|
|
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') {
|
|
var 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|<font size="1">' + Object.keys(details).map(function (detail) {
|
|
return '<font color=#585858>' + detail + (details[detail] !== '' ? ':</font> ' + details[detail] : '</font>');
|
|
}).join(" |  ") + '</font>';
|
|
|
|
if (isSnatch) buffer += ' |  <a href="https://pokemonshowdown.com/dex/moves/snatch"><font size="1">Snatchable Moves</font></a>';
|
|
if (isMirrorMove) buffer += ' |  <a href="https://pokemonshowdown.com/dex/moves/mirrormove"><font size="1">Mirrorable Moves</font></a>';
|
|
}
|
|
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: + % @ # & ~"],
|
|
|
|
ds: 'dexsearch',
|
|
dsearch: 'dexsearch',
|
|
dexsearch: function (target, room, user, connection, cmd, message) {
|
|
if (!this.canBroadcast()) return;
|
|
|
|
if (!target) return this.parse('/help dexsearch');
|
|
var targets = target.split(',');
|
|
var searches = {};
|
|
var allTiers = {'uber':1, 'ou':1, 'bl':1, 'uu':1, 'bl2':1, 'ru':1, 'bl3':1, 'nu':1, 'bl4':1, 'pu':1, 'nfe':1, 'lc uber':1, 'lc':1, 'cap':1};
|
|
var allColours = {'green':1, 'red':1, 'blue':1, 'white':1, 'brown':1, 'yellow':1, 'purple':1, 'pink':1, 'gray':1, 'black':1};
|
|
var allStats = {'hp':1, 'atk':1, 'def':1, 'spa':1, 'spd':1, 'spe':1, 'bst':1};
|
|
var showAll = false;
|
|
var megaSearch = null;
|
|
var randomOutput = 0;
|
|
var categories = ['gen', 'tier', 'color', 'types', 'ability', 'stats', 'compileLearnsets', 'moves', 'recovery', 'priority'];
|
|
|
|
for (var i = 0; i < targets.length; i++) {
|
|
var isNotSearch = false;
|
|
target = targets[i].trim().toLowerCase();
|
|
if (target.charAt(0) === '!') {
|
|
isNotSearch = true;
|
|
target = target.substr(1);
|
|
}
|
|
|
|
var targetAbility = Tools.getAbility(targets[i]);
|
|
if (targetAbility.exists) {
|
|
if (!searches['ability']) searches['ability'] = {};
|
|
if (Object.count(searches['ability'], true) === 1 && !isNotSearch) return this.sendReplyBox("Specify only one ability.");
|
|
if ((searches['ability'][targetAbility.name] && isNotSearch) || (searches['ability'][targetAbility.name] === false && !isNotSearch)) return this.sendReplyBox("A search cannot both exclude and include an ability.");
|
|
searches['ability'][targetAbility.name] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target in allTiers) {
|
|
if (!searches['tier']) searches['tier'] = {};
|
|
if ((searches['tier'][target] && isNotSearch) || (searches['tier'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a tier.');
|
|
searches['tier'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target in allColours) {
|
|
if (!searches['color']) searches['color'] = {};
|
|
if ((searches['color'][target] && isNotSearch) || (searches['color'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a color.');
|
|
searches['color'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target.substr(0, 3) === 'gen' && Number.isInteger(parseFloat(target.substr(3)))) target = target.substr(3).trim();
|
|
var targetInt = parseInt(target);
|
|
if (0 < targetInt && targetInt < 7) {
|
|
if (!searches['gen']) searches['gen'] = {};
|
|
if ((searches['gen'][target] && isNotSearch) || (searches['gen'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a generation.');
|
|
searches['gen'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target === 'all') {
|
|
if (this.broadcasting) return this.sendReplyBox("A search with the parameter 'all' cannot be broadcast.");
|
|
showAll = true;
|
|
continue;
|
|
}
|
|
|
|
if (target.substr(0, 6) === 'random' && cmd === 'randpoke') {
|
|
randomOutput = parseInt(target.substr(6));
|
|
continue;
|
|
}
|
|
|
|
if (target === 'megas' || target === 'mega') {
|
|
if ((megaSearch && isNotSearch) || (megaSearch === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include Mega Evolutions.');
|
|
megaSearch = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target === 'recovery') {
|
|
if ((searches['recovery'] && isNotSearch) || (searches['recovery'] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and recovery moves.');
|
|
searches['recovery'] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target === 'priority') {
|
|
if ((searches['priority'] && isNotSearch) || (searches['priority'] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and recovery moves.');
|
|
searches['priority'] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
var targetMove = Tools.getMove(target);
|
|
if (targetMove.exists) {
|
|
if (!searches['moves']) searches['moves'] = {};
|
|
if (Object.count(searches['moves'], true) === 4 && !isNotSearch) return this.sendReplyBox("Specify a maximum of 4 moves.");
|
|
if ((searches['moves'][targetMove.id] && isNotSearch) || (searches['moves'][targetMove.id] === false && !isNotSearch)) return this.sendReplyBox("A search cannot both exclude and include a move.");
|
|
searches['moves'][targetMove.id] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
var typeIndex = target.indexOf(' type');
|
|
if (typeIndex >= 0) {
|
|
target = target.charAt(0).toUpperCase() + target.substring(1, typeIndex);
|
|
if (target in Tools.data.TypeChart) {
|
|
if (!searches['types']) searches['types'] = {};
|
|
if (Object.count(searches['types'], true) === 2 && !isNotSearch) return this.sendReplyBox("Specify a maximum of two types.");
|
|
if ((searches['types'][target] && isNotSearch) || (searches['types'][target] === false && !isNotSearch)) return this.sendReplyBox("A search cannot both exclude and include a type.");
|
|
searches['types'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var inequality = target.search(/>|<|=/);
|
|
if (inequality >= 0) {
|
|
if (isNotSearch) return this.sendReplyBox("You cannot use the negation symbol '!' in stat ranges.");
|
|
if (target.charAt(inequality + 1) === '=') {
|
|
inequality = target.substr(inequality, 2);
|
|
} else {
|
|
inequality = target.charAt(inequality);
|
|
}
|
|
var inequalityOffset = (inequality.charAt(1) === '=' ? 0 : -1);
|
|
var targetParts = target.replace(/\s/g, '').split(inequality);
|
|
var num, stat, direction;
|
|
if (!isNaN(targetParts[0])) {
|
|
// e.g. 100 < spe
|
|
num = parseFloat(targetParts[0]);
|
|
stat = targetParts[1];
|
|
switch (inequality.charAt(0)) {
|
|
case '>': direction = 'less'; num += inequalityOffset; break;
|
|
case '<': direction = 'greater'; num -= inequalityOffset; break;
|
|
case '=': direction = 'equal'; break;
|
|
}
|
|
} else if (!isNaN(targetParts[1])) {
|
|
// e.g. spe > 100
|
|
num = parseFloat(targetParts[1]);
|
|
stat = targetParts[0];
|
|
switch (inequality.charAt(0)) {
|
|
case '<': direction = 'less'; num += inequalityOffset; break;
|
|
case '>': direction = 'greater'; num -= inequalityOffset; break;
|
|
case '=': direction = 'equal'; break;
|
|
}
|
|
} else {
|
|
return this.sendReplyBox("No value given to compare with '" + Tools.escapeHTML(target) + "'.");
|
|
}
|
|
switch (toId(stat)) {
|
|
case 'attack': stat = 'atk'; break;
|
|
case 'defense': stat = 'def'; break;
|
|
case 'specialattack': stat = 'spa'; break;
|
|
case 'spatk': stat = 'spa'; break;
|
|
case 'specialdefense': stat = 'spd'; break;
|
|
case 'spdef': stat = 'spd'; break;
|
|
case 'speed': stat = 'spe'; break;
|
|
}
|
|
if (!(stat in allStats)) return this.sendReplyBox("'" + Tools.escapeHTML(target) + "' did not contain a valid stat.");
|
|
if (!searches['stats']) searches['stats'] = {};
|
|
if (direction === 'equal') {
|
|
if (searches['stats'][stat]) return this.sendReplyBox("Invalid stat range for " + stat + ".");
|
|
searches['stats'][stat] = {};
|
|
searches['stats'][stat]['less'] = num;
|
|
searches['stats'][stat]['greater'] = num;
|
|
} else {
|
|
if (!searches['stats'][stat]) searches['stats'][stat] = {};
|
|
if (searches['stats'][stat][direction]) return this.sendReplyBox("Invalid stat range for " + stat + ".");
|
|
searches['stats'][stat][direction] = num;
|
|
}
|
|
continue;
|
|
}
|
|
return this.sendReplyBox("'" + Tools.escapeHTML(target) + "' could not be found in any of the search categories.");
|
|
}
|
|
|
|
if (showAll && Object.size(searches) === 0 && megaSearch === null) return this.sendReplyBox("No search parameters other than 'all' were found. Try '/help dexsearch' for more information on this command.");
|
|
|
|
var dex = {};
|
|
for (var pokemon in Tools.data.Pokedex) {
|
|
var template = Tools.getTemplate(pokemon);
|
|
var megaSearchResult = (megaSearch === null || (megaSearch === true && template.isMega) || (megaSearch === false && !template.isMega));
|
|
if (template.tier !== 'Unreleased' && template.tier !== 'Illegal' && (template.tier !== 'CAP' || (searches['tier'] && searches['tier']['cap'])) && megaSearchResult) {
|
|
dex[pokemon] = template;
|
|
}
|
|
}
|
|
|
|
//Only construct full learnsets for Pokemon if learnsets are used in the search
|
|
if (searches.moves || searches.recovery || searches.priority) searches['compileLearnsets'] = true;
|
|
|
|
for (var cat = 0; cat < categories.length; cat++) {
|
|
var search = categories[cat];
|
|
if (!searches[search]) continue;
|
|
switch (search) {
|
|
case 'types':
|
|
for (var mon in dex) {
|
|
if (Object.count(searches[search], true) === 2) {
|
|
if (!(searches[search][dex[mon].types[0]]) || !(searches[search][dex[mon].types[1]])) delete dex[mon];
|
|
} else {
|
|
if (searches[search][dex[mon].types[0]] === false || searches[search][dex[mon].types[1]] === false || (Object.count(searches[search], true) > 0 &&
|
|
(!(searches[search][dex[mon].types[0]]) && !(searches[search][dex[mon].types[1]])))) delete dex[mon];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'tier':
|
|
for (var mon in dex) {
|
|
if ('lc' in searches[search]) {
|
|
// some LC legal Pokemon are stored in other tiers (Ferroseed/Murkrow etc)
|
|
// this checks for LC legality using the going criteria, instead of dex[mon].tier
|
|
var isLC = (dex[mon].evos && dex[mon].evos.length > 0) && !dex[mon].prevo && dex[mon].tier !== "LC Uber" && Tools.data.Formats['lc'].banlist.indexOf(dex[mon].species) < 0;
|
|
if ((searches[search]['lc'] && !isLC) || (!searches[search]['lc'] && isLC)) {
|
|
delete dex[mon];
|
|
continue;
|
|
}
|
|
}
|
|
if (searches[search][String(dex[mon][search]).toLowerCase()] === false ||
|
|
Object.count(searches[search], true) > 0 && !searches[search][String(dex[mon][search]).toLowerCase()]) {
|
|
delete dex[mon];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'gen':
|
|
case 'color':
|
|
for (var mon in dex) {
|
|
if (searches[search][String(dex[mon][search]).toLowerCase()] === false ||
|
|
Object.count(searches[search], true) > 0 && !searches[search][String(dex[mon][search]).toLowerCase()]) {
|
|
delete dex[mon];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'ability':
|
|
for (var mon in dex) {
|
|
for (var ability in searches[search]) {
|
|
var needsAbility = searches[search][ability];
|
|
var hasAbility = Object.count(dex[mon].abilities, ability) > 0;
|
|
if (hasAbility !== needsAbility) {
|
|
delete dex[mon];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'compileLearnsets':
|
|
for (var mon in dex) {
|
|
var template = dex[mon];
|
|
if (!template.learnset) template = Tools.getTemplate(template.baseSpecies);
|
|
if (!template.learnset) continue;
|
|
var fullLearnset = template.learnset;
|
|
while (template.prevo) {
|
|
template = Tools.getTemplate(template.prevo);
|
|
for (var move in template.learnset) {
|
|
if (!fullLearnset[move]) fullLearnset[move] = template.learnset[move];
|
|
}
|
|
}
|
|
dex[mon].learnset = fullLearnset;
|
|
}
|
|
break;
|
|
|
|
case 'moves':
|
|
for (var mon in dex) {
|
|
if (!dex[mon].learnset) continue;
|
|
for (var move in searches[search]) {
|
|
var canLearn = (dex[mon].learnset.sketch && ['chatter', 'struggle', 'magikarpsrevenge'].indexOf(move) < 0) || dex[mon].learnset[move];
|
|
if ((!canLearn && searches[search][move]) || (searches[search][move] === false && canLearn)) {
|
|
delete dex[mon];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'recovery':
|
|
for (var mon in dex) {
|
|
if (!dex[mon].learnset) continue;
|
|
var recoveryMoves = ["recover", "roost", "moonlight", "morningsun", "synthesis", "milkdrink", "slackoff", "softboiled", "wish", "healorder"];
|
|
var canLearn = false;
|
|
for (var i = 0; i < recoveryMoves.length; i++) {
|
|
canLearn = (dex[mon].learnset.sketch) || dex[mon].learnset[recoveryMoves[i]];
|
|
if (canLearn) break;
|
|
}
|
|
if ((!canLearn && searches[search]) || (searches[search] === false && canLearn)) delete dex[mon];
|
|
}
|
|
break;
|
|
|
|
case 'priority':
|
|
var priorityMoves = [];
|
|
for (var move in Tools.data.Movedex) {
|
|
var moveData = Tools.getMove(move);
|
|
if (moveData.category === "Status" || moveData.id === "bide") continue;
|
|
if (moveData.priority > 0) priorityMoves.push(move);
|
|
}
|
|
for (var mon in dex) {
|
|
if (!dex[mon].learnset) continue;
|
|
var canLearn = false;
|
|
for (var i = 0; i < priorityMoves.length; i++) {
|
|
canLearn = (dex[mon].learnset.sketch) || dex[mon].learnset[priorityMoves[i]];
|
|
if (canLearn) break;
|
|
}
|
|
if ((!canLearn && searches[search]) || (searches[search] === false && canLearn)) delete dex[mon];
|
|
}
|
|
break;
|
|
|
|
case 'stats':
|
|
for (var stat in searches[search]) {
|
|
for (var mon in dex) {
|
|
var monStat = 0;
|
|
if (stat === 'bst') {
|
|
for (var monStats in dex[mon].baseStats) {
|
|
monStat += dex[mon].baseStats[monStats];
|
|
}
|
|
} else {
|
|
monStat = dex[mon].baseStats[stat];
|
|
}
|
|
if (typeof searches[search][stat].less === 'number') {
|
|
if (monStat > searches[search][stat].less) {
|
|
delete dex[mon];
|
|
continue;
|
|
}
|
|
}
|
|
if (typeof searches[search][stat].greater === 'number') {
|
|
if (monStat < searches[search][stat].greater) {
|
|
delete dex[mon];
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Error("/dexsearch search category '" + search + "' was unrecognised.");
|
|
}
|
|
}
|
|
|
|
var results = [];
|
|
for (var mon in dex) {
|
|
if (dex[mon].baseSpecies && results.indexOf(dex[mon].baseSpecies) >= 0) continue;
|
|
results.push(dex[mon].species);
|
|
}
|
|
|
|
if (randomOutput && randomOutput < results.length) {
|
|
results = results.randomize().slice(0, randomOutput);
|
|
}
|
|
|
|
var resultsStr = this.broadcasting ? "" : ("<font color=#999999>" + message + ":</font><br>");
|
|
if (results.length > 1) {
|
|
if (showAll || results.length <= RESULTS_MAX_LENGTH + 5) {
|
|
results.sort();
|
|
resultsStr += results.join(", ");
|
|
} else {
|
|
resultsStr += results.slice(0, RESULTS_MAX_LENGTH).join(", ") + ", and " + (results.length - RESULTS_MAX_LENGTH) + " more. <font color=#999999>Redo the search with 'all' as a search parameter to show all results.</font>";
|
|
}
|
|
} else if (results.length === 1) {
|
|
return CommandParser.commands.data.call(this, results[0], room, user, connection, 'dt');
|
|
} else {
|
|
resultsStr += "No Pokémon found.";
|
|
}
|
|
return this.sendReplyBox(resultsStr);
|
|
},
|
|
dexsearchhelp: ["/dexsearch [type], [move], [move], ... - Searches for Pok\u00e9mon that fulfill the selected criteria",
|
|
"Search categories are: type, tier, color, moves, ability, gen, recovery, priority, stat.",
|
|
"Valid colors are: green, red, blue, white, brown, yellow, purple, pink, gray and black.",
|
|
"Valid tiers are: Uber/OU/BL/UU/BL2/RU/BL3/NU/PU/NFE/LC/CAP.",
|
|
"Types must be followed by ' type', e.g., 'dragon type'.",
|
|
"Inequality ranges use the characters '>' and '<' though they behave as '≥' and '≤', e.g., 'speed > 100' searches for all Pokemon equal to and greater than 100 speed.",
|
|
"Parameters can be excluded through the use of '!', e.g., '!water type' excludes all water types.",
|
|
"The parameter 'mega' can be added to search for Mega Evolutions only, and the parameter 'NFE' can be added to search not-fully evolved Pokemon only.",
|
|
"The order of the parameters does not matter."],
|
|
|
|
rollpokemon: 'randompokemon',
|
|
randpoke: 'randompokemon',
|
|
randompokemon: function (target, room, user, connection, cmd, message) {
|
|
var targets = target.split(",");
|
|
var targetsBuffer = [];
|
|
var qty;
|
|
for (var i = 0; i < targets.length; i++) {
|
|
if (!targets[i]) continue;
|
|
var num = Number(targets[i]);
|
|
if (Number.isInteger(num)) {
|
|
if (qty) return this.sendReply("Only specify the number of Pokemon once.");
|
|
qty = num;
|
|
if (qty < 1 || 15 < qty) return this.sendReply("Number of random Pokemon must be between 1 and 15.");
|
|
targetsBuffer.push("random" + qty);
|
|
} else {
|
|
targetsBuffer.push(targets[i]);
|
|
}
|
|
}
|
|
if (!qty) targetsBuffer.push("random1");
|
|
|
|
CommandParser.commands.dexsearch.call(this, targetsBuffer.join(","), room, user, connection, "randpoke", message);
|
|
},
|
|
randompokemonhelp: ["/randompokemon - Generates random Pokemon based on given search conditions.",
|
|
"/randompokemon uses the same parameters as /dexsearch (see '/help ds').",
|
|
"Adding a number as a parameter returns that many random Pokemon, e.g., '/randpoke 6' returns 6 random Pokemon."],
|
|
|
|
ms: 'movesearch',
|
|
msearch: 'movesearch',
|
|
movesearch: function (target, room, user, connection, cmd, message) {
|
|
if (!this.canBroadcast()) return;
|
|
|
|
if (!target) return this.parse('/help movesearch');
|
|
var targets = target.split(',');
|
|
var searches = {};
|
|
var allCategories = {'physical':1, 'special':1, 'status':1};
|
|
var allProperties = {'basePower':1, 'accuracy':1, 'priority':1, 'pp':1};
|
|
var allFlags = {'authentic':1, 'bite':1, 'bullet':1, 'contact':1, 'defrost':1, 'powder':1, 'pulse':1, 'punch':1, 'secondary':1, 'snatch':1, 'sound':1};
|
|
var allStatus = {'psn':1, 'tox':1, 'brn':1, 'par':1, 'frz':1, 'slp':1};
|
|
var allVolatileStatus = {'flinch':1, 'confusion':1, 'partiallytrapped':1};
|
|
var allBoosts = {'hp':1, 'atk':1, 'def':1, 'spa':1, 'spd':1, 'spe':1, 'accuracy':1, 'evasion':1};
|
|
var showAll = false;
|
|
var lsetData = {};
|
|
var targetMon = '';
|
|
|
|
for (var i = 0; i < targets.length; i++) {
|
|
var isNotSearch = false;
|
|
target = targets[i].toLowerCase().trim();
|
|
if (target.charAt(0) === '!') {
|
|
isNotSearch = true;
|
|
target = target.substr(1);
|
|
}
|
|
|
|
var typeIndex = target.indexOf(' type');
|
|
if (typeIndex >= 0) {
|
|
target = target.charAt(0).toUpperCase() + target.substring(1, typeIndex);
|
|
if (!(target in Tools.data.TypeChart)) return this.sendReplyBox("Type '" + Tools.escapeHTML(target) + "' not found.");
|
|
if (!searches['type']) searches['type'] = {};
|
|
if ((searches['type'][target] && isNotSearch) || (searches['type'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a type.');
|
|
searches['type'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target in allCategories) {
|
|
target = target.charAt(0).toUpperCase() + target.substr(1);
|
|
if (!searches['category']) searches['category'] = {};
|
|
if ((searches['category'][target] && isNotSearch) || (searches['category'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a category.');
|
|
searches['category'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target === 'bypassessubstitute') target = 'authentic';
|
|
if (target in allFlags) {
|
|
if (!searches['flags']) searches['flags'] = {};
|
|
if ((searches['flags'][target] && isNotSearch) || (searches['flags'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include \'' + target + '\'.');
|
|
searches['flags'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target === 'all') {
|
|
if (this.broadcasting) return this.sendReplyBox("A search with the parameter 'all' cannot be broadcast.");
|
|
showAll = true;
|
|
continue;
|
|
}
|
|
|
|
if (target === 'recovery') {
|
|
if (!searches['recovery']) {
|
|
searches['recovery'] = !isNotSearch;
|
|
} else if ((searches['recovery'] && isNotSearch) || (searches['recovery'] === false && !isNotSearch)) {
|
|
return this.sendReplyBox('A search cannot both exclude and include recovery moves.');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var template = Tools.getTemplate(target);
|
|
if (template.exists) {
|
|
if (Object.size(lsetData) !== 0) return this.sendReplyBox("A search can only include one Pok\u00e9mon learnset.");
|
|
if (!template.learnset) template = Tools.getTemplate(template.baseSpecies);
|
|
lsetData = template.learnset;
|
|
targetMon = template.name;
|
|
while (template.prevo) {
|
|
template = Tools.getTemplate(template.prevo);
|
|
for (var move in template.learnset) {
|
|
if (!lsetData[move]) lsetData[move] = template.learnset[move];
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var inequality = target.search(/>|<|=/);
|
|
if (inequality >= 0) {
|
|
if (isNotSearch) return this.sendReplyBox("You cannot use the negation symbol '!' in quality ranges.");
|
|
inequality = target.charAt(inequality);
|
|
var targetParts = target.replace(/\s/g, '').split(inequality);
|
|
var numSide, propSide, direction;
|
|
if (!isNaN(targetParts[0])) {
|
|
numSide = 0;
|
|
propSide = 1;
|
|
switch (inequality) {
|
|
case '>': direction = 'less'; break;
|
|
case '<': direction = 'greater'; break;
|
|
case '=': direction = 'equal'; break;
|
|
}
|
|
} else if (!isNaN(targetParts[1])) {
|
|
numSide = 1;
|
|
propSide = 0;
|
|
switch (inequality) {
|
|
case '<': direction = 'less'; break;
|
|
case '>': direction = 'greater'; break;
|
|
case '=': direction = 'equal'; break;
|
|
}
|
|
} else {
|
|
return this.sendReplyBox("No value given to compare with '" + Tools.escapeHTML(target) + "'.");
|
|
}
|
|
var prop = targetParts[propSide];
|
|
switch (toId(targetParts[propSide])) {
|
|
case 'basepower': prop = 'basePower'; break;
|
|
case 'bp': prop = 'basePower'; break;
|
|
case 'acc': prop = 'accuracy'; break;
|
|
}
|
|
if (!(prop in allProperties)) return this.sendReplyBox("'" + Tools.escapeHTML(target) + "' did not contain a valid property.");
|
|
if (!searches['property']) searches['property'] = {};
|
|
if (direction === 'equal') {
|
|
if (searches['property'][prop]) return this.sendReplyBox("Invalid property range for " + prop + ".");
|
|
searches['property'][prop] = {};
|
|
searches['property'][prop]['less'] = parseFloat(targetParts[numSide]);
|
|
searches['property'][prop]['greater'] = parseFloat(targetParts[numSide]);
|
|
} else {
|
|
if (!searches['property'][prop]) searches['property'][prop] = {};
|
|
if (searches['property'][prop][direction]) {
|
|
return this.sendReplyBox("Invalid property range for " + prop + ".");
|
|
} else {
|
|
searches['property'][prop][direction] = parseFloat(targetParts[numSide]);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (target.substr(0, 8) === 'priority') {
|
|
var sign = '';
|
|
target = target.substr(8).trim();
|
|
if (target === "+") {
|
|
sign = 'greater';
|
|
} else if (target === "-") {
|
|
sign = 'less';
|
|
} else {
|
|
return this.sendReplyBox("Priority type '" + target + "' not recognized.");
|
|
}
|
|
if (!searches['property']) searches['property'] = {};
|
|
if (searches['property']['priority']) {
|
|
return this.sendReplyBox("Priority cannot be set with both shorthand and inequality range.");
|
|
} else {
|
|
searches['property']['priority'] = {};
|
|
searches['property']['priority'][sign] = (sign === 'less' ? -1 : 1);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (target.substr(0, 7) === 'boosts ') {
|
|
switch (target.substr(7)) {
|
|
case 'attack': target = 'atk'; break;
|
|
case 'defense': target = 'def'; break;
|
|
case 'specialattack': target = 'spa'; break;
|
|
case 'spatk': target = 'spa'; break;
|
|
case 'specialdefense': target = 'spd'; break;
|
|
case 'spdef': target = 'spd'; break;
|
|
case 'speed': target = 'spe'; break;
|
|
case 'acc': target = 'accuracy'; break;
|
|
case 'evasiveness': target = 'evasion'; break;
|
|
default: target = target.substr(7);
|
|
}
|
|
if (!(target in allBoosts)) return this.sendReplyBox("'" + Tools.escapeHTML(target.substr(7)) + "' is not a recognized stat.");
|
|
if (!searches['boost']) searches['boost'] = {};
|
|
if ((searches['boost'][target] && isNotSearch) || (searches['boost'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a stat boost.');
|
|
searches['boost'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
var oldTarget = target;
|
|
if (target.charAt(target.length - 1) === 's') target = target.substr(0, target.length - 1);
|
|
switch (target) {
|
|
case 'toxic': target = 'tox'; break;
|
|
case 'poison': target = 'psn'; break;
|
|
case 'burn': target = 'brn'; break;
|
|
case 'paralyze': target = 'par'; break;
|
|
case 'freeze': target = 'frz'; break;
|
|
case 'sleep': target = 'slp'; break;
|
|
case 'confuse': target = 'confusion'; break;
|
|
case 'trap': target = 'partiallytrapped'; break;
|
|
case 'flinche': target = 'flinch'; break;
|
|
}
|
|
|
|
if (target in allStatus) {
|
|
if (!searches['status']) searches['status'] = {};
|
|
if ((searches['status'][target] && isNotSearch) || (searches['status'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a status.');
|
|
searches['status'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
if (target in allVolatileStatus) {
|
|
if (!searches['volatileStatus']) searches['volatileStatus'] = {};
|
|
if ((searches['volatileStatus'][target] && isNotSearch) || (searches['volatileStatus'][target] === false && !isNotSearch)) return this.sendReplyBox('A search cannot both exclude and include a volitile status.');
|
|
searches['volatileStatus'][target] = !isNotSearch;
|
|
continue;
|
|
}
|
|
|
|
return this.sendReplyBox("'" + Tools.escapeHTML(oldTarget) + "' could not be found in any of the search categories.");
|
|
}
|
|
|
|
if (showAll && Object.size(searches) === 0 && !targetMon) return this.sendReplyBox("No search parameters other than 'all' were found. Try '/help movesearch' for more information on this command.");
|
|
|
|
var dex = {};
|
|
if (targetMon) {
|
|
for (var move in lsetData) {
|
|
dex[move] = Tools.getMove(move);
|
|
}
|
|
} else {
|
|
for (var move in Tools.data.Movedex) {
|
|
dex[move] = Tools.getMove(move);
|
|
}
|
|
delete dex.magikarpsrevenge;
|
|
}
|
|
|
|
for (var search in searches) {
|
|
switch (search) {
|
|
case 'type':
|
|
case 'category':
|
|
for (var move in dex) {
|
|
if (searches[search][String(dex[move][search])] === false ||
|
|
Object.count(searches[search], true) > 0 && !searches[search][String(dex[move][search])]) {
|
|
delete dex[move];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'flags':
|
|
for (var flag in searches[search]) {
|
|
for (var move in dex) {
|
|
if (flag !== 'secondary') {
|
|
if ((!dex[move].flags[flag] && searches[search][flag]) || (dex[move].flags[flag] && !searches[search][flag])) delete dex[move];
|
|
} else {
|
|
if (searches[search][flag]) {
|
|
if (!dex[move].secondary && !dex[move].secondaries) delete dex[move];
|
|
} else {
|
|
if (dex[move].secondary && dex[move].secondaries) delete dex[move];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'recovery':
|
|
for (var move in dex) {
|
|
var hasRecovery = (dex[move].drain || dex[move].flags.heal);
|
|
if ((!hasRecovery && searches[search]) || (hasRecovery && !searches[search])) delete dex[move];
|
|
}
|
|
break;
|
|
|
|
case 'property':
|
|
for (var prop in searches[search]) {
|
|
for (var move in dex) {
|
|
if (typeof searches[search][prop].less === "number") {
|
|
if (dex[move][prop] === true) {
|
|
delete dex[move];
|
|
continue;
|
|
}
|
|
if (dex[move][prop] > searches[search][prop].less) {
|
|
delete dex[move];
|
|
continue;
|
|
}
|
|
}
|
|
if (typeof searches[search][prop].greater === "number") {
|
|
if (dex[move][prop] === true) {
|
|
if (dex[move].category === "Status") delete dex[move];
|
|
continue;
|
|
}
|
|
if (dex[move][prop] < searches[search][prop].greater) {
|
|
delete dex[move];
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'boost':
|
|
for (var boost in searches[search]) {
|
|
for (var move in dex) {
|
|
if (dex[move].boosts) {
|
|
if ((dex[move].boosts[boost] > 0 && searches[search][boost]) ||
|
|
(dex[move].boosts[boost] < 1 && !searches[search][boost])) continue;
|
|
} else if (dex[move].secondary && dex[move].secondary.self && dex[move].secondary.self.boosts) {
|
|
if ((dex[move].secondary.self.boosts[boost] > 0 && searches[search][boost]) ||
|
|
(dex[move].secondary.self.boosts[boost] < 1 && !searches[search][boost])) continue;
|
|
}
|
|
delete dex[move];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'status':
|
|
case 'volatileStatus':
|
|
for (var searchStatus in searches[search]) {
|
|
for (var move in dex) {
|
|
if (dex[move][search] !== searchStatus) {
|
|
if (!dex[move].secondaries) {
|
|
if (!dex[move].secondary) {
|
|
if (searches[search][searchStatus]) delete dex[move];
|
|
} else {
|
|
if ((dex[move].secondary[search] !== searchStatus && searches[search][searchStatus]) ||
|
|
(dex[move].secondary[search] === searchStatus && !searches[search][searchStatus])) delete dex[move];
|
|
}
|
|
} else {
|
|
var hasSecondary = false;
|
|
for (var i = 0; i < dex[move].secondaries.length; i++) {
|
|
if (dex[move].secondaries[i][search] === searchStatus) hasSecondary = true;
|
|
}
|
|
if ((!hasSecondary && searches[search][searchStatus]) || (hasSecondary && !searches[search][searchStatus])) delete dex[move];
|
|
}
|
|
} else {
|
|
if (!searches[search][searchStatus]) delete dex[move];
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Error("/movesearch search category '" + search + "' was unrecognised.");
|
|
}
|
|
}
|
|
|
|
var results = [];
|
|
for (var move in dex) {
|
|
results.push(dex[move].name);
|
|
}
|
|
|
|
var resultsStr = "";
|
|
if (targetMon) {
|
|
resultsStr += "<font color=#999999>Matching moves found in learnset for</font> " + targetMon + ":<br>";
|
|
} else {
|
|
resultsStr += this.broadcasting ? "" : ("<font color=#999999>" + message + ":</font><br>");
|
|
}
|
|
if (results.length > 0) {
|
|
if (showAll || results.length <= RESULTS_MAX_LENGTH + 5) {
|
|
results.sort();
|
|
resultsStr += results.join(", ");
|
|
} else {
|
|
resultsStr += results.slice(0, RESULTS_MAX_LENGTH).join(", ") + ", and " + (results.length - RESULTS_MAX_LENGTH) + " more. <font color=#999999>Redo the search with 'all' as a search parameter to show all results.</font>";
|
|
}
|
|
} else {
|
|
resultsStr += "No moves found.";
|
|
}
|
|
return this.sendReplyBox(resultsStr);
|
|
},
|
|
movesearchhelp: ["/movesearch [parameter], [parameter], [parameter], ... - Searches for moves that fulfill the selected criteria.",
|
|
"Search categories are: type, category, flag, status inflicted, type boosted, and numeric range for base power, pp, and accuracy.",
|
|
"Types must be followed by ' type', e.g., 'dragon type'.",
|
|
"Stat boosts must be preceded with 'boosts ', e.g., 'boosts attack' searches for moves that boost the attack stat.",
|
|
"Inequality ranges use the characters '>' and '<' though they behave as '≥' and '≤', e.g., 'bp > 100' searches for all moves equal to and greater than 100 base power.",
|
|
"Parameters can be excluded through the use of '!', e.g., !water type' excludes all water type moves.",
|
|
"Valid flags are: authentic (bypasses substitute), bite, bullet, contact, defrost, powder, pulse, punch, secondary, snatch, sound",
|
|
"If a Pok\u00e9mon is included as a parameter, moves will be searched from it's movepool.",
|
|
"The order of the parameters does not matter."],
|
|
|
|
itemsearch: function (target, room, user, connection, cmd, message) {
|
|
if (!target) return this.parse('/help itemsearch');
|
|
if (!this.canBroadcast()) return;
|
|
|
|
var showAll = false;
|
|
|
|
target = target.trim();
|
|
if (target.substr(target.length - 5) === ', all' || target.substr(target.length - 4) === ',all') {
|
|
showAll = true;
|
|
target = target.substr(0, target.length - 5);
|
|
}
|
|
|
|
target = target.toLowerCase().replace('-', ' ').replace(/[^a-z0-9.\s\/]/g, '');
|
|
var rawSearch = target.split(' ');
|
|
var searchedWords = [];
|
|
var foundItems = [];
|
|
|
|
//refine searched words
|
|
for (var i = 0; i < rawSearch.length; i++) {
|
|
var newWord = rawSearch[i].trim();
|
|
if (isNaN(newWord)) newWord = newWord.replace('.', '');
|
|
switch (newWord) {
|
|
// words that don't really help identify item removed to speed up search
|
|
case 'a':
|
|
case 'an':
|
|
case 'is':
|
|
case 'it':
|
|
case 'its':
|
|
case 'the':
|
|
case 'that':
|
|
case 'which':
|
|
case 'user':
|
|
case 'holder':
|
|
case 'holders':
|
|
newWord = '';
|
|
break;
|
|
// replace variations of common words with standardized versions
|
|
case 'opponent': newWord = 'attacker'; break;
|
|
case 'flung': newWord = 'fling'; break;
|
|
case 'heal': case 'heals':
|
|
case 'recovers': newWord = 'restores'; break;
|
|
case 'boost':
|
|
case 'boosts': newWord = 'raises'; break;
|
|
case 'weakens': newWord = 'halves'; break;
|
|
case 'more': newWord = 'increases'; break;
|
|
case 'super':
|
|
if (rawSearch[i + 1] === 'effective') {
|
|
newWord = 'supereffective';
|
|
rawSearch.splice(i + 1, 1);
|
|
}
|
|
break;
|
|
case 'special': newWord = 'sp'; break;
|
|
case 'spa':
|
|
newWord = 'sp';
|
|
rawSearch.splice(i, 0, 'atk');
|
|
break;
|
|
case 'atk':
|
|
case 'attack':
|
|
if (rawSearch[i - 1] === 'sp') {
|
|
newWord = 'atk';
|
|
} else {
|
|
newWord = 'attack';
|
|
}
|
|
break;
|
|
case 'spd':
|
|
newWord = 'sp';
|
|
rawSearch.splice(i, 0, 'def');
|
|
break;
|
|
case 'def':
|
|
case 'defense':
|
|
if (rawSearch[i - 1] === 'sp') {
|
|
newWord = 'def';
|
|
} else {
|
|
newWord = 'defense';
|
|
}
|
|
break;
|
|
case 'burns': newWord = 'burn'; break;
|
|
case 'poisons': newWord = 'poison'; break;
|
|
default:
|
|
if (/x[\d\.]+/.test(newWord)) {
|
|
newWord = newWord.substr(1) + 'x';
|
|
}
|
|
}
|
|
if (!newWord || searchedWords.indexOf(newWord) >= 0) continue;
|
|
searchedWords.push(newWord);
|
|
}
|
|
|
|
if (searchedWords.length === 0) return this.sendReplyBox("No distinguishing words were used. Try a more specific search.");
|
|
|
|
if (searchedWords.indexOf('fling') >= 0) {
|
|
var basePower = 0;
|
|
var effect;
|
|
|
|
for (var k = 0; k < searchedWords.length; k++) {
|
|
var wordEff = "";
|
|
switch (searchedWords[k]) {
|
|
case 'burn': case 'burns':
|
|
case 'brn': wordEff = 'brn'; break;
|
|
case 'paralyze': case 'paralyzes':
|
|
case 'par': wordEff = 'par'; break;
|
|
case 'poison': case 'poisons':
|
|
case 'psn': wordEff = 'psn'; break;
|
|
case 'toxic':
|
|
case 'tox': wordEff = 'tox'; break;
|
|
case 'flinches':
|
|
case 'flinch': wordEff = 'flinch'; break;
|
|
case 'badly': wordEff = 'tox'; break;
|
|
}
|
|
if (wordEff && effect) {
|
|
if (!(wordEff === 'psn' && effect === 'tox')) return this.sendReplyBox("Only specify fling effect once.");
|
|
} else if (wordEff) {
|
|
effect = wordEff;
|
|
} else {
|
|
if (searchedWords[k].substr(searchedWords[k].length - 2) === 'bp' && searchedWords[k].length > 2) searchedWords[k] = searchedWords[k].substr(0, searchedWords[k].length - 2);
|
|
if (Number.isInteger(Number(searchedWords[k]))) {
|
|
if (basePower) return this.sendReplyBox("Only specify a number for base power once.");
|
|
basePower = parseInt(searchedWords[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var n in Tools.data.Items) {
|
|
var item = Tools.getItem(n);
|
|
if (!item.fling) continue;
|
|
|
|
if (basePower && effect) {
|
|
if (item.fling.basePower === basePower &&
|
|
(item.fling.status === effect || item.fling.volatileStatus === effect)) foundItems.push(item.name);
|
|
} else if (basePower) {
|
|
if (item.fling.basePower === basePower) foundItems.push(item.name);
|
|
} else {
|
|
if (item.fling.status === effect || item.fling.volatileStatus === effect) foundItems.push(item.name);
|
|
}
|
|
}
|
|
if (foundItems.length === 0) return this.sendReplyBox('No items inflict ' + basePower + 'bp damage when used with Fling.');
|
|
} else if (target.search(/natural ?gift/i) >= 0) {
|
|
var basePower = 0;
|
|
var type = "";
|
|
|
|
for (var k = 0; k < searchedWords.length; k++) {
|
|
searchedWords[k] = searchedWords[k].capitalize();
|
|
if (searchedWords[k] in Tools.data.TypeChart) {
|
|
if (type) return this.sendReplyBox("Only specify natural gift type once.");
|
|
type = searchedWords[k];
|
|
} else {
|
|
if (searchedWords[k].substr(searchedWords[k].length - 2) === 'bp' && searchedWords[k].length > 2) searchedWords[k] = searchedWords[k].substr(0, searchedWords[k].length - 2);
|
|
if (Number.isInteger(Number(searchedWords[k]))) {
|
|
if (basePower) return this.sendReplyBox("Only specify a number for base power once.");
|
|
basePower = parseInt(searchedWords[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var n in Tools.data.Items) {
|
|
var item = Tools.getItem(n);
|
|
if (!item.isBerry) continue;
|
|
|
|
if (basePower && type) {
|
|
if (item.naturalGift.basePower === basePower && item.naturalGift.type === type) foundItems.push(item.name);
|
|
} else if (basePower) {
|
|
if (item.naturalGift.basePower === basePower) foundItems.push(item.name);
|
|
} else {
|
|
if (item.naturalGift.type === type) foundItems.push(item.name);
|
|
}
|
|
}
|
|
if (foundItems.length === 0) return this.sendReplyBox('No berries inflict ' + basePower + 'bp damage when used with Natural Gift.');
|
|
} else {
|
|
var bestMatched = 0;
|
|
for (var n in Tools.data.Items) {
|
|
var item = Tools.getItem(n);
|
|
var matched = 0;
|
|
// splits words in the description into a toId()-esk format except retaining / and . in numbers
|
|
var descWords = item.desc;
|
|
// add more general quantifier words to descriptions
|
|
if (/[1-9\.]+x/.test(descWords)) descWords += ' increases';
|
|
if (item.isBerry) descWords += ' berry';
|
|
descWords = descWords.replace(/super[\-\s]effective/g, 'supereffective');
|
|
descWords = descWords.toLowerCase().replace('-', ' ').replace(/[^a-z0-9\s\/]/g, '').replace(/(\D)\./, function (p0, p1) { return p1; }).split(' ');
|
|
|
|
for (var k = 0; k < searchedWords.length; k++) {
|
|
if (descWords.indexOf(searchedWords[k]) >= 0) matched++;
|
|
}
|
|
|
|
if (matched >= bestMatched && matched >= (searchedWords.length * 3 / 5)) foundItems.push(item.name);
|
|
if (matched > bestMatched) bestMatched = matched;
|
|
}
|
|
|
|
// iterate over found items again to make sure they all are the best match
|
|
for (var l = 0; l < foundItems.length; l++) {
|
|
var item = Tools.getItem(foundItems[l]);
|
|
var matched = 0;
|
|
var descWords = item.desc;
|
|
if (/[1-9\.]+x/.test(descWords)) descWords += ' increases';
|
|
if (item.isBerry) descWords += ' berry';
|
|
descWords = descWords.replace(/super[\-\s]effective/g, 'supereffective');
|
|
descWords = descWords.toLowerCase().replace('-', ' ').replace(/[^a-z0-9\s\/]/g, '').replace(/(\D)\./, function (p0, p1) { return p1; }).split(' ');
|
|
|
|
for (var k = 0; k < searchedWords.length; k++) {
|
|
if (descWords.indexOf(searchedWords[k]) >= 0) matched++;
|
|
}
|
|
|
|
if (matched !== bestMatched) {
|
|
foundItems.splice(l, 1);
|
|
l--;
|
|
}
|
|
}
|
|
}
|
|
|
|
var resultsStr = this.broadcasting ? "" : ("<font color=#999999>" + message + ":</font><br>");
|
|
if (foundItems.length > 0) {
|
|
if (showAll || foundItems.length <= RESULTS_MAX_LENGTH + 5) {
|
|
foundItems.sort();
|
|
resultsStr += foundItems.join(", ");
|
|
} else {
|
|
resultsStr += foundItems.slice(0, RESULTS_MAX_LENGTH).join(", ") + ", and " + (foundItems.length - RESULTS_MAX_LENGTH) + " more. <font color=#999999>Redo the search with ', all' at the end to show all results.</font>";
|
|
}
|
|
} else {
|
|
resultsStr += "No items found. Try a more general search";
|
|
}
|
|
return this.sendReplyBox(resultsStr);
|
|
},
|
|
itemsearchhelp: ["/itemsearch [move description] - finds items that match the given key words.",
|
|
"Command accepts natural language. (tip: fewer words tend to work better)",
|
|
"Searches with \"fling\" in them will find items with the specified Fling behavior.",
|
|
"Searches with \"natural gift\" in them will find items with the specified Natural Gift behavior."],
|
|
|
|
learnset: 'learn',
|
|
learnall: 'learn',
|
|
learn5: 'learn',
|
|
g6learn: 'learn',
|
|
rbylearn: 'learn',
|
|
gsclearn: 'learn',
|
|
advlearn: 'learn',
|
|
dpplearn: 'learn',
|
|
bw2learn: 'learn',
|
|
learn: function (target, room, user, connection, cmd) {
|
|
if (!target) return this.parse('/help learn');
|
|
|
|
if (!this.canBroadcast()) return;
|
|
|
|
var lsetData = {set:{}};
|
|
var targets = target.split(',');
|
|
var template = Tools.getTemplate(targets[0]);
|
|
var move = {};
|
|
var problem;
|
|
var format = {rby:'gen1ou', gsc:'gen2ou', adv:'gen3ou', dpp:'gen4ou', bw2:'gen5ou'}[cmd.substring(0, 3)];
|
|
var all = (cmd === 'learnall');
|
|
if (cmd === 'learn5') lsetData.set.level = 5;
|
|
if (cmd === 'g6learn') lsetData.format = {noPokebank: true};
|
|
|
|
if (!template.exists) {
|
|
return this.sendReply("Pok\u00e9mon '" + template.id + "' not found.");
|
|
}
|
|
|
|
if (targets.length < 2) {
|
|
return this.sendReply("You must specify at least one move.");
|
|
}
|
|
|
|
for (var i = 1, len = targets.length; i < len; ++i) {
|
|
move = Tools.getMove(targets[i]);
|
|
if (!move.exists) {
|
|
return this.sendReply("Move '" + move.id + "' not found.");
|
|
}
|
|
problem = TeamValidator.checkLearnsetSync(format, move, template.species, lsetData);
|
|
if (problem) break;
|
|
}
|
|
var buffer = template.name + (problem ? " <span class=\"message-learn-cannotlearn\">can't</span> learn " : " <span class=\"message-learn-canlearn\">can</span> learn ") + (targets.length > 2 ? "these moves" : move.name);
|
|
if (format) buffer += ' on ' + cmd.substring(0, 3).toUpperCase();
|
|
if (!problem) {
|
|
var sourceNames = {E:"egg", S:"event", D:"dream world"};
|
|
if (lsetData.sources || lsetData.sourcesBefore) buffer += " only when obtained from:<ul class=\"message-learn-list\">";
|
|
if (lsetData.sources) {
|
|
var sources = lsetData.sources.sort();
|
|
var prevSource;
|
|
var prevSourceType;
|
|
var prevSourceCount = 0;
|
|
for (var i = 0, len = sources.length; i < len; ++i) {
|
|
var source = sources[i];
|
|
if (source.substr(0, 2) === prevSourceType) {
|
|
if (prevSourceCount < 0) {
|
|
buffer += ": " + source.substr(2);
|
|
} else if (all || prevSourceCount < 3) {
|
|
buffer += ", " + source.substr(2);
|
|
} else if (prevSourceCount === 3) {
|
|
buffer += ", ...";
|
|
}
|
|
++prevSourceCount;
|
|
continue;
|
|
}
|
|
prevSourceType = source.substr(0, 2);
|
|
prevSourceCount = source.substr(2) ? 0 : -1;
|
|
buffer += "<li>gen " + source.charAt(0) + " " + sourceNames[source.charAt(1)];
|
|
if (prevSourceType === '5E' && template.maleOnlyHidden) buffer += " (cannot have hidden ability)";
|
|
if (source.substr(2)) buffer += ": " + source.substr(2);
|
|
}
|
|
}
|
|
if (lsetData.sourcesBefore) {
|
|
if (!(cmd.substring(0, 3) in {'rby':1, 'gsc':1})) {
|
|
buffer += "<li>any generation before " + (lsetData.sourcesBefore + 1);
|
|
} else if (!lsetData.sources) {
|
|
buffer += "<li>gen " + lsetData.sourcesBefore;
|
|
}
|
|
}
|
|
buffer += "</ul>";
|
|
}
|
|
this.sendReplyBox(buffer);
|
|
},
|
|
learnhelp: ["/learn [pokemon], [move, move, ...] - Displays how a Pok\u00e9mon can learn the given moves, if it can at all.",
|
|
"!learn [pokemon], [move, move, ...] - Show everyone that information. Requires: + % @ # & ~"],
|
|
|
|
weaknesses: 'weakness',
|
|
weak: 'weakness',
|
|
resist: 'weakness',
|
|
weakness: function (target, room, user) {
|
|
if (!target) return this.parse('/help weakness');
|
|
if (!this.canBroadcast()) return;
|
|
target = target.trim();
|
|
var targets = target.split(/ ?[,\/ ] ?/);
|
|
|
|
var pokemon = Tools.getTemplate(target);
|
|
var type1 = Tools.getType(targets[0]);
|
|
var 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.");
|
|
}
|
|
|
|
var weaknesses = [];
|
|
var resistances = [];
|
|
var immunities = [];
|
|
Object.keys(Tools.data.TypeChart).forEach(function (type) {
|
|
var notImmune = Tools.getImmunity(type, pokemon);
|
|
if (notImmune) {
|
|
var typeMod = Tools.getEffectiveness(type, pokemon);
|
|
switch (typeMod) {
|
|
case 1:
|
|
weaknesses.push(type);
|
|
break;
|
|
case 2:
|
|
weaknesses.push("<b>" + type + "</b>");
|
|
break;
|
|
case -1:
|
|
resistances.push(type);
|
|
break;
|
|
case -2:
|
|
resistances.push("<b>" + type + "</b>");
|
|
break;
|
|
}
|
|
} else {
|
|
immunities.push(type);
|
|
}
|
|
});
|
|
|
|
var buffer = [];
|
|
buffer.push(pokemon.exists ? "" + target + ' (ignoring abilities):' : '' + target + ':');
|
|
buffer.push('<span class="message-effect-weak">Weaknesses</span>: ' + (weaknesses.join(', ') || '<font color=#999999>None</font>'));
|
|
buffer.push('<span class="message-effect-resist">Resistances</span>: ' + (resistances.join(', ') || '<font color=#999999>None</font>'));
|
|
buffer.push('<span class="message-effect-immune">Immunities</span>: ' + (immunities.join(', ') || '<font color=#999999>None</font>'));
|
|
this.sendReplyBox(buffer.join('<br>'));
|
|
},
|
|
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) {
|
|
var targets = target.split(/[,/]/).slice(0, 2);
|
|
if (targets.length !== 2) return this.sendReply("Attacker and defender must be separated with a comma.");
|
|
|
|
var searchMethods = {'getType':1, 'getMove':1, 'getTemplate':1};
|
|
var sourceMethods = {'getType':1, 'getMove':1};
|
|
var targetMethods = {'getType':1, 'getTemplate':1};
|
|
var source, defender, foundData, atkName, defName;
|
|
|
|
for (var i = 0; i < 2; ++i) {
|
|
var 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.canBroadcast()) return;
|
|
|
|
var factor = 0;
|
|
if (Tools.getImmunity(source, defender) || source.ignoreImmunity && (source.ignoreImmunity === true || source.ignoreImmunity[source.type])) {
|
|
var totalTypeMod = 0;
|
|
if (source.effectType !== 'Move' || source.category !== 'Status' && (source.basePower || source.basePowerCallback)) {
|
|
for (var i = 0; i < defender.types.length; i++) {
|
|
var baseMod = Tools.getEffectiveness(source, defender.types[i]);
|
|
var moveMod = source.onEffectiveness && source.onEffectiveness.call(Tools, baseMod, defender.types[i], source);
|
|
totalTypeMod += typeof moveMod === 'number' ? moveMod : baseMod;
|
|
}
|
|
}
|
|
factor = Math.pow(2, totalTypeMod);
|
|
}
|
|
|
|
var hasThousandArrows = source.id === 'thousandarrows' && defender.types.indexOf('Flying') >= 0;
|
|
var additionalInfo = hasThousandArrows ? "<br>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.canBroadcast()) return;
|
|
if (!target) return this.parse("/help coverage");
|
|
|
|
var targets = target.split(/[,+]/);
|
|
var sources = [];
|
|
|
|
var dispTable = false;
|
|
var bestCoverage = {};
|
|
var hasThousandArrows = false;
|
|
|
|
for (var type in Tools.data.TypeChart) {
|
|
// This command uses -5 to designate immunity
|
|
bestCoverage[type] = -5;
|
|
}
|
|
|
|
for (var i = 0; i < targets.length; i++) {
|
|
var move = targets[i].trim().capitalize();
|
|
if (move === 'Table' || move === 'All') {
|
|
if (this.broadcasting) return this.sendReplyBox("The full table cannot be broadcast.");
|
|
dispTable = true;
|
|
continue;
|
|
}
|
|
|
|
var eff;
|
|
if (move in Tools.data.TypeChart) {
|
|
sources.push(move);
|
|
for (var 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 (var type in bestCoverage) {
|
|
if (move.id === "struggle") {
|
|
eff = 0;
|
|
} else {
|
|
if (!Tools.getImmunity(move.type, type) && !move.ignoreImmunity) continue;
|
|
var baseMod = Tools.getEffectiveness(move, type);
|
|
var 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.sendReply("No moves using a type table for determining damage were specified.");
|
|
if (sources.length > 4) return this.sendReply("Specify a maximum of 4 moves or types.");
|
|
|
|
// converts to fractional effectiveness, 0 for immune
|
|
for (var type in bestCoverage) {
|
|
if (bestCoverage[type] === -5) {
|
|
bestCoverage[type] = 0;
|
|
continue;
|
|
}
|
|
bestCoverage[type] = Math.pow(2, bestCoverage[type]);
|
|
}
|
|
|
|
if (!dispTable) {
|
|
var buffer = [];
|
|
var superEff = [];
|
|
var neutral = [];
|
|
var resists = [];
|
|
var immune = [];
|
|
|
|
for (var 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('<b><font color=#559955>Super Effective</font></b>: ' + (superEff.join(', ') || '<font color=#999999>None</font>'));
|
|
buffer.push('<span class="message-effect-resist">Neutral</span>: ' + (neutral.join(', ') || '<font color=#999999>None</font>'));
|
|
buffer.push('<span class="message-effect-weak">Resists</span>: ' + (resists.join(', ') || '<font color=#999999>None</font>'));
|
|
buffer.push('<span class="message-effect-immune">Immunities</span>: ' + (immune.join(', ') || '<font color=#999999>None</font>'));
|
|
return this.sendReplyBox(buffer.join('<br>'));
|
|
} else {
|
|
var buffer = '<div class="scrollable"><table cellpadding="1" width="100%"><tr><th></th>';
|
|
var icon = {};
|
|
for (var type in Tools.data.TypeChart) {
|
|
icon[type] = '<img src="https://play.pokemonshowdown.com/sprites/types/' + type + '.png" width="32" height="14">';
|
|
// row of icons at top
|
|
buffer += '<th>' + icon[type] + '</th>';
|
|
}
|
|
buffer += '</tr>';
|
|
for (var type1 in Tools.data.TypeChart) {
|
|
// assembles the rest of the rows
|
|
buffer += '<tr><th>' + icon[type1] + '</th>';
|
|
for (var type2 in Tools.data.TypeChart) {
|
|
var typing;
|
|
var cell = '<th ';
|
|
var bestEff = -5;
|
|
if (type1 === type2) {
|
|
// when types are the same it's considered pure type
|
|
typing = type1;
|
|
bestEff = bestCoverage[type1];
|
|
} else {
|
|
typing = type1 + "/" + type2;
|
|
for (var i = 0; i < sources.length; i++) {
|
|
var move = sources[i];
|
|
|
|
var curEff = 0;
|
|
if ((!Tools.getImmunity((move.type || move), type1) || !Tools.getImmunity((move.type || move), type2)) && !move.ignoreImmunity) continue;
|
|
var baseMod = Tools.getEffectiveness(move, type1);
|
|
var moveMod = move.onEffectiveness && move.onEffectiveness.call(Tools, baseMod, type1, move);
|
|
curEff += typeof moveMod === 'number' ? moveMod : baseMod;
|
|
baseMod = Tools.getEffectiveness(move, type2);
|
|
moveMod = move.onEffectiveness && move.onEffectiveness.call(Tools, baseMod, type2, move);
|
|
curEff += typeof moveMod === 'number' ? moveMod : baseMod;
|
|
|
|
if (curEff > bestEff) bestEff = curEff;
|
|
}
|
|
if (bestEff === -5) {
|
|
bestEff = 0;
|
|
} else {
|
|
bestEff = Math.pow(2, bestEff);
|
|
}
|
|
}
|
|
switch (bestEff) {
|
|
case 0:
|
|
cell += 'bgcolor=#666666 title="' + typing + '"><font color=#000000>' + bestEff + '</font>';
|
|
break;
|
|
case 0.25:
|
|
case 0.5:
|
|
cell += 'bgcolor=#AA5544 title="' + typing + '"><font color=#660000>' + bestEff + '</font>';
|
|
break;
|
|
case 1:
|
|
cell += 'bgcolor=#6688AA title="' + typing + '"><font color=#000066>' + bestEff + '</font>';
|
|
break;
|
|
case 2:
|
|
case 4:
|
|
cell += 'bgcolor=#559955 title="' + typing + '"><font color=#003300>' + bestEff + '</font>';
|
|
break;
|
|
default:
|
|
throw new Error("/coverage effectiveness of " + bestEff + " from parameters: " + target);
|
|
}
|
|
cell += '</th>';
|
|
buffer += cell;
|
|
}
|
|
}
|
|
buffer += '</table></div>';
|
|
|
|
if (hasThousandArrows) {
|
|
buffer += "<br><b>Thousand Arrows has neutral type effectiveness on Flying-type Pokemon if not already smacked down.";
|
|
}
|
|
|
|
this.sendReplyBox('Coverage for ' + sources.join(' + ') + ':<br>' + 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."],
|
|
|
|
/*********************************************************
|
|
* Informational commands
|
|
*********************************************************/
|
|
|
|
uptime: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
var uptime = process.uptime();
|
|
var uptimeText;
|
|
if (uptime > 24 * 60 * 60) {
|
|
var uptimeDays = Math.floor(uptime / (24 * 60 * 60));
|
|
uptimeText = uptimeDays + " " + (uptimeDays === 1 ? "day" : "days");
|
|
var uptimeHours = Math.floor(uptime / (60 * 60)) - uptimeDays * 24;
|
|
if (uptimeHours) uptimeText += ", " + uptimeHours + " " + (uptimeHours === 1 ? "hour" : "hours");
|
|
} else {
|
|
uptimeText = uptime.seconds().duration();
|
|
}
|
|
this.sendReplyBox("Uptime: <b>" + uptimeText + "</b>");
|
|
},
|
|
|
|
groups: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"+ <b>Voice</b> - They can use ! commands like !groups, and talk during moderated chat<br />" +
|
|
"% <b>Driver</b> - The above, and they can mute. Global % can also lock users and check for alts<br />" +
|
|
"@ <b>Moderator</b> - The above, and they can ban users<br />" +
|
|
"& <b>Leader</b> - The above, and they can promote to moderator and force ties<br />" +
|
|
"# <b>Room Owner</b> - They are leaders of the room and can almost totally control it<br />" +
|
|
"~ <b>Administrator</b> - 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.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"Pokémon Showdown is open source:<br />" +
|
|
"- Language: JavaScript (Node.js or io.js)<br />" +
|
|
"- <a href=\"https://github.com/Zarel/Pokemon-Showdown/commits/master\">What's new?</a><br />" +
|
|
"- <a href=\"https://github.com/Zarel/Pokemon-Showdown\">Server source code</a><br />" +
|
|
"- <a href=\"https://github.com/Zarel/Pokemon-Showdown-Client\">Client source code</a>"
|
|
);
|
|
},
|
|
opensourcehelp: ["/opensource - Links to PS's source code repository.",
|
|
"!opensource - Show everyone that information. Requires: + % @ # & ~"],
|
|
|
|
staff: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/sim/staff_list\">Pokémon Showdown Staff List</a>");
|
|
},
|
|
|
|
forums: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/forums/forums/pok%C3%A9mon-showdown.209\">Pokémon Showdown Forums</a>");
|
|
},
|
|
|
|
suggestions: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/forums/threads/3534365/\">Make a suggestion for Pokémon Showdown</a>");
|
|
},
|
|
|
|
bugreport: 'bugs',
|
|
bugs: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
if (room.battle) {
|
|
this.sendReplyBox("<center><button name=\"saveReplay\"><i class=\"icon-upload\"></i> Save Replay</button> — <a href=\"https://www.smogon.com/forums/threads/3520646/\">Questions</a> — <a href=\"https://www.smogon.com/forums/threads/3469932/\">Bug Reports</a></center>");
|
|
} else {
|
|
this.sendReplyBox(
|
|
"Have a replay showcasing a bug on Pokémon Showdown?<br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/3520646/\">Questions</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/3469932/\">Bug Reports</a>"
|
|
);
|
|
}
|
|
},
|
|
|
|
avatars: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox("You can <button name=\"avatars\">change your avatar</button> by clicking on it in the <button name=\"openOptions\"><i class=\"icon-cog\"></i> Options</button> 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.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"New to competitive Pokémon?<br />" +
|
|
"- <a href=\"https://www.smogon.com/sim/ps_guide\">Beginner's Guide to Pokémon Showdown</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/dp/articles/intro_comp_pokemon\">An introduction to competitive Pokémon</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/bw/articles/bw_tiers\">What do 'OU', 'UU', etc mean?</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/xyhub/tiers\">What are the rules for each format? What is 'Sleep Clause'?</a>"
|
|
);
|
|
},
|
|
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.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"Welcome to Smogon's official simulator! The <a href=\"https://www.smogon.com/forums/forums/264\">Smogon Info / Intro Hub</a> can help you get integrated into the community.<br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/3526346\">Useful Smogon Info</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/3498332\">Tiering FAQ</a><br />"
|
|
);
|
|
},
|
|
|
|
calculator: 'calc',
|
|
calc: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"Pokémon Showdown! damage calculator. (Courtesy of Honko)<br />" +
|
|
"- <a href=\"https://pokemonshowdown.com/damagecalc/\">Damage Calculator</a>"
|
|
);
|
|
},
|
|
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.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"An introduction to the Create-A-Pokémon project:<br />" +
|
|
"- <a href=\"https://www.smogon.com/cap/\">CAP project website and description</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/48782/\">What Pokémon have been made?</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/forums/311\">Talk about the metagame here</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/3512318/\">Sample XY CAP teams</a>"
|
|
);
|
|
},
|
|
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.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"NEXT (also called Gen-NEXT) is a mod that makes changes to the game:<br />" +
|
|
"- <a href=\"https://github.com/Zarel/Pokemon-Showdown/blob/master/mods/gennext/README.md\">README: overview of NEXT</a><br />" +
|
|
"Example replays:<br />" +
|
|
"- <a href=\"https://replay.pokemonshowdown.com/gennextou-120689854\">Zergo vs Mr Weegle Snarf</a><br />" +
|
|
"- <a href=\"https://replay.pokemonshowdown.com/gennextou-130756055\">NickMP vs Khalogie</a>"
|
|
);
|
|
},
|
|
|
|
om: 'othermetas',
|
|
othermetas: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
target = toId(target);
|
|
var buffer = "";
|
|
var matched = false;
|
|
|
|
if (target === 'all' && this.broadcasting) {
|
|
return this.sendReplyBox("You cannot broadcast information about all Other Metagames at once.");
|
|
}
|
|
|
|
if (!target || target === 'all') {
|
|
matched = true;
|
|
buffer += "- <a href=\"https://www.smogon.com/tiers/om/\">Other Metagames Hub</a><br />";
|
|
buffer += "- <a href=\"https://www.smogon.com/forums/threads/3505031/\">Other Metagames Index</a><br />";
|
|
if (!target) return this.sendReplyBox(buffer);
|
|
}
|
|
var showMonthly = (target === 'all' || target === 'omofthemonth' || target === 'omotm' || target === 'month');
|
|
var monthBuffer = "- <a href=\"https://www.smogon.com/forums/threads/3541792/\">Other Metagame of the Month</a>";
|
|
|
|
if (target === 'all') {
|
|
// Display OMotM formats, with forum thread links as caption
|
|
this.parse('/formathelp omofthemonth');
|
|
if (showMonthly) this.sendReply('|raw|<center>' + monthBuffer + '</center>');
|
|
|
|
// Display the rest of OM formats, with OM hub/index forum links as caption
|
|
this.parse('/formathelp othermetagames');
|
|
return this.sendReply('|raw|<center>' + buffer + '</center>');
|
|
}
|
|
if (showMonthly) {
|
|
this.target = 'omofthemonth';
|
|
this.run('formathelp');
|
|
this.sendReply('|raw|<center>' + monthBuffer + '</center>');
|
|
} 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.canBroadcast()) return;
|
|
if (!target) {
|
|
return this.sendReplyBox(
|
|
"- <a href=\"https://www.smogon.com/tiers/\">Smogon Tiers</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/forums/threads/3498332/\">Tiering FAQ</a><br />" +
|
|
"- <a href=\"https://www.smogon.com/xyhub/tiers\">The banlists for each tier</a><br />" +
|
|
"<br /><em>Type /formatshelp <strong>[format|section]</strong> to get details about an available format or group of formats.</em>"
|
|
);
|
|
}
|
|
var targetId = toId(target);
|
|
if (targetId === 'ladder') targetId = 'search';
|
|
if (targetId === 'all') targetId = '';
|
|
|
|
var formatList;
|
|
var 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(function (formatid) {return Tools.data.Formats[formatid].effectType === 'Format';});
|
|
}
|
|
|
|
// Filter formats and group by section
|
|
var exactMatch = '';
|
|
var sections = {};
|
|
var totalMatches = 0;
|
|
for (var i = 0; i < formatList.length; i++) {
|
|
var format = Tools.getFormat(formatList[i]);
|
|
var 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) {
|
|
var format = Tools.getFormat(Object.values(sections)[0].formats[0]);
|
|
if (!format.desc) return this.sendReplyBox("No description found for this " + (format.gameType || "singles").capitalize() + " " + format.section + " format.");
|
|
return this.sendReplyBox(format.desc.join("<br />"));
|
|
}
|
|
|
|
// Build tables
|
|
var buf = [];
|
|
for (var sectionId in sections) {
|
|
if (exactMatch && sectionId !== exactMatch) continue;
|
|
buf.push("<h3>" + Tools.escapeHTML(sections[sectionId].name) + "</h3>");
|
|
buf.push("<table class=\"scrollable\" style=\"display:inline-block; max-height:200px; border:1px solid gray; border-collapse:collapse\" cellspacing=\"0\" cellpadding=\"5\"><thead><th style=\"border:1px solid gray\" >Name</th><th style=\"border:1px solid gray\" >Description</th></thead><tbody>");
|
|
for (var i = 0; i < sections[sectionId].formats.length; i++) {
|
|
var format = Tools.getFormat(sections[sectionId].formats[i]);
|
|
var mod = format.mod && format.mod !== 'base' ? " - " + Tools.escapeHTML(format.mod === format.id ? format.name : format.mod).capitalize() : "";
|
|
buf.push("<tr><td style=\"border:1px solid gray\">" + Tools.escapeHTML(format.name) + "</td><td style=\"border: 1px solid gray; margin-left:10px\">" + (format.desc ? format.desc.join("<br />") : "—") + "</td></tr>");
|
|
}
|
|
buf.push("</tbody></table>");
|
|
}
|
|
return this.sendReply("|raw|<center>" + buf.join("") + "</center>");
|
|
},
|
|
|
|
roomhelp: function (target, room, user) {
|
|
if (room.id === 'lobby' || room.battle) return this.sendReply("This command is too spammy for lobby/battles.");
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"Room drivers (%) can use:<br />" +
|
|
"- /warn OR /k <em>username</em>: warn a user and show the Pokémon Showdown rules<br />" +
|
|
"- /mute OR /m <em>username</em>: 7 minute mute<br />" +
|
|
"- /hourmute OR /hm <em>username</em>: 60 minute mute<br />" +
|
|
"- /unmute <em>username</em>: unmute<br />" +
|
|
"- /announce OR /wall <em>message</em>: make an announcement<br />" +
|
|
"- /modlog <em>username</em>: search the moderator log of the room<br />" +
|
|
"- /modnote <em>note</em>: adds a moderator note that can be read through modlog<br />" +
|
|
"<br />" +
|
|
"Room moderators (@) can also use:<br />" +
|
|
"- /roomban OR /rb <em>username</em>: bans user from the room<br />" +
|
|
"- /roomunban <em>username</em>: unbans user from the room<br />" +
|
|
"- /roomvoice <em>username</em>: appoint a room voice<br />" +
|
|
"- /roomdevoice <em>username</em>: remove a room voice<br />" +
|
|
"- /modchat <em>[off/autoconfirmed/+]</em>: set modchat level<br />" +
|
|
"<br />" +
|
|
"Room owners (#) can also use:<br />" +
|
|
"- /roomintro <em>intro</em>: sets the room introduction that will be displayed for all users joining the room<br />" +
|
|
"- /rules <em>rules link</em>: set the room rules link seen when using /rules<br />" +
|
|
"- /roommod, /roomdriver <em>username</em>: appoint a room moderator/driver<br />" +
|
|
"- /roomdemod, /roomdedriver <em>username</em>: remove a room moderator/driver<br />" +
|
|
"- /modchat <em>[%/@/#]</em>: set modchat level<br />" +
|
|
"- /declare <em>message</em>: make a large blue declaration to the room<br />" +
|
|
"- !htmlbox <em>HTML code</em>: broadcasts a box of HTML code to the room<br />" +
|
|
"- !showimage <em>[url], [width], [height]</em>: shows an image to the room<br />" +
|
|
"<br />" +
|
|
"More detailed help can be found in the <a href=\"https://www.smogon.com/sim/roomauth_guide\">roomauth guide</a><br />" +
|
|
"</div>"
|
|
);
|
|
},
|
|
|
|
restarthelp: function (target, room, user) {
|
|
if (room.id === 'lobby' && !this.can('lockdown')) return false;
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox(
|
|
"The server is restarting. Things to know:<br />" +
|
|
"- We wait a few minutes before restarting so people can finish up their battles<br />" +
|
|
"- The restart itself will take around 0.6 seconds<br />" +
|
|
"- Your ladder ranking and teams will not change<br />" +
|
|
"- We are restarting to update Pokémon Showdown to a newer version"
|
|
);
|
|
},
|
|
|
|
rule: 'rules',
|
|
rules: function (target, room, user) {
|
|
if (!target) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox("Please follow the rules:<br />" +
|
|
(room.rulesLink ? "- <a href=\"" + Tools.escapeHTML(room.rulesLink) + "\">" + Tools.escapeHTML(room.title) + " room rules</a><br />" : "") +
|
|
"- <a href=\"https://pokemonshowdown.com/rules\">" + (room.rulesLink ? "Global rules" : "Rules") + "</a>");
|
|
return;
|
|
}
|
|
if (!this.can('roommod', null, room)) return;
|
|
if (target.length > 100) {
|
|
return this.sendReply("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();
|
|
}
|
|
},
|
|
|
|
faq: function (target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
target = target.toLowerCase();
|
|
var buffer = "";
|
|
var matched = false;
|
|
|
|
if (target === 'all' && this.broadcasting) {
|
|
return this.sendReplyBox("You cannot broadcast all FAQs at once.");
|
|
}
|
|
|
|
if (!target || target === 'all') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq\">Frequently Asked Questions</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'elo') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#elo\">Why did this user gain or lose so many points?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'doubles' || target === 'triples' || target === 'rotation') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#doubles\">Can I play doubles/triples/rotation battles here?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'restarts') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#restarts\">Why is the server restarting?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'star' || target === 'player') {
|
|
matched = true;
|
|
buffer += '<a href="https://www.smogon.com/sim/faq#star">Why is there this star (★) in front of my username?</a><br />';
|
|
}
|
|
if (target === 'all' || target === 'staff') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/staff_faq\">Staff FAQ</a><br />";
|
|
}
|
|
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.<br />";
|
|
}
|
|
if (target === 'all' || target === 'customavatar' || target === 'ca') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#customavatar\">How can I get a custom avatar?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'pm' || target === 'msg' || target === 'w') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#pm\">How can I send a user a private message?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'challenge' || target === 'chall') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#challenge\">How can I battle a specific user?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'gxe') {
|
|
matched = true;
|
|
buffer += "<a href=\"https://www.smogon.com/sim/faq#gxe\">What does GXE mean?</a><br />";
|
|
}
|
|
if (target === 'all' || target === 'coil') {
|
|
matched = true;
|
|
buffer += "<a href=\"http://www.smogon.com/forums/threads/coil-explained.3508013\">What is COIL?</a><br />";
|
|
}
|
|
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.canBroadcast()) return;
|
|
|
|
var targets = target.split(',');
|
|
var pokemon = Tools.getTemplate(targets[0]);
|
|
var item = Tools.getItem(targets[0]);
|
|
var move = Tools.getMove(targets[0]);
|
|
var ability = Tools.getAbility(targets[0]);
|
|
var format = Tools.getFormat(targets[0]);
|
|
var atLeastOne = false;
|
|
var generation = (targets[1] || 'xy').trim().toLowerCase();
|
|
var genNumber = 6;
|
|
var 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';
|
|
if (pokemon.tier === 'CAP') return this.sendReply("CAP is not currently supported by Smogon Strategic Pokedex.");
|
|
|
|
var illegalStartNums = {'351':1, '421':1, '487':1, '555':1, '647':1, '648':1, '649':1, '681':1};
|
|
if (pokemon.isMega || pokemon.num in illegalStartNums) pokemon = Tools.getTemplate(pokemon.baseSpecies);
|
|
|
|
var formatName = extraFormat.name;
|
|
var 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 = '';
|
|
}
|
|
var speciesid = pokemon.speciesid;
|
|
// Special case for Meowstic-M
|
|
if (speciesid === 'meowstic') speciesid = 'meowsticm';
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/dex/" + generation + "/pokemon/" + speciesid + (formatId ? '/' + formatId : '') + "\">" + generation.toUpperCase() + " " + Tools.escapeHTML(formatName) + " " + pokemon.name + " analysis</a>, brought to you by <a href=\"https://www.smogon.com\">Smogon University</a>");
|
|
}
|
|
|
|
// Item
|
|
if (item.exists && genNumber > 1 && item.gen <= genNumber) {
|
|
atLeastOne = true;
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/dex/" + generation + "/items/" + item.id + "\">" + generation.toUpperCase() + " " + item.name + " item analysis</a>, brought to you by <a href=\"https://www.smogon.com\">Smogon University</a>");
|
|
}
|
|
|
|
// Ability
|
|
if (ability.exists && genNumber > 2 && ability.gen <= genNumber) {
|
|
atLeastOne = true;
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/dex/" + generation + "/abilities/" + ability.id + "\">" + generation.toUpperCase() + " " + ability.name + " ability analysis</a>, brought to you by <a href=\"https://www.smogon.com\">Smogon University</a>");
|
|
}
|
|
|
|
// Move
|
|
if (move.exists && move.gen <= genNumber) {
|
|
atLeastOne = true;
|
|
this.sendReplyBox("<a href=\"https://www.smogon.com/dex/" + generation + "/moves/" + toId(move.name) + "\">" + generation.toUpperCase() + " " + move.name + " move analysis</a>, brought to you by <a href=\"https://www.smogon.com\">Smogon University</a>");
|
|
}
|
|
|
|
// Format
|
|
if (format.id) {
|
|
var formatName = format.name;
|
|
var 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("<a href=\"https://www.smogon.com/dex/" + generation + "/formats/" + formatId + "\">" + generation.toUpperCase() + " " + Tools.escapeHTML(formatName) + " format analysis</a>, brought to you by <a href=\"https://www.smogon.com\">Smogon University</a>");
|
|
}
|
|
}
|
|
|
|
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: + % @ # & ~"],
|
|
|
|
register: function () {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox('You will be prompted to register upon winning a rated battle. Alternatively, there is a register button in the <button name="openOptions"><i class="icon-cog"></i> Options</button> 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("<div class=\"broadcast-blue\"><b>The Pokemon of the Day is now " + target + "!</b><br />This Pokemon will be guaranteed to show up in random battles.</div>");
|
|
this.logModCommand("The Pokemon of the Day was changed to " + target + " by " + user.name + ".");
|
|
} else {
|
|
if (Rooms.lobby) Rooms.lobby.addRaw("<div class=\"broadcast-blue\"><b>The Pokemon of the Day was removed!</b><br />No pokemon will be guaranteed in random battles.</div>");
|
|
this.logModCommand("The Pokemon 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.canBroadcast()) 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.
|
|
var maxDice = 40;
|
|
|
|
var diceQuantity = 1;
|
|
var 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 + ".");
|
|
}
|
|
var offset = 0;
|
|
var removeOutlier = 0;
|
|
|
|
var 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.sendReply("The specified offset must be an integer up to " + Number.MAX_SAFE_INTEGER + ".");
|
|
}
|
|
if (removeOutlier && diceQuantity <= 1) return this.sendReply("More than one dice should be rolled before removing outliers.");
|
|
target = target.slice(0, modifierData.index);
|
|
}
|
|
|
|
var diceFaces = 6;
|
|
if (target.length) {
|
|
diceFaces = Number(target);
|
|
if (!Number.isSafeInteger(diceFaces) || diceFaces <= 0) {
|
|
return this.sendReply("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.sendReply("The maximum sum of rolled dice must be lower or equal than " + Number.MAX_SAFE_INTEGER + ".");
|
|
}
|
|
}
|
|
|
|
var maxRoll = 0;
|
|
var minRoll = Number.MAX_SAFE_INTEGER;
|
|
|
|
var trackRolls = diceQuantity * (('' + diceFaces).length + 1) <= 60;
|
|
var rolls = [];
|
|
var rollSum = 0;
|
|
|
|
for (var i = 0; i < diceQuantity; ++i) {
|
|
var 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
|
|
|
|
var offsetFragment = "";
|
|
if (offset) offsetFragment += (offset > 0 ? "+" + offset : offset);
|
|
|
|
if (diceQuantity === 1) return this.sendReplyBox("Roll (1 - " + diceFaces + ")" + offsetFragment + ": " + rollSum);
|
|
|
|
var sumFragment = "<br />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) {
|
|
var options = target.split(',');
|
|
if (options.length < 2) return this.parse('/help pick');
|
|
if (!this.canBroadcast()) return false;
|
|
return this.sendReplyBox('<em>We randomly picked:</em> ' + Tools.escapeHTML(options.sample().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.canBroadcast()) return;
|
|
|
|
var targets = target.split(',');
|
|
if (targets.length !== 3) {
|
|
return this.parse('/help showimage');
|
|
}
|
|
|
|
this.sendReply('|raw|<img src="' + Tools.escapeHTML(targets[0]) + '" alt="" width="' + toId(targets[1]) + '" height="' + toId(targets[2]) + '" />');
|
|
},
|
|
showimagehelp: ["/showimage [url], [width], [height] - Show an image. Requires: # & ~"],
|
|
|
|
htmlbox: function (target, room, user, connection, cmd, message) {
|
|
if (!target) return this.parse('/help htmlbox');
|
|
if (!this.canHTML(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.canBroadcast('!htmlbox')) return;
|
|
}
|
|
|
|
this.sendReplyBox(target);
|
|
},
|
|
htmlboxhelp: ["/htmlbox [message] - Displays a message, parsing HTML code contained. Requires: ~ # with global authority"]
|
|
};
|
|
|
|
process.nextTick(function () {
|
|
// This slow operation is done *after* we start listening for connections
|
|
// to the server. Anybody who connects while data is loading will
|
|
// have to wait a couple seconds before they are able to join the server, but
|
|
// at least they probably won't receive a connection error message.
|
|
|
|
Tools.includeData();
|
|
});
|