pokemon-showdown/config/commands.js

1259 lines
50 KiB
JavaScript

/**
* Commands
* Pokemon Showdown - http://pokemonshowdown.com/
*
* These are commands. For instance, you can define the command 'whois'
* here, then use it by typing /whois into Pokemon Showdown.
*
* A command can be in the form:
* ip: 'whois',
* This is called an alias: it makes it so /ip does the same thing as
* /whois.
*
* But to actually define a command, it's a function:
* birkal: function(target, room, user) {
* this.sendReply("It's not funny anymore.");
* },
*
* Commands are actually passed five parameters:
* function(target, room, user, connection, cmd, message)
* Most of the time, you only need the first three, though.
*
* target = the part of the message after the command
* room = the room object the message was sent to
* The room name is room.id
* user = the user object that sent the message
* The user's name is user.name
* connection = the connection that the message was sent from
* cmd = the name of the command
* message = the entire message sent by the user
*
* If a user types in "/msg zarel, hello"
* target = "zarel, hello"
* cmd = "msg"
* message = "/msg zarel, hello"
*
* Commands return the message the user should say. If they don't
* return anything or return something falsy, the user won't say
* anything.
*
* Commands have access to the following functions:
*
* this.sendReply(message)
* Sends a message back to the room the user typed the command into.
*
* this.sendReplyBox(html)
* Same as sendReply, but shows it in a box, and you can put HTML in
* it.
*
* this.popupReply(message)
* Shows a popup in the window the user typed the command into.
*
* this.add(message)
* Adds a message to the room so that everyone can see it.
* This is like this.sendReply, except everyone in the room gets it,
* instead of just the user that typed the command.
*
* this.send(message)
* Sends a message to the room so that everyone can see it.
* This is like this.add, except it's not logged, and users who join
* the room later won't see it in the log, and if it's a battle, it
* won't show up in saved replays.
* You USUALLY want to use this.add instead.
*
* this.logEntry(message)
* Log a message to the room's log without sending it to anyone. This
* is like this.add, except no one will see it.
*
* this.addModCommand(message)
* Like this.add, but also logs the message to the moderator log
* which can be seen with /modlog.
*
* this.logModCommand(message)
* Like this.addModCommand, except users in the room won't see it.
*
* this.can(permission)
* this.can(permission, targetUser)
* Checks if the user has the permission to do something, or if a
* targetUser is passed, check if the user has permission to do
* it to that user. Will automatically give the user an "Access
* denied" message if the user doesn't have permission: use
* user.can() if you don't want that message.
*
* Should usually be near the top of the command, like:
* if (!this.can('potd')) return false;
*
* this.canBroadcast()
* Signifies that a message can be broadcast, as long as the user
* has permission to. This will check to see if the user used
* "!command" instead of "/command". If so, it will check to see
* if the user has permission to broadcast (by default, voice+ can),
* and return false if not. Otherwise, it will set it up so that
* this.sendReply and this.sendReplyBox will broadcast to the room
* instead of just the user that used the command.
*
* Should usually be near the top of the command, like:
* if (!this.canBroadcast()) return false;
*
* this.canTalk()
* Checks to see if the user can speak in the room. Returns false
* if the user can't speak (is muted, the room has modchat on, etc),
* or true otherwise.
*
* Should usually be near the top of the command, like:
* if (!this.canTalk()) return false;
*
* this.canTalk(message)
* Checks to see if the user can say the message. In addition to
* running the checks from this.canTalk(), it also checks to see if
* the message has any banned words or is too long. Returns the
* filtered message, or a falsy value if the user can't speak.
*
* Should usually be near the top of the command, like:
* target = this.canTalk(target);
* if (!target) return false;
*
* this.parse(message)
* Runs the message as if the user had typed it in.
*
* Mostly useful for giving help messages, like for commands that
* require a target:
* if (!target) return this.parse('/help msg');
*
* After 10 levels of recursion (calling this.parse from a command
* called by this.parse from a command called by this.parse etc)
* we will assume it's a bug in your command and error out.
*
* this.targetUserOrSelf(target)
* If target is blank, returns the user that sent the message.
* Otherwise, returns the user with the username in target, or
* a falsy value if no user with that username exists.
*
* this.splitTarget(target)
* Splits a target in the form "user, message" into its
* constituent parts. Returns message, and sets this.targetUser to
* the user, and this.targetUsername to the username.
*
* Remember to check if this.targetUser exists before going further.
*
* Unless otherwise specified, these functions will return undefined,
* so you can return this.sendReply or something to send a reply and
* stop the command there.
*
* @license MIT license
*/
var commands = exports.commands = {
ip: 'whois',
getip: 'whois',
rooms: 'whois',
altcheck: 'whois',
alt: 'whois',
alts: 'whois',
getalts: 'whois',
whois: function(target, room, user) {
var targetUser = this.targetUserOrSelf(target);
if (!targetUser) {
return this.sendReply('User '+this.targetUsername+' not found.');
}
this.sendReply('User: '+targetUser.name);
if (user.can('alts', targetUser.getHighestRankedAlt())) {
var alts = targetUser.getAlts();
var output = '';
for (var i in targetUser.prevNames) {
if (output) output += ", ";
output += targetUser.prevNames[i];
}
if (output) this.sendReply('Previous names: '+output);
for (var j=0; j<alts.length; j++) {
var targetAlt = Users.get(alts[j]);
if (!targetAlt.named && !targetAlt.connected) continue;
this.sendReply('Alt: '+targetAlt.name);
output = '';
for (var i in targetAlt.prevNames) {
if (output) output += ", ";
output += targetAlt.prevNames[i];
}
if (output) this.sendReply('Previous names: '+output);
}
}
if (config.groups[targetUser.group] && config.groups[targetUser.group].name) {
this.sendReply('Group: ' + config.groups[targetUser.group].name + ' (' + targetUser.group + ')');
}
if (targetUser.staffAccess) {
this.sendReply('(Pok\xE9mon Showdown Development Staff)');
}
if (!targetUser.authenticated) {
this.sendReply('(Unregistered)');
}
if (!this.broadcasting && user.can('ip', targetUser)) {
var ips = Object.keys(targetUser.ips);
this.sendReply('IP' + ((ips.length > 1) ? 's' : '') + ': ' + ips.join(', '));
}
var output = 'In rooms: ';
var first = true;
for (var i in targetUser.roomCount) {
if (i === 'global' || Rooms.get(i).isPrivate) continue;
if (!first) output += ' | ';
first = false;
output += '<a href="/'+i+'" room="'+i+'">'+i+'</a>';
}
this.sendReply('|raw|'+output);
},
ipsearch: function(target, room, user) {
if (!this.can('rangeban')) return;
var atLeastOne = false;
this.sendReply("Users with IP "+target+":");
for (var userid in Users.users) {
var user = Users.users[userid];
if (user.latestIp === target) {
this.sendReply((user.connected?"+":"-")+" "+user.name);
atLeastOne = true;
}
}
if (!atLeastOne) this.sendReply("No results found.");
},
/*********************************************************
* Shortcuts
*********************************************************/
invite: function(target, room, user) {
target = this.splitTarget(target);
if (!this.targetUser) {
return this.sendReply('User '+this.targetUsername+' not found.');
}
var roomid = (target || room.id);
if (!Rooms.get(roomid)) {
return this.sendReply('Room '+roomid+' not found.');
}
return this.parse('/msg '+this.targetUsername+', /invite '+roomid);
},
/*********************************************************
* Informational commands
*********************************************************/
stats: 'data',
dex: 'data',
pokedex: 'data',
data: function(target, room, user) {
if (!this.canBroadcast()) return;
var pokemon = Tools.getTemplate(target);
var item = Tools.getItem(target);
var move = Tools.getMove(target);
var ability = Tools.getAbility(target);
var data = '';
if (pokemon.exists) {
data += '|c|~|/data-pokemon '+pokemon.name+'\n';
}
if (ability.exists) {
data += '|c|~|/data-ability '+ability.name+'\n';
}
if (item.exists) {
data += '|c|~|/data-item '+item.name+'\n';
}
if (move.exists) {
data += '|c|~|/data-move '+move.name+'\n';
}
if (!data) {
data = "||No pokemon, item, move, or ability named '"+target+"' was found. (Check your spelling?)";
}
this.sendReply(data);
},
dexsearch: function (target, room, user) {
if (!this.canBroadcast()) return;
if (!target) return this.parse('/help dexsearch');
var targets = target.split(',');
var target;
var moves = {}, tiers = {}, colours = {}, ability = {}, gens = {}, types = {};
var count = 0;
var all = false;
var output = 10;
for (var i in targets) {
target = Tools.getMove(targets[i]);
if (target.exists) {
if (!moves.count) {
count++;
moves.count = 0;
}
if (moves.count === 4) {
return this.sendReply('Specify a maximum of 4 moves.');
}
moves[target] = 1;
moves.count++;
continue;
}
target = Tools.getAbility(targets[i]);
if (target.exists) {
if (!ability.count) {
count++;
ability.count = 0;
}
if (ability.count === 1) {
return this.sendReply('Specify only one ability.');
}
ability[target] = 1;
ability.count++;
continue;
}
target = targets[i].trim().toLowerCase();
if (['fire','water','electric','dragon','rock','fighting','ground','ghost','psychic','dark','bug','flying','grass','poison','normal','steel','ice'].indexOf(toId(target.substring(0, target.length - 4))) > -1) {
if (!types.count) {
count++;
types.count = 0;
}
if (types.count === 2) {
return this.sendReply('Specify a maximum of two types.');
}
types[toId(target.substring(0, target.length - 4)).substring(0, 1).toUpperCase() + toId(target.substring(0, target.length - 4)).substring(1)] = 1;
types.count++;
}
else if (['uber','ou','uu','ru','nu','lc','cap','bl','bl2','nfe','illegal'].indexOf(target) > -1) {
if (!tiers.count) {
count++;
tiers.count = 0;
}
tiers[target] = 1;
tiers.count++;
}
else if (['green','red','blue','white','brown','yellow','purple','pink','gray','black'].indexOf(target) > -1) {
if (!colours.count) {
count++;
colours.count = 0;
}
colours[target] = 1;
colours.count++;
}
else if (parseInt(target, 10) > 0) {
if (!gens.count) {
count++;
gens.count = 0;
}
gens[parseInt(target, 10)] = 1;
gens.count++;
}
else if (target === 'all') {
if (this.broadcasting) {
return this.sendReply('A search with the parameter "all" cannot be broadcast.')
}
all = true;
}
else {
return this.sendReply('"' + target + '" could not be found in any of the search categories.');
}
}
if (all && count === 0) return this.sendReply('No search parameters other than "all" were found.\nTry "/help dexsearch" for more information on this command.');
while (count > 0) {
--count;
var tempResults = [];
if (!results) {
for (var pokemon in Tools.data.Pokedex) {
if (pokemon === 'arceusunknown') continue;
pokemon = Tools.getTemplate(pokemon);
if (!(!('illegal' in tiers) && pokemon.tier === 'Illegal')) {
tempResults.add(pokemon);
}
}
} else {
for (var mon in results) tempResults.add(results[mon]);
}
var results = [];
if (types.count > 0) {
for (var mon in tempResults) {
if (types.count === 1) {
if (tempResults[mon].types[0] in types || tempResults[mon].types[1] in types) results.add(tempResults[mon]);
} else {
if (tempResults[mon].types[0] in types && tempResults[mon].types[1] in types) results.add(tempResults[mon]);
}
}
types.count = 0;
continue;
}
if (tiers.count > 0) {
for (var mon in tempResults) {
if ('cap' in tiers) {
if (tempResults[mon].tier.substring(2).toLowerCase() === 'cap') results.add(tempResults[mon]);
}
if (tempResults[mon].tier.toLowerCase() in tiers) results.add(tempResults[mon]);
}
tiers.count = 0;
continue;
}
if (ability.count > 0) {
for (var mon in tempResults) {
for (var monAbility in tempResults[mon].abilities) {
if (Tools.getAbility(tempResults[mon].abilities[monAbility]) in ability) results.add(tempResults[mon]);
}
}
ability.count = 0;
continue;
}
if (colours.count > 0) {
for (var mon in tempResults) {
if (tempResults[mon].color.toLowerCase() in colours) results.add(tempResults[mon]);
}
colours.count = 0;
continue;
}
if (moves.count > 0) {
var problem;
var move = {};
for (var mon in tempResults) {
var lsetData = {set:{}};
template = Tools.getTemplate(tempResults[mon].id);
for (var i in moves) {
move = Tools.getMove(i);
if (move.id !== 'count') {
if (!move.exists) return this.sendReply('"' + move + '" is not a known move.');
problem = Tools.checkLearnset(move, template, lsetData);
if (problem) break;
}
}
if (!problem) results.add(tempResults[mon]);
}
moves.count = 0;
continue;
}
if (gens.count > 0) {
for (var mon in tempResults) {
if (tempResults[mon].gen in gens) results.add(tempResults[mon]);
}
gens.count = 0;
continue;
}
}
var resultsStr = '';
if (results.length > 0) {
if (all || results.length <= output) {
for (var i = 0; i < results.length; i++) resultsStr += results[i].species + ', ';
} else {
var hidden = string(results.length - output);
results.sort(function(a,b) {return Math.round(Math.random());});
for (var i = 0; i < output; i++) resultsStr += results[i].species + ', ';
resultsStr += ' and ' + hidden + ' more. Redo the search with "all" as a search parameter to show all results. '
}
} else {
resultsStr = 'No Pokemon found. ';
}
return this.sendReplyBox(resultsStr.substring(0, resultsStr.length - 2));
},
learnset: 'learn',
learnall: 'learn',
learn5: '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 all = (cmd === 'learnall');
if (cmd === 'learn5') lsetData.set.level = 5;
if (!template.exists) {
return this.sendReply('Pokemon "'+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 = Tools.checkLearnset(move, template, 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 (!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;
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.substr(0,1)+" "+sourceNames[source.substr(1,1)];
if (prevSourceType === '5E' && template.maleOnlyDreamWorld) buffer += " (cannot have DW ability)";
if (source.substr(2)) buffer += ": "+source.substr(2);
}
}
if (lsetData.sourcesBefore) buffer += "<li>any generation before "+(lsetData.sourcesBefore+1);
buffer += "</ul>";
}
this.sendReplyBox(buffer);
},
weak: 'weakness',
weakness: function(target, room, user){
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) {
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(target + " isn't a recognized type or pokemon.");
}
var weaknesses = [];
Object.keys(Tools.data.TypeChart).forEach(function (type) {
var notImmune = Tools.getImmunity(type, pokemon);
if (notImmune) {
var typeMod = Tools.getEffectiveness(type, pokemon);
if (typeMod == 1) weaknesses.push(type);
if (typeMod == 2) weaknesses.push("<b>" + type + "</b>");
}
});
if (!weaknesses.length) {
this.sendReplyBox(target + " has no weaknesses.");
} else {
this.sendReplyBox(target + " is weak to: " + weaknesses.join(', ') + " (not counting abilities).");
}
},
matchup: 'effectiveness',
effectiveness: function(target, room, user) {
var targets = target.split(/[,/]/);
var type = Tools.getType(targets[1]);
var pokemon = Tools.getTemplate(targets[0]);
var defender;
if (!pokemon.exists || !type.exists) {
// try the other way around
pokemon = Tools.getTemplate(targets[1]);
type = Tools.getType(targets[0]);
}
defender = pokemon.species+' (not counting abilities)';
if (!pokemon.exists || !type.exists) {
// try two types
if (Tools.getType(targets[0]).exists && Tools.getType(targets[1]).exists) {
// two types
type = Tools.getType(targets[0]);
defender = Tools.getType(targets[1]).id;
pokemon = {types: [defender]};
if (Tools.getType(targets[2]).exists) {
defender = Tools.getType(targets[1]).id + '/' + Tools.getType(targets[2]).id;
pokemon = {types: [Tools.getType(targets[1]).id, Tools.getType(targets[2]).id]};
}
} else {
if (!targets[1]) {
return this.sendReply("Attacker and defender must be separated with a comma.");
}
return this.sendReply("'"+targets[0].trim()+"' and '"+targets[1].trim()+"' aren't a recognized combination.");
}
}
if (!this.canBroadcast()) return;
var typeMod = Tools.getEffectiveness(type.id, pokemon);
var notImmune = Tools.getImmunity(type.id, pokemon);
var factor = 0;
if (notImmune) {
factor = Math.pow(2, typeMod);
}
this.sendReplyBox(''+type.id+' attacks are '+factor+'x effective against '+defender+'.');
},
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 also mute and lock users and check for alts<br />' +
'@ <b>Moderator</b> - The above, and they can ban users<br />' +
'&amp; <b>Leader</b> - The above, and they can promote moderators and force ties<br />'+
'~ <b>Administrator</b> - They can do anything, like change what this message says');
},
opensource: function(target, room, user) {
if (!this.canBroadcast()) return;
this.sendReplyBox('Pokemon Showdown is open source:<br />- Language: JavaScript<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>');
},
avatars: function(target, room, user) {
if (!this.canBroadcast()) return;
this.sendReplyBox('Your avatar can be changed using the Options menu (it looks like a gear) in the upper right of Pokemon Showdown.');
},
introduction: 'intro',
intro: function(target, room, user) {
if (!this.canBroadcast()) return;
this.sendReplyBox('New to competitive pokemon?<br />' +
'- <a href="http://www.pokemonshowdown.com/forums/viewtopic.php?f=2&t=76">Beginner\'s Guide to Pokémon Showdown</a><br />' +
'- <a href="http://www.smogon.com/dp/articles/intro_comp_pokemon">An introduction to competitive Pokémon</a><br />' +
'- <a href="http://www.smogon.com/bw/articles/bw_tiers">What do "OU", "UU", etc mean?</a><br />' +
'- <a href="http://www.smogon.com/bw/banlist/">What are the rules for each format? What is "Sleep Clause"?</a>');
},
calculator: 'calc',
calc: function(target, room, user) {
if (!this.canBroadcast()) return;
this.sendReplyBox('Pokemon Showdown! damage calculator. (Courtesy of Honko)<br />' +
'- <a href="http://pokemonshowdown.com/damagecalc/">Damage Calculator</a>');
},
cap: function(target, room, user) {
if (!this.canBroadcast()) return;
this.sendReplyBox('An introduction to the Create-A-Pokemon project:<br />' +
'- <a href="http://www.smogon.com/cap/">CAP project website and description</a><br />' +
'- <a href="http://www.smogon.com/forums/showthread.php?t=48782">What Pokemon have been made?</a><br />' +
'- <a href="http://www.smogon.com/forums/showthread.php?t=3464513">Talk about the metagame here</a><br />' +
'- <a href="http://www.smogon.com/forums/showthread.php?t=3466826">Practice BW CAP teams</a>');
},
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="http://pokemonshowdown.com/replay/gennextou-37815908">roseyraid vs Zarel</a><br />' +
'- <a href="http://pokemonshowdown.com/replay/gennextou-37900768">QwietQwilfish vs pickdenis</a>');
},
om: 'othermetas',
othermetas: function(target, room, user) {
if (!this.canBroadcast()) return;
target = toId(target);
var buffer = '';
var matched = false;
if (!target || target === 'all') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/forums/206/">Information on the Other Metagames</a><br />';
}
if (target === 'all' || target === 'hackmons') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/threads/3475624/">Hackmons</a><br />';
}
if (target === 'all' || target === 'balancedhackmons' || target === 'bh') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/threads/3463764/">Balanced Hackmons</a><br />';
}
if (target === 'all' || target === 'glitchmons') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/threads/3467120/">Glitchmons</a><br />';
}
if (target === 'all' || target === 'tiershift' || target === 'ts') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/threads/3479358/">Tier Shift</a><br />';
}
if (target === 'all' || target === 'seasonal') {
matched = true;
buffer += '- <a href="http://www.smogon.com/sim/seasonal">Seasonal Ladder</a><br />';
}
if (target === 'all' || target === 'vgc2013' || target === 'vgc') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/threads/3471161/">VGC 2013</a><br />';
}
if (target === 'all' || target === 'omotm' || target === 'omofthemonth' || target === 'month') {
matched = true;
buffer += '- <a href="http://www.smogon.com/forums/threads/3481155/">OM of the Month</a>';
}
if (!matched) {
return this.sendReply('The Other Metas entry "'+target+'" was not found. Try /othermetas or /om for general help.');
}
this.sendReplyBox(buffer);
},
roomhelp: function(target, room, user) {
if (room.id === 'lobby') return this.sendReply('This command is too spammy for lobby.');
if (!this.canBroadcast()) return;
this.sendReplyBox('Room moderators (%) can use:<br />' +
'- /mute <em>username</em>: 7 minute mute<br />' +
'- /hourmute <em>username</em>: 60 minute mute<br />' +
'- /unmute <em>username</em>: unmute<br />' +
'- /roomvoice <em>username</em>: appoint a room voice<br />' +
'- /deroomvoice <em>username</em>: remove a room voice<br />' +
'- /announce <em>message</em>: make an announcement<br />' +
'<br />' +
'Room owners (#) can use:<br />' +
'- /roomdesc <em>description</em>: set the room description on the room join page<br />' +
'- /roommod <em>username</em>: appoint a room moderator<br />' +
'- /deroommod <em>username</em>: remove a room moderator<br />' +
'- /declare <em>message</em>: make a global declaration<br />' +
'- /modchat <em>level</em>: set modchat (to turn off: /modchat off)<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' +
'</div>');
},
rule: 'rules',
rules: function(target, room, user) {
if (!this.canBroadcast()) return;
this.sendReplyBox('Please follow the rules:<br />' +
'- <a href="http://pokemonshowdown.com/rules">Rules</a><br />' +
'</div>');
},
faq: function(target, room, user) {
if (!this.canBroadcast()) return;
target = target.toLowerCase();
var buffer = '';
var matched = false;
if (!target || target === 'all') {
matched = true;
buffer += '<a href="http://www.smogon.com/sim/faq">Frequently Asked Questions</a><br />';
}
if (target === 'all' || target === 'deviation') {
matched = true;
buffer += '<a href="http://www.smogon.com/sim/faq#deviation">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="http://www.smogon.com/sim/faq#doubles">Can I play doubles/triples/rotation battles here?</a><br />';
}
if (target === 'all' || target === 'randomcap') {
matched = true;
buffer += '<a href="http://www.smogon.com/sim/faq#randomcap">What is this fakemon and what is it doing in my random battle?</a><br />';
}
if (target === 'all' || target === 'restarts') {
matched = true;
buffer += '<a href="http://www.smogon.com/sim/faq#restarts">Why is the server restarting?</a><br />';
}
if (target === 'all' || target === 'staff') {
matched = true;
buffer += '<a href="http://www.smogon.com/sim/staff_faq">Staff FAQ</a><br />';
}
if (!matched) {
return this.sendReply('The FAQ entry "'+target+'" was not found. Try /faq for general help.');
}
this.sendReplyBox(buffer);
},
banlists: 'tiers',
tier: 'tiers',
tiers: function(target, room, user) {
if (!this.canBroadcast()) return;
target = toId(target);
var buffer = '';
var matched = false;
if (!target || target === 'all') {
matched = true;
buffer += '- <a href="http://www.smogon.com/tiers/">Smogon Tiers</a><br />';
buffer += '- <a href="http://www.smogon.com/bw/banlist/">The banlists for each tier</a><br />';
}
if (target === 'all' || target === 'ubers' || target === 'uber') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/tiers/uber">Uber Pokemon</a><br />';
}
if (target === 'all' || target === 'overused' || target === 'ou') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/tiers/ou">Overused Pokemon</a><br />';
}
if (target === 'all' || target === 'underused' || target === 'uu') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/tiers/uu">Underused Pokemon</a><br />';
}
if (target === 'all' || target === 'rarelyused' || target === 'ru') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/tiers/ru">Rarelyused Pokemon</a><br />';
}
if (target === 'all' || target === 'neverused' || target === 'nu') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/tiers/nu">Neverused Pokemon</a><br />';
}
if (target === 'all' || target === 'littlecup' || target === 'lc') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/tiers/lc">Little Cup Pokemon</a><br />';
}
if (target === 'all' || target === 'doubles') {
matched = true;
buffer += '- <a href="http://www.smogon.com/bw/metagames/doubles">Doubles</a><br />';
}
if (!matched) {
return this.sendReply('The Tiers entry "'+target+'" was not found. Try /tiers for general help.');
}
this.sendReplyBox(buffer);
},
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 atLeastOne = false;
var generation = (targets[1] || "bw").trim().toLowerCase();
var genNumber = 5;
var doublesFormats = {'vgc2012':1,'vgc2013':1,'doubles':1};
var doublesFormat = (!targets[2] && generation in doublesFormats)? generation : (targets[2] || '').trim().toLowerCase();
var doublesText = '';
if (generation === "bw" || generation === "bw2" || generation === "5" || generation === "five") {
generation = "bw";
} 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 = "bw";
}
if (doublesFormat !== '') {
// Smogon only has doubles formats analysis from gen 5 onwards.
if (!(generation in {'bw':1,'xy':1}) || !(doublesFormat in doublesFormats)) {
doublesFormat = '';
} else {
doublesText = {'vgc2012':'VGC 2012 ','vgc2013':'VGC 2013 ','doubles':'Doubles '}[doublesFormat];
doublesFormat = '/' + doublesFormat;
}
}
// Pokemon
if (pokemon.exists) {
atLeastOne = true;
if (genNumber < pokemon.gen) {
return this.sendReplyBox(pokemon.name+' did not exist in '+generation.toUpperCase()+'!');
}
if (pokemon.tier === 'G4CAP' || pokemon.tier === 'G5CAP') {
generation = "cap";
}
var poke = pokemon.name.toLowerCase();
if (poke === 'nidoranm') poke = 'nidoran-m';
if (poke === 'nidoranf') poke = 'nidoran-f';
if (poke === 'farfetch\'d') poke = 'farfetchd';
if (poke === 'mr. mime') poke = 'mr_mime';
if (poke === 'mime jr.') poke = 'mime_jr';
if (poke === 'deoxys-attack' || poke === 'deoxys-defense' || poke === 'deoxys-speed' || poke === 'kyurem-black' || poke === 'kyurem-white') poke = poke.substr(0,8);
if (poke === 'wormadam-trash') poke = 'wormadam-s';
if (poke === 'wormadam-sandy') poke = 'wormadam-g';
if (poke === 'rotom-wash' || poke === 'rotom-frost' || poke === 'rotom-heat') poke = poke.substr(0,7);
if (poke === 'rotom-mow') poke = 'rotom-c';
if (poke === 'rotom-fan') poke = 'rotom-s';
if (poke === 'giratina-origin' || poke === 'tornadus-therian' || poke === 'landorus-therian') poke = poke.substr(0,10);
if (poke === 'shaymin-sky') poke = 'shaymin-s';
if (poke === 'arceus') poke = 'arceus-normal';
if (poke === 'thundurus-therian') poke = 'thundurus-t';
this.sendReplyBox('<a href="http://www.smogon.com/'+generation+'/pokemon/'+poke+doublesFormat+'">'+generation.toUpperCase()+' '+doublesText+pokemon.name+' analysis</a>, brought to you by <a href="http://www.smogon.com">Smogon University</a>');
}
// Item
if (item.exists && genNumber > 1 && item.gen <= genNumber) {
atLeastOne = true;
var itemName = item.name.toLowerCase().replace(' ', '_');
this.sendReplyBox('<a href="http://www.smogon.com/'+generation+'/items/'+itemName+'">'+generation.toUpperCase()+' '+item.name+' item analysis</a>, brought to you by <a href="http://www.smogon.com">Smogon University</a>');
}
// Ability
if (ability.exists && genNumber > 2 && ability.gen <= genNumber) {
atLeastOne = true;
var abilityName = ability.name.toLowerCase().replace(' ', '_');
this.sendReplyBox('<a href="http://www.smogon.com/'+generation+'/abilities/'+abilityName+'">'+generation.toUpperCase()+' '+ability.name+' ability analysis</a>, brought to you by <a href="http://www.smogon.com">Smogon University</a>');
}
// Move
if (move.exists && move.gen <= genNumber) {
atLeastOne = true;
var moveName = move.name.toLowerCase().replace(' ', '_');
this.sendReplyBox('<a href="http://www.smogon.com/'+generation+'/moves/'+moveName+'">'+generation.toUpperCase()+' '+move.name+' move analysis</a>, brought to you by <a href="http://www.smogon.com">Smogon University</a>');
}
if (!atLeastOne) {
return this.sendReplyBox('Pokemon, item, move, or ability not found for generation ' + generation.toUpperCase() + '.');
}
},
/*********************************************************
* Miscellaneous commands
*********************************************************/
birkal: function(target, room, user) {
this.sendReply("It's not funny anymore.");
},
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+'.');
}
},
register: function() {
if (!this.canBroadcast()) return;
this.sendReply("You must win a rated battle to register.");
},
br: 'banredirect',
banredirect: function() {
this.sendReply('/banredirect - This command is obsolete and has been removed.');
},
lobbychat: function(target, room, user, connection) {
if (!Rooms.lobby) return this.popupReply("This server doesn't have a lobby.");
target = toId(target);
if (target === 'off') {
user.leaveRoom(Rooms.lobby, connection.socket);
connection.send('|users|');
this.sendReply('You are now blocking lobby chat.');
} else {
user.joinRoom(Rooms.lobby, connection);
this.sendReply('You are now receiving lobby chat.');
}
},
a: function(target, room, user) {
if (!this.can('battlemessage')) return false;
// secret sysop command
room.add(target);
},
/*********************************************************
* Help commands
*********************************************************/
commands: 'help',
h: 'help',
'?': 'help',
help: function(target, room, user) {
target = target.toLowerCase();
var matched = false;
if (target === 'all' || target === 'msg' || target === 'pm' || target === 'whisper' || target === 'w') {
matched = true;
this.sendReply('/msg OR /whisper OR /w [username], [message] - Send a private message.');
}
if (target === 'all' || target === 'r' || target === 'reply') {
matched = true;
this.sendReply('/reply OR /r [message] - Send a private message to the last person you received a message from, or sent a message to.');
}
if (target === 'all' || target === 'getip' || target === 'ip') {
matched = true;
this.sendReply('/ip - Get your own IP address.');
this.sendReply('/ip [username] - Get a user\'s IP address. Requires: @ & ~');
}
if (target === 'all' || target === 'rating' || target === 'ranking' || target === 'rank' || target === 'ladder') {
matched = true;
this.sendReply('/rating - Get your own rating.');
this.sendReply('/rating [username] - Get user\'s rating.');
}
if (target === 'all' || target === 'nick') {
matched = true;
this.sendReply('/nick [new username] - Change your username.');
}
if (target === 'all' || target === 'avatar') {
matched = true;
this.sendReply('/avatar [new avatar number] - Change your trainer sprite.');
}
if (target === 'all' || target === 'rooms') {
matched = true;
this.sendReply('/rooms [username] - Show what rooms a user is in.');
}
if (target === 'all' || target === 'whois') {
matched = true;
this.sendReply('/whois [username] - Get details on a username: group, and rooms.');
}
if (target === 'all' || target === 'data') {
matched = true;
this.sendReply('/data [pokemon/item/move/ability] - Get details on this pokemon/item/move/ability.');
this.sendReply('!data [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ & ~');
}
if (target === "all" || target === 'analysis') {
matched = true;
this.sendReply('/analysis [pokemon], [generation] - Links to the Smogon University analysis for this Pokemon in the given generation.');
this.sendReply('!analysis [pokemon], [generation] - Shows everyone this link. Requires: + % @ & ~');
}
if (target === 'all' || target === 'groups') {
matched = true;
this.sendReply('/groups - Explains what the + % @ & next to people\'s names mean.');
this.sendReply('!groups - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'opensource') {
matched = true;
this.sendReply('/opensource - Links to PS\'s source code repository.');
this.sendReply('!opensource - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'avatars') {
matched = true;
this.sendReply('/avatars - Explains how to change avatars.');
this.sendReply('!avatars - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'intro') {
matched = true;
this.sendReply('/intro - Provides an introduction to competitive pokemon.');
this.sendReply('!intro - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'cap') {
matched = true;
this.sendReply('/cap - Provides an introduction to the Create-A-Pokemon project.');
this.sendReply('!cap - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'om') {
matched = true;
this.sendReply('/om - Provides links to information on the Other Metagames.');
this.sendReply('!om - Show everyone that information. Requires: + % @ & ~');
}
if (target === 'all' || target === 'learn' || target === 'learnset' || target === 'learnall') {
matched = true;
this.sendReply('/learn [pokemon], [move, move, ...] - Displays how a Pokemon can learn the given moves, if it can at all.')
this.sendReply('!learn [pokemon], [move, move, ...] - Show everyone that information. Requires: + % @ & ~')
}
if (target === 'all' || target === 'calc' || target === 'caclulator') {
matched = true;
this.sendReply('/calc - Provides a link to a damage calculator');
this.sendReply('!calc - Shows everyone a link to a damage calculator. Requires: + % @ & ~');
}
if (target === 'all' || target === 'blockchallenges' || target === 'away' || target === 'idle') {
matched = true;
this.sendReply('/away - Blocks challenges so no one can challenge you. Deactivate it with /back.');
}
if (target === 'all' || target === 'allowchallenges' || target === 'back') {
matched = true;
this.sendReply('/back - Unlocks challenges so you can be challenged again. Deactivate it with /away.');
}
if (target === 'all' || target === 'faq') {
matched = true;
this.sendReply('/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.');
this.sendReply('!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: + % @ & ~');
}
if (target === 'all' || target === 'highlight') {
matched = true;
this.sendReply('Set up highlights:');
this.sendReply('/highlight add, word - add a new word to the highlight list.');
this.sendReply('/highlight list - list all words that currently highlight you.');
this.sendReply('/highlight delete, word - delete a word from the highlight list.');
this.sendReply('/highlight delete - clear the highlight list');
}
if (target === 'all' || target === 'timestamps') {
matched = true;
this.sendReply('Set your timestamps preference:');
this.sendReply('/timestamps [all|lobby|pms], [minutes|seconds|off]');
this.sendReply('all - change all timestamps preferences, lobby - change only lobby chat preferences, pms - change only PM preferences');
this.sendReply('off - set timestamps off, minutes - show timestamps of the form [hh:mm], seconds - show timestamps of the form [hh:mm:ss]');
}
if (target === 'all' || target === 'effectiveness') {
matched = true;
this.sendReply('/effectiveness [type1], [type2] - Provides the effectiveness of a [type1] attack to a [type2] Pokémon.');
this.sendReply('!effectiveness [type1], [type2] - Shows everyone the effectiveness of a [type1] attack to a [type2] Pokémon.');
}
if (target === 'all' || target === 'dexsearch') {
matched = true;
this.sendReply('Searches for Pokemon that fulfill the selected criteria.');
this.sendReply('Search categories are: type, tier, color, moves, ability, gen.');
this.sendReply('Valid colors are: green, red, blue, white, brown, yellow, purple, pink, gray and black.');
this.sendReply('Valid tiers are: Uber/OU/BL/UU/BL2/RU/NU/NFE/LC/CAP/Illegal.');
this.sendReply('Types must be followed by " type", e.g., "dragon type".');
this.sendReply('/dexsearch [type], [move], [move],...');
this.sendReply('The order of the parameters does not matter.');
}
if (target === '%' || target === 'roomban') {
matched = true;
this.sendReply('/roomban [username] - Bans the user from the room you are in. Requires: % @ & ~');
}
if (target === '%' || target === 'roomunban') {
matched = true;
this.sendReply('/roomunban [username] - Unbans the user from the room you are in. Requires: % @ & ~');
}
if (target === '%' || target === 'modnote') {
matched = true;
this.sendReply('/modnote [note] - Adds a moderator note that can be read through modlog. Requires: % @ & ~');
}
if (target === '%' || target === 'altcheck' || target === 'alt' || target === 'alts' || target === 'getalts') {
matched = true;
this.sendReply('/alts OR /altcheck OR /alt OR /getalts [username] - Get a user\'s alts. Requires: % @ & ~');
}
if (target === '%' || target === 'forcerename' || target === 'fr') {
matched = true;
this.sendReply('/forcerename OR /fr [username], [reason] - Forcibly change a user\'s name and shows them the [reason]. Requires: % @ & ~');
}
if (target === '%' || target === 'redir' || target === 'redirect') {
matched = true;
this.sendReply('/redirect OR /redir [username], [room] - Forcibly move a user from the current room to [room]. Requires: % @ & ~');
}
if (target === '@' || target === 'ban' || target === 'b') {
matched = true;
this.sendReply('/ban OR /b [username], [reason] - Kick user from all rooms and ban user\'s IP address with reason. Requires: @ & ~');
}
if (target === '@' || target === 'unban') {
matched = true;
this.sendReply('/unban [username] - Unban a user. Requires: @ & ~');
}
if (target === '@' || target === 'unbanall') {
matched = true;
this.sendReply('/unbanall - Unban all IP addresses. Requires: @ & ~');
}
if (target === '%' || target === 'modlog') {
matched = true;
this.sendReply('/modlog [n] - If n is a number or omitted, display the last n lines of the moderator log. Defaults to 15. If n is not a number, search the moderator log for "n". Requires: % @ & ~');
}
if (target === "%" || target === 'kickbattle ') {
matched = true;
this.sendReply('/kickbattle [username], [reason] - Kicks an user from a battle with reason. Requires: % @ & ~');
}
if (target === "%" || target === 'warn' || target === 'k') {
matched = true;
this.sendReply('/warn OR /k [username], [reason] - Warns a user showing them the Pokemon Showdown Rules and [reason] in an overlay. Requires: % @ & ~');
}
if (target === '%' || target === 'mute' || target === 'm') {
matched = true;
this.sendReply('/mute OR /m [username], [reason] - Mute user with reason for 7 minutes. Requires: % @ & ~');
}
if (target === '%' || target === 'hourmute') {
matched = true;
this.sendReply('/hourmute , [reason] - Mute user with reason for an hour. Requires: % @ & ~');
}
if (target === '%' || target === 'unmute') {
matched = true;
this.sendReply('/unmute [username] - Remove mute from user. Requires: % @ & ~');
}
if (target === '&' || target === 'promote') {
matched = true;
this.sendReply('/promote [username], [group] - Promotes the user to the specified group or next ranked group. Requires: & ~');
}
if (target === '&' || target === 'demote') {
matched = true;
this.sendReply('/demote [username], [group] - Demotes the user to the specified group or previous ranked group. Requires: & ~');
}
if (target === '~' || target === 'forcerenameto' || target === 'frt') {
matched = true;
this.sendReply('/forcerenameto OR /frt [username] - Force a user to choose a new name. Requires: & ~');
this.sendReply('/forcerenameto OR /frt [username], [new name] - Forcibly change a user\'s name to [new name]. Requires: & ~');
}
if (target === '&' || target === 'forcetie') {
matched = true;
this.sendReply('/forcetie - Forces the current match to tie. Requires: & ~');
}
if (target === '&' || target === 'declare' ) {
matched = true;
this.sendReply('/declare [message] - Anonymously announces a message. Requires: & ~');
}
if (target === '&' || target === 'potd' ) {
matched = true;
this.sendReply('/potd [pokemon] - Sets the Random Battle Pokemon of the Day. Requires: & ~');
}
if (target === '%' || target === 'announce' || target === 'wall' ) {
matched = true;
this.sendReply('/announce OR /wall [message] - Makes an announcement. Requires: % @ & ~');
}
if (target === '@' || target === 'modchat') {
matched = true;
this.sendReply('/modchat [off/registered/+/%/@/&/~] - Set the level of moderated chat. Requires: @ & ~');
}
if (target === '~' || target === 'hotpatch') {
matched = true;
this.sendReply('Hot-patching the game engine allows you to update parts of Showdown without interrupting currently-running battles. Requires: ~');
this.sendReply('Hot-patching has greater memory requirements than restarting.');
this.sendReply('/hotpatch chat - reload chat-commands.js');
this.sendReply('/hotpatch battles - spawn new simulator processes');
this.sendReply('/hotpatch formats - reload the tools.js tree, rebuild and rebroad the formats list, and also spawn new simulator processes');
}
if (target === '~' || target === 'lockdown') {
matched = true;
this.sendReply('/lockdown - locks down the server, which prevents new battles from starting so that the server can eventually be restarted. Requires: ~');
}
if (target === '~' || target === 'kill') {
matched = true;
this.sendReply('/kill - kills the server. Can\'t be done unless the server is in lockdown state. Requires: ~');
}
if (target === 'all' || target === 'help' || target === 'h' || target === '?' || target === 'commands') {
matched = true;
this.sendReply('/help OR /h OR /? - Gives you help.');
}
if (!target) {
this.sendReply('COMMANDS: /msg, /reply, /ip, /rating, /nick, /avatar, /rooms, /whois, /help, /away, /back, /timestamps');
this.sendReply('INFORMATIONAL COMMANDS: /data, /groups, /opensource, /avatars, /faq, /rules, /intro, /tiers, /othermetas, /learn, /analysis, /calc (replace / with ! to broadcast. (Requires: + % @ & ~))');
this.sendReply('For details on all commands, use /help all');
if (user.group !== config.groupsranking[0]) {
this.sendReply('DRIVER COMMANDS: /mute, /unmute, /announce, /forcerename, /alts')
this.sendReply('MODERATOR COMMANDS: /ban, /unban, /unbanall, /ip, /modlog, /redirect, /kick');
this.sendReply('LEADER COMMANDS: /promote, /demote, /forcewin, /forcetie, /declare');
this.sendReply('For details on all moderator commands, use /help @');
}
this.sendReply('For details of a specific command, use something like: /help data');
} else if (!matched) {
this.sendReply('The command "/'+target+'" was not found. Try /help for general help');
}
},
};