/**
* Informational Commands
* Pokemon Showdown - https://pokemonshowdown.com/
*
* These are informational commands. For instance, you can define the command
* 'whois' here, then use it by typing /whois into Pokemon Showdown.
*
* For the API, see chat-plugins/COMMANDS.md
*
* @license MIT license
*/
'use strict';
const RESULTS_MAX_LENGTH = 10;
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;
let targetUser = this.targetUserOrSelf(target, user.group === ' ');
if (!targetUser) {
return this.errorReply("User " + this.targetUsername + " not found.");
}
let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
if (showAll && !user.confirmed && targetUser !== user) {
return this.errorReply("/alts - Access denied.");
}
let buf = '' + targetUser.group + '' + Tools.escapeHTML(targetUser.name) + ' ' + (!targetUser.connected ? ' (offline)' : '');
if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) {
buf += "
" + Config.groups[targetUser.group].name + " (" + targetUser.group + ")";
}
if (targetUser.isSysop) {
buf += "
(Pokémon Showdown System Operator)";
}
if (!targetUser.registered) {
buf += "
(Unregistered)";
}
let publicrooms = "";
let hiddenrooms = "";
let privaterooms = "";
for (let i in targetUser.roomCount) {
if (i === 'global') continue;
let targetRoom = Rooms.get(i);
let output = (targetRoom.auth && targetRoom.auth[targetUser.userid] ? targetRoom.auth[targetUser.userid] : '') + '' + i + '';
if (targetRoom.isPrivate === true) {
if (targetRoom.modjoin === '~') continue;
if (privaterooms) privaterooms += " | ";
privaterooms += output;
} else if (targetRoom.isPrivate) {
if (hiddenrooms) hiddenrooms += " | ";
hiddenrooms += output;
} else {
if (publicrooms) publicrooms += " | ";
publicrooms += output;
}
}
buf += '
Rooms: ' + (publicrooms || '(no public rooms)');
if (!showAll) {
return this.sendReplyBox(buf);
}
buf += '
';
if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) {
let alts = targetUser.getAlts(true);
let output = Object.keys(targetUser.prevNames).join(", ");
if (output) buf += "
Previous names: " + Tools.escapeHTML(output);
for (let j = 0; j < alts.length; ++j) {
let targetAlt = Users.get(alts[j]);
if (!targetAlt.named && !targetAlt.connected) continue;
if (targetAlt.group === '~' && user.group !== '~') continue;
buf += '
Alt: ' + Tools.escapeHTML(targetAlt.name) + '' + (!targetAlt.connected ? " (offline)" : "");
output = Object.keys(targetAlt.prevNames).join(", ");
if (output) buf += "
Previous names: " + output;
}
if (targetUser.locked) {
buf += '
Locked: ' + targetUser.locked;
switch (targetUser.locked) {
case '#dnsbl':
buf += " - IP is in a DNS-based blacklist";
break;
case '#range':
buf += " - IP or host is in a temporary range-lock";
break;
case '#hostfilter':
buf += " - host is permanently locked for being a proxy";
break;
}
}
if (targetUser.semilocked) {
buf += '
Semilocked: ' + targetUser.semilocked;
}
}
if ((user.can('ip', targetUser) || user === targetUser)) {
let ips = Object.keys(targetUser.ips);
buf += "
IP" + ((ips.length > 1) ? "s" : "") + ": " + ips.join(", ") +
(user.group !== ' ' && targetUser.latestHost ? "
Host: " + Tools.escapeHTML(targetUser.latestHost) : "");
}
if ((user === targetUser || user.can('alts', targetUser)) && hiddenrooms) {
buf += '
Hidden rooms: ' + hiddenrooms;
}
if ((user === targetUser || user.can('makeroom')) && privaterooms) {
buf += '
Private rooms: ' + privaterooms;
}
if (user.can('alts', targetUser) || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.userid in room.users)) {
let bannedFrom = "";
for (let i = 0; i < Rooms.global.chatRooms.length; i++) {
let thisRoom = Rooms.global.chatRooms[i];
if (!thisRoom || thisRoom.isPrivate === true) continue;
if (thisRoom.bannedIps && (targetUser.latestIp in thisRoom.bannedIps || targetUser.userid in thisRoom.bannedUsers)) {
if (bannedFrom) bannedFrom += ", ";
bannedFrom += '' + thisRoom + '';
}
}
if (bannedFrom) buf += '
Banned from: ' + bannedFrom;
}
this.sendReplyBox(buf);
},
whoishelp: ["/whois - Get details on yourself: alts, group, IP address, and rooms.",
"/whois [username] - Get details on a username: alts (Requires: % @ & ~), group, IP address (Requires: @ & ~), and rooms."],
host: function (target, room, user, connection, cmd) {
if (!target) return this.parse('/help host');
if (!this.can('rangeban')) return;
if (!/[0-9.]+/.test(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.');
let 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: & ~"],
searchip: 'ipsearch',
ipsearchall: 'ipsearch',
hostsearch: 'ipsearch',
ipsearch: function (target, room, user, connection, cmd) {
if (!target.trim()) return this.parse('/help ipsearch');
if (!this.can('rangeban')) return;
let results = [];
let isAll = (cmd === 'ipsearchall');
if (/[a-z]/.test(target)) {
// host
this.sendReply("Users with host " + target + ":");
Users.users.forEach(function (curUser) {
if (results.length > 100 && !isAll) return;
if (!curUser.latestHost || !curUser.latestHost.endsWith(target)) return;
results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
});
if (results.length > 100 && !isAll) {
return this.sendReply("More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.");
}
} else if (target.slice(-1) === '*') {
// IP range
this.sendReply("Users in IP range " + target + ":");
target = target.slice(0, -1);
Users.users.forEach(function (curUser) {
if (results.length > 100 && !isAll) return;
if (!curUser.latestIp.startsWith(target)) return;
results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
});
if (results.length > 100 && !isAll) {
return this.sendReply("More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.");
}
} else {
this.sendReply("Users with IP " + target + ":");
Users.users.forEach(function (curUser) {
if (curUser.latestIp === target) {
results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
}
});
}
if (!results.length) return this.errorReply("No results found.");
return this.sendReply(results.join('; '));
},
ipsearchhelp: ["/ipsearch [ip|range|host] - Find all users with specified IP, IP range, or host. Requires: & ~"],
/*********************************************************
* Shortcuts
*********************************************************/
inv: 'invite',
invite: function (target, room, user) {
if (!target) return this.parse('/help invite');
target = this.splitTarget(target);
if (!this.targetUser) {
return this.errorReply("User " + this.targetUsername + " not found.");
}
let targetRoom = (target ? Rooms.search(target) : room);
if (!targetRoom) {
return this.errorReply("Room " + target + " not found.");
}
return this.parse('/msg ' + this.targetUsername + ', /invite ' + targetRoom.id);
},
invitehelp: ["/invite [username], [roomname] - Invites the player [username] to join the room [roomname]."],
/*********************************************************
* Data Search Tools
*********************************************************/
pstats: 'data',
stats: 'data',
dex: 'data',
pokedex: 'data',
data: function (target, room, user, connection, cmd) {
if (!this.canBroadcast()) return;
let buffer = '';
let targetId = toId(target);
if (!targetId) return this.parse('/help data');
let targetNum = parseInt(targetId, 10);
if (!isNaN(targetNum)) {
for (let p in Tools.data.Pokedex) {
let pokemon = Tools.getTemplate(p);
if (pokemon.num === targetNum) {
target = pokemon.species;
targetId = pokemon.id;
break;
}
}
}
let newTargets = Tools.dataSearch(target);
let showDetails = (cmd === 'dt' || cmd === 'details');
if (newTargets && newTargets.length) {
for (let i = 0; i < newTargets.length; ++i) {
if (!newTargets[i].exactMatch && !i) {
buffer = "No Pok\u00e9mon, item, move, ability or nature named '" + target + "' was found. Showing the data of '" + newTargets[0].name + "' instead.\n";
}
if (newTargets[i].searchType === 'nature') {
let nature = Tools.getNature(newTargets[i].name);
buffer += "" + nature.name + " nature: ";
if (nature.plus) {
let statNames = {'atk': "Attack", 'def': "Defense", 'spa': "Special Attack", 'spd': "Special Defense", 'spe': "Speed"};
buffer += "+10% " + statNames[nature.plus] + ", -10% " + statNames[nature.minus] + ".";
} else {
buffer += "No effect.";
}
return this.sendReply(buffer);
} else {
buffer += '|c|~|/data-' + newTargets[i].searchType + ' ' + newTargets[i].name + '\n';
}
}
} else {
return this.errorReply("No Pok\u00e9mon, item, move, ability or nature named '" + target + "' was found. (Check your spelling?)");
}
if (showDetails) {
let details;
let isSnatch = false;
let isMirrorMove = false;
if (newTargets[0].searchType === 'pokemon') {
let pokemon = Tools.getTemplate(newTargets[0].name);
let weighthit = 20;
if (pokemon.weightkg >= 200) {
weighthit = 120;
} else if (pokemon.weightkg >= 100) {
weighthit = 100;
} else if (pokemon.weightkg >= 50) {
weighthit = 80;
} else if (pokemon.weightkg >= 25) {
weighthit = 60;
} else if (pokemon.weightkg >= 10) {
weighthit = 40;
}
details = {
"Dex#": pokemon.num,
"Gen": pokemon.gen,
"Height": pokemon.heightm + " m",
"Weight": pokemon.weightkg + " kg (" + weighthit + " BP)",
"Dex Colour": pokemon.color,
"Egg Group(s)": pokemon.eggGroups.join(", ")
};
if (!pokemon.evos.length) {
details["Does Not Evolve"] = "";
} else {
details["Evolution"] = pokemon.evos.map(function (evo) {
evo = Tools.getTemplate(evo);
return evo.name + " (" + evo.evoLevel + ")";
}).join(", ");
}
} else if (newTargets[0].searchType === 'move') {
let move = Tools.getMove(newTargets[0].name);
details = {
"Priority": move.priority,
"Gen": move.gen
};
if (move.secondary || move.secondaries) details["✓ Secondary effect"] = "";
if (move.flags['contact']) details["✓ Contact"] = "";
if (move.flags['sound']) details["✓ Sound"] = "";
if (move.flags['bullet']) details["✓ Bullet"] = "";
if (move.flags['pulse']) details["✓ Pulse"] = "";
if (!move.flags['protect'] && !/(ally|self)/i.test(move.target)) details["✓ Bypasses Protect"] = "";
if (move.flags['authentic']) details["✓ Bypasses Substitutes"] = "";
if (move.flags['defrost']) details["✓ Thaws user"] = "";
if (move.flags['bite']) details["✓ Bite"] = "";
if (move.flags['punch']) details["✓ Punch"] = "";
if (move.flags['powder']) details["✓ Powder"] = "";
if (move.flags['reflectable']) details["✓ Bounceable"] = "";
if (move.flags['gravity']) details["✗ Suppressed by Gravity"] = "";
if (move.id === 'snatch') isSnatch = true;
if (move.id === 'mirrormove') isMirrorMove = true;
details["Target"] = {
'normal': "One Adjacent Pok\u00e9mon",
'self': "User",
'adjacentAlly': "One Ally",
'adjacentAllyOrSelf': "User or Ally",
'adjacentFoe': "One Adjacent Opposing Pok\u00e9mon",
'allAdjacentFoes': "All Adjacent Opponents",
'foeSide': "Opposing Side",
'allySide': "User's Side",
'allyTeam': "User's Side",
'allAdjacent': "All Adjacent Pok\u00e9mon",
'any': "Any Pok\u00e9mon",
'all': "All Pok\u00e9mon"
}[move.target] || "Unknown";
} else if (newTargets[0].searchType === 'item') {
let item = Tools.getItem(newTargets[0].name);
details = {
"Gen": item.gen
};
if (item.fling) {
details["Fling Base Power"] = item.fling.basePower;
if (item.fling.status) details["Fling Effect"] = item.fling.status;
if (item.fling.volatileStatus) details["Fling Effect"] = item.fling.volatileStatus;
if (item.isBerry) details["Fling Effect"] = "Activates the Berry's effect on the target.";
if (item.id === 'whiteherb') details["Fling Effect"] = "Restores the target's negative stat stages to 0.";
if (item.id === 'mentalherb') details["Fling Effect"] = "Removes the effects of Attract, Disable, Encore, Heal Block, Taunt, and Torment from the target.";
} else {
details["Fling"] = "This item cannot be used with Fling.";
}
if (item.naturalGift) {
details["Natural Gift Type"] = item.naturalGift.type;
details["Natural Gift Base Power"] = item.naturalGift.basePower;
}
} else {
details = {};
}
buffer += '|raw|' + Object.keys(details).map(function (detail) {
return '' + detail + (details[detail] !== '' ? ': ' + details[detail] : '');
}).join(" |  ") + '';
if (isSnatch) buffer += ' |  Snatchable Moves';
if (isMirrorMove) buffer += ' |  Mirrorable Moves';
}
this.sendReply(buffer);
},
datahelp: ["/data [pokemon/item/move/ability] - Get details on this pokemon/item/move/ability/nature.",
"!data [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ # & ~"],
dt: 'details',
details: function (target) {
if (!target) return this.parse('/help details');
this.run('data');
},
detailshelp: ["/details [pokemon] - Get additional details on this pokemon/item/move/ability/nature.",
"!details [pokemon] - Show everyone these details. Requires: + % @ # & ~"],
ds: 'dexsearch',
dsearch: 'dexsearch',
dexsearch: function (target, room, user, connection, cmd, message) {
if (!this.canBroadcast()) return;
if (!target) return this.parse('/help dexsearch');
let searches = [];
let 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};
let allColours = {'green':1, 'red':1, 'blue':1, 'white':1, 'brown':1, 'yellow':1, 'purple':1, 'pink':1, 'gray':1, 'black':1};
let allStats = {'hp':1, 'atk':1, 'def':1, 'spa':1, 'spd':1, 'spe':1, 'bst':1};
let showAll = false;
let megaSearch = null;
let capSearch = null;
let randomOutput = 0;
let self = this;
let validParameter = function (cat, param, isNotSearch) {
for (let h = 0; h < searches.length; h++) {
let group = searches[h];
if (group[cat] === undefined) continue;
if (group[cat][param] === undefined) continue;
if (group[cat][param] === isNotSearch) {
self.sendReplyBox("A search cannot both include and exclude '" + param + "'.");
} else {
self.sendReplyBox("The search included '" + (isNotSearch ? "!" : "") + param + "' more than once.");
}
return false;
}
return true;
};
let andGroups = target.split(',');
for (let i = 0; i < andGroups.length; i++) {
let orGroup = {abilities: {}, tiers: {}, colors: {}, gens: {}, moves: {}, types: {}, stats: {}, skip: false};
let parameters = andGroups[i].split("|");
if (parameters.length > 3) return this.sendReply("No more than 3 alternatives for each parameter may be used.");
for (let j = 0; j < parameters.length; j++) {
let isNotSearch = false;
target = parameters[j].trim().toLowerCase();
if (target.charAt(0) === '!') {
isNotSearch = true;
target = target.substr(1);
}
let targetAbility = Tools.getAbility(target);
if (targetAbility.exists) {
if (!validParameter("abilities", targetAbility, isNotSearch)) return;
orGroup.abilities[targetAbility] = !isNotSearch;
continue;
}
if (target in allTiers) {
if (target === "cap") {
if (parameters.length > 1) return this.sendReplyBox("The parameter 'cap' cannot have alternative parameters");
capSearch = !isNotSearch;
}
if (!validParameter("tiers", target, isNotSearch)) return;
orGroup.tiers[target] = !isNotSearch;
continue;
}
if (target in allColours) {
target = target.capitalize();
if (!validParameter("colors", target, isNotSearch)) return;
orGroup.colors[target] = !isNotSearch;
continue;
}
if (target.substr(0, 3) === 'gen' && Number.isInteger(parseFloat(target.substr(3)))) target = target.substr(3).trim();
let targetInt = parseInt(target, 10);
if (0 < targetInt && targetInt < 7) {
if (!validParameter("gens", target, isNotSearch)) return;
orGroup.gens[target] = !isNotSearch;
continue;
}
if (target === 'all') {
if (this.broadcasting && !room.isPersonal) return this.sendReplyBox("A search with the parameter 'all' cannot be broadcast.");
if (parameters.length > 1) return this.sendReplyBox("The parameter 'all' cannot have alternative parameters");
showAll = true;
orGroup.skip = true;
break;
}
if (target.substr(0, 6) === 'random' && cmd === 'randpoke') {
//validation for this is in the /randpoke command
randomOutput = parseInt(target.substr(6), 10);
orGroup.skip = true;
continue;
}
if (target === 'megas' || target === 'mega') {
if (parameters.length > 1) return this.sendReplyBox("The parameter 'mega' cannot have alternative parameters");
megaSearch = !isNotSearch;
orGroup.skip = true;
break;
}
if (target === 'recovery') {
if (parameters.length > 1) return this.sendReplyBox("The parameter 'recovery' cannot have alternative parameters");
let recoveryMoves = ["recover", "roost", "moonlight", "morningsun", "synthesis", "milkdrink", "slackoff", "softboiled", "wish", "healorder"];
for (let k = 0; k < recoveryMoves.length; k++) {
if (!validParameter("moves", recoveryMoves[k], isNotSearch)) return;
if (isNotSearch) {
let bufferObj = {moves: {}};
bufferObj.moves[recoveryMoves[k]] = false;
searches.push(bufferObj);
} else {
orGroup.moves[recoveryMoves[k]] = true;
}
}
if (isNotSearch) orGroup.skip = true;
break;
}
if (target === 'priority') {
if (parameters.length > 1) return this.sendReplyBox("The parameter 'priority' cannot have alternative parameters");
for (let move in Tools.data.Movedex) {
let moveData = Tools.getMove(move);
if (moveData.category === "Status" || moveData.id === "bide") continue;
if (moveData.priority > 0) {
if (!validParameter("moves", move, isNotSearch)) return;
if (isNotSearch) {
let bufferObj = {moves: {}};
bufferObj.moves[move] = false;
searches.push(bufferObj);
} else {
orGroup.moves[move] = true;
}
}
}
if (isNotSearch) orGroup.skip = true;
break;
}
let targetMove = Tools.getMove(target);
if (targetMove.exists) {
if (!validParameter("moves", targetMove.id, isNotSearch)) return;
orGroup.moves[targetMove.id] = !isNotSearch;
continue;
}
let typeIndex = target.indexOf(' type');
if (typeIndex >= 0) {
target = target.charAt(0).toUpperCase() + target.substring(1, typeIndex);
if (target in Tools.data.TypeChart) {
if (!validParameter("types", target, isNotSearch)) return;
orGroup.types[target] = !isNotSearch;
continue;
}
}
let 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);
}
let inequalityOffset = (inequality.charAt(1) === '=' ? 0 : -1);
let targetParts = target.replace(/\s/g, '').split(inequality);
let 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 (!orGroup.stats[stat]) orGroup.stats[stat] = {};
if (orGroup.stats[stat][direction]) return this.sendReplyBox("Invalid stat range for " + stat + ".");
orGroup.stats[stat][direction] = num;
continue;
}
return this.sendReplyBox("'" + Tools.escapeHTML(target) + "' could not be found in any of the search categories.");
}
searches.push(orGroup);
}
if (showAll && searches.length === 0 && megaSearch === null) return this.sendReplyBox("No search parameters other than 'all' were found. Try '/help dexsearch' for more information on this command.");
let dex = {};
for (let pokemon in Tools.data.Pokedex) {
let template = Tools.getTemplate(pokemon);
let megaSearchResult = (megaSearch === null || (megaSearch === true && template.isMega) || (megaSearch === false && !template.isMega));
if (template.tier !== 'Unreleased' && template.tier !== 'Illegal' && (template.tier !== 'CAP' || capSearch) && megaSearchResult) {
dex[pokemon] = template;
}
}
let learnSetsCompiled = false;
//ensure searches with the least alternatives are run first
searches.sort(function (a, b) {
let aCount = 0, bCount = 0;
for (let cat in a) {
if (typeof a[cat] === "object") aCount += Object.size(a[cat]);
}
for (let cat in b) {
if (typeof b[cat] === "object") bCount += Object.size(b[cat]);
}
return aCount - bCount;
});
for (let group = 0; group < searches.length; group++) {
let alts = searches[group];
if (alts.skip) continue;
for (let mon in dex) {
let matched = false;
if (Object.size(alts.gens) > 0) {
if (alts.gens[dex[mon].gen] || (Object.count(alts.gens, false) > 0 &&
alts.gens[dex[mon].gen] !== false)) continue;
}
if (Object.size(alts.colors) > 0) {
if (alts.colors[dex[mon].color] || (Object.count(alts.colors, false) > 0 &&
alts.colors[dex[mon].color] !== false)) continue;
}
if (Object.size(alts.tiers) > 0) {
if (alts.tiers[dex[mon].tier.toLowerCase()] || (Object.count(alts.tiers, false) > 0 &&
alts.tiers[dex[mon].tier.toLowerCase()] !== false)) continue;
}
for (let type in alts.types) {
if (dex[mon].types.indexOf(type) >= 0 === alts.types[type]) {
matched = true;
break;
}
}
if (matched) continue;
for (let ability in alts.abilities) {
if (alts.abilities[ability] === (Object.count(dex[mon].abilities, ability) > 0)) {
matched = true;
break;
}
}
if (matched) continue;
for (let stat in alts.stats) {
let monStat = 0;
if (stat === 'bst') {
for (let monStats in dex[mon].baseStats) {
monStat += dex[mon].baseStats[monStats];
}
} else {
monStat = dex[mon].baseStats[stat];
}
if (typeof alts.stats[stat].less === 'number') {
if (monStat <= alts.stats[stat].less) {
matched = true;
break;
}
}
if (typeof alts.stats[stat].greater === 'number') {
if (monStat >= alts.stats[stat].greater) {
matched = true;
break;
}
}
if (typeof alts.stats[stat].equal === 'number') {
if (monStat === alts.stats[stat].equal) {
matched = true;
break;
}
}
}
if (matched) continue;
if (!learnSetsCompiled) {
for (let mon2 in dex) {
let template = dex[mon2];
if (!template.learnset) template = Tools.getTemplate(template.baseSpecies);
if (!template.learnset) continue;
let fullLearnset = template.learnset;
while (template.prevo) {
template = Tools.getTemplate(template.prevo);
for (let move in template.learnset) {
if (!fullLearnset[move]) fullLearnset[move] = template.learnset[move];
}
}
dex[mon2].learnset = fullLearnset;
}
learnSetsCompiled = true;
}
for (let move in alts.moves) {
let canLearn = (dex[mon].learnset.sketch && ['chatter', 'struggle', 'magikarpsrevenge'].indexOf(move) < 0) || dex[mon].learnset[move];
if ((canLearn && alts.moves[move]) || (alts.moves[move] === false && !canLearn)) {
matched = true;
break;
}
}
if (matched) continue;
delete dex[mon];
}
}
let results = [];
for (let 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);
}
let resultsStr = this.broadcasting ? "" : ("" + Tools.escapeHTML(message) + ":
");
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. Redo the search with 'all' as a search parameter to show all results.";
}
} 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 [parameter], [parameter], [parameter], ... - 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/BL4/PU/NFE/LC/CAP.",
"Types must be followed by ' type', e.g., 'dragon type'.",
"Inequality ranges use the characters '>=' for '≥' and '<=' for '≤', e.g., 'hp <= 95' searches all Pok\u00e9mon with HP less than or equal to 95.",
"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 Pok\u00e9mon only.",
"Parameters separated with '|' will be searched as alternatives for each other, e.g., 'trick | switcheroo' searches for all Pok\u00e9mon that learn either Trick or Switcheroo.",
"The order of the parameters does not matter."],
rollpokemon: 'randompokemon',
randpoke: 'randompokemon',
randompokemon: function (target, room, user, connection, cmd, message) {
let targets = target.split(",");
let targetsBuffer = [];
let qty;
for (let i = 0; i < targets.length; i++) {
if (!targets[i]) continue;
let num = Number(targets[i]);
if (Number.isInteger(num)) {
if (qty) return this.errorReply("Only specify the number of Pok\u00e9mon once.");
qty = num;
if (qty < 1 || 15 < qty) return this.errorReply("Number of random Pok\u00e9mon 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 Pok\u00e9mon 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 Pok\u00e9mon, e.g., '/randpoke 6' returns 6 random Pok\u00e9mon."],
ms: 'movesearch',
msearch: 'movesearch',
movesearch: function (target, room, user, connection, cmd, message) {
if (!this.canBroadcast()) return;
if (!target) return this.parse('/help movesearch');
let targets = target.split(',');
let searches = {};
let allCategories = {'physical':1, 'special':1, 'status':1};
let allProperties = {'basePower':1, 'accuracy':1, 'priority':1, 'pp':1};
let allFlags = {'authentic':1, 'bite':1, 'bullet':1, 'contact':1, 'defrost':1, 'powder':1, 'pulse':1, 'punch':1, 'secondary':1, 'snatch':1, 'sound':1};
let allStatus = {'psn':1, 'tox':1, 'brn':1, 'par':1, 'frz':1, 'slp':1};
let allVolatileStatus = {'flinch':1, 'confusion':1, 'partiallytrapped':1};
let allBoosts = {'hp':1, 'atk':1, 'def':1, 'spa':1, 'spd':1, 'spe':1, 'accuracy':1, 'evasion':1};
let showAll = false;
let lsetData = {};
let targetMon = '';
for (let i = 0; i < targets.length; i++) {
let isNotSearch = false;
target = targets[i].toLowerCase().trim();
if (target.charAt(0) === '!') {
isNotSearch = true;
target = target.substr(1);
}
let 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 && !room.isPersonal) 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;
}
let 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 (let move in template.learnset) {
if (!lsetData[move]) lsetData[move] = template.learnset[move];
}
}
continue;
}
let inequality = target.search(/>|<|=/);
if (inequality >= 0) {
if (isNotSearch) return this.sendReplyBox("You cannot use the negation symbol '!' in quality ranges.");
inequality = target.charAt(inequality);
let targetParts = target.replace(/\s/g, '').split(inequality);
let 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) + "'.");
}
let 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') {
let 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;
}
let 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.");
let dex = {};
if (targetMon) {
for (let move in lsetData) {
dex[move] = Tools.getMove(move);
}
} else {
for (let move in Tools.data.Movedex) {
dex[move] = Tools.getMove(move);
}
delete dex.magikarpsrevenge;
}
for (let search in searches) {
switch (search) {
case 'type':
case 'category':
for (let 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 (let flag in searches[search]) {
for (let 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 (let move in dex) {
let hasRecovery = (dex[move].drain || dex[move].flags.heal);
if ((!hasRecovery && searches[search]) || (hasRecovery && !searches[search])) delete dex[move];
}
break;
case 'property':
for (let prop in searches[search]) {
for (let 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 (let boost in searches[search]) {
for (let 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 (let searchStatus in searches[search]) {
for (let 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 {
let hasSecondary = false;
for (let 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.");
}
}
let results = [];
for (let move in dex) {
results.push(dex[move].name);
}
let resultsStr = "";
if (targetMon) {
resultsStr += "Matching moves found in learnset for " + targetMon + ":
";
} else {
resultsStr += this.broadcasting ? "" : ("" + Tools.escapeHTML(message) + ":
");
}
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. Redo the search with 'all' as a search parameter to show all results.";
}
} 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."],
isearch: 'itemsearch',
itemsearch: function (target, room, user, connection, cmd, message) {
if (!target) return this.parse('/help itemsearch');
if (!this.canBroadcast()) return;
let 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, '');
let rawSearch = target.split(' ');
let searchedWords = [];
let foundItems = [];
//refine searched words
for (let i = 0; i < rawSearch.length; i++) {
let 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';
}
break;
case 'special': newWord = 'sp'; break;
case 'spa':
newWord = 'sp';
break;
case 'atk':
case 'attack':
if (rawSearch[i - 1] === 'sp') {
newWord = 'atk';
} else {
newWord = 'attack';
}
break;
case 'spd':
newWord = 'sp';
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) {
let basePower = 0;
let effect;
for (let k = 0; k < searchedWords.length; k++) {
let 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], 10);
}
}
}
for (let n in Tools.data.Items) {
let 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) {
let basePower = 0;
let type = "";
for (let 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], 10);
}
}
}
for (let n in Tools.data.Items) {
let 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 {
let bestMatched = 0;
for (let n in Tools.data.Items) {
let item = Tools.getItem(n);
let matched = 0;
// splits words in the description into a toId()-esk format except retaining / and . in numbers
let 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 (let 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 (let l = 0; l < foundItems.length; l++) {
let item = Tools.getItem(foundItems[l]);
let matched = 0;
let 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 (let k = 0; k < searchedWords.length; k++) {
if (descWords.indexOf(searchedWords[k]) >= 0) matched++;
}
if (matched !== bestMatched) {
foundItems.splice(l, 1);
l--;
}
}
}
let resultsStr = this.broadcasting ? "" : ("" + Tools.escapeHTML(message) + ":
");
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. Redo the search with ', all' at the end to show all results.";
}
} 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;
let lsetData = {set:{}};
let targets = target.split(',');
let template = Tools.getTemplate(targets[0]);
let move = {};
let problem;
let format = {rby:'gen1ou', gsc:'gen2ou', adv:'gen3ou', dpp:'gen4ou', bw2:'gen5ou'}[cmd.substring(0, 3)];
let all = (cmd === 'learnall');
if (cmd === 'learn5') lsetData.set.level = 5;
if (cmd === 'g6learn') lsetData.format = {noPokebank: true};
if (!template.exists) {
return this.errorReply("Pok\u00e9mon '" + template.id + "' not found.");
}
if (targets.length < 2) {
return this.errorReply("You must specify at least one move.");
}
for (let i = 1, len = targets.length; i < len; ++i) {
move = Tools.getMove(targets[i]);
if (!move.exists) {
return this.errorReply("Move '" + move.id + "' not found.");
}
problem = TeamValidator.checkLearnsetSync(format, move, template.species, lsetData);
if (problem) break;
}
let buffer = template.name + (problem ? " learn " : " learn ") + (targets.length > 2 ? "these moves" : move.name);
if (format) buffer += ' on ' + cmd.substring(0, 3).toUpperCase();
if (!problem) {
let sourceNames = {E:"egg", S:"event", D:"dream world"};
if (lsetData.sources || lsetData.sourcesBefore) buffer += " only when obtained from:
| '; let icon = {}; for (let type in Tools.data.TypeChart) { icon[type] = ' | ' + icon[type] + ' | '; } buffer += '
|---|---|
| ' + icon[type1] + ' | '; for (let type2 in Tools.data.TypeChart) { let typing; let cell = 'bestEff) bestEff = curEff; } if (bestEff === -5) { bestEff = 0; } else { bestEff = Math.pow(2, bestEff); } } switch (bestEff) { case 0: cell += 'bgcolor=#666666 title="' + typing + '">' + bestEff + ''; break; case 0.25: case 0.5: cell += 'bgcolor=#AA5544 title="' + typing + '">' + bestEff + ''; break; case 1: cell += 'bgcolor=#6688AA title="' + typing + '">' + bestEff + ''; break; case 2: case 4: cell += 'bgcolor=#559955 title="' + typing + '">' + bestEff + ''; break; default: throw new Error("/coverage effectiveness of " + bestEff + " from parameters: " + target); } cell += ' | '; buffer += cell; } } buffer += '
| Name | Description | "); for (let i = 0; i < sections[sectionId].formats.length; i++) { let format = Tools.getFormat(sections[sectionId].formats[i]); buf.push("
|---|---|
| " + Tools.escapeHTML(format.name) + " | " + (format.desc ? format.desc.join(" ") : "—") + " |