';
if (hasThousandArrows) {
buffer += " Thousand Arrows has neutral type effectiveness on Flying-type Pok\u00e9mon if not already smacked down.";
}
this.sendReplyBox('Coverage for ' + sources.join(' + ') + ': ' + buffer);
}
},
coveragehelp: ["/coverage [move 1], [move 2] ... - Provides the best effectiveness match-up against all defending types for given moves or attacking types",
"!coverage [move 1], [move 2] ... - Shows this information to everyone.",
"Adding the parameter 'all' or 'table' will display the information with a table of all type combinations."],
statcalc: function (target, room, user) {
if (!target) return this.parse("/help statcalc");
if (!this.runBroadcast()) return;
let targets = target.split(' ');
let lvlSet, natureSet, ivSet, evSet, baseSet, modSet = false;
let pokemon;
let useStat = '';
let level = 100;
let calcHP = false;
let nature = 1.0;
let iv = 31;
let ev = 252;
let statValue = -1;
let modifier = 0;
let positiveMod = true;
for (let i = 0; i < targets.length; i++) {
let lowercase = targets[i].toLowerCase();
if (!lvlSet) {
if (lowercase === 'lc') {
level = 5;
lvlSet = true;
continue;
} else if (lowercase === 'vgc') {
level = 50;
lvlSet = true;
continue;
} else if (lowercase.startsWith('lv') || lowercase.startsWith('level')) {
level = parseInt(targets[i].replace(/\D/g, ''));
lvlSet = true;
if (level < 1 || level > 9999) {
return this.sendReplyBox('Invalid value for level: ' + level);
}
continue;
}
}
if (!useStat) {
switch (lowercase) {
case 'hp':
case 'hitpoints':
calcHP = true;
useStat = 'hp';
continue;
case 'atk':
case 'attack':
useStat = 'atk';
continue;
case 'def':
case 'defense':
useStat = 'def';
continue;
case 'spa':
useStat = 'spa';
continue;
case 'spd':
case 'sdef':
useStat = 'spd';
continue;
case 'spe':
case 'speed':
useStat = 'spe';
continue;
}
}
if (!natureSet) {
if (lowercase === 'boosting' || lowercase === 'positive') {
nature = 1.1;
natureSet = true;
continue;
} else if (lowercase === 'negative' || lowercase === 'inhibiting') {
nature = 0.9;
natureSet = true;
continue;
} else if (lowercase === 'neutral') {
continue;
}
}
if (!ivSet) {
if (lowercase.endsWith('iv') || lowercase.endsWith('ivs')) {
iv = parseInt(targets[i]);
ivSet = true;
if (isNaN(iv)) {
return this.sendReplyBox('Invalid value for IVs: ' + Tools.escapeHTML(targets[i]));
}
continue;
}
}
if (!evSet) {
if (lowercase === 'invested' || lowercase === 'max') {
evSet = true;
} else if (lowercase === 'uninvested') {
ev = 0;
evSet = true;
} else if (lowercase.endsWith('ev') || lowercase.endsWith('evs') || lowercase.endsWith('+') || lowercase.endsWith('-')) {
ev = parseInt(targets[i]);
evSet = true;
if (isNaN(ev)) {
return this.sendReplyBox('Invalid value for EVs: ' + Tools.escapeHTML(targets[i]));
}
if (ev > 255 || ev < 0) {
return this.sendReplyBox('The amount of EVs should be between 0 and 255.');
}
if (!natureSet) {
if (targets[i].indexOf('+') > -1) {
nature = 1.1;
natureSet = true;
} else if (targets[i].indexOf('-') > -1) {
nature = 0.9;
natureSet = true;
}
}
continue;
}
}
if (!modSet) {
if (targets[i] === 'scarf' || targets[i] === 'specs' || targets[i] === 'band') {
modifier = 1;
modSet = true;
} else if (targets[i].charAt(0) === '+') {
modifier = parseInt(targets[i].charAt(1));
modSet = true;
} else if (targets[i].charAt(0) === '-') {
positiveMod = false;
modifier = parseInt(targets[i].charAt(1));
modSet = true;
}
if (isNaN(modifier)) {
return this.sendReplyBox('Invalid value for modifier: ' + Tools.escapeHTML(modifier));
}
if (modifier > 6) {
return this.sendReplyBox('Modifier should be a number between -6 and +6');
}
}
if (!pokemon) {
let testPoke = Tools.getTemplate(targets[i]);
if (testPoke.baseStats) {
pokemon = testPoke.baseStats;
baseSet = true;
continue;
}
}
let tempStat = parseInt(targets[i]);
if (!isNaN(tempStat) && !baseSet && tempStat > 0 && tempStat < 256) {
statValue = tempStat;
baseSet = true;
}
}
if (pokemon) {
if (useStat) {
statValue = pokemon[useStat];
} else {
return this.sendReplyBox('No stat found.');
}
}
if (statValue < 0) {
return this.sendReplyBox('No valid value for base stat found.');
}
let output;
if (calcHP) {
output = (((iv + (2 * statValue) + (ev / 4) + 100) * level) / 100) + 10;
} else {
output = Math.floor(nature * Math.floor((((iv + (2 * statValue) + (ev / 4)) * level) / 100) + 5));
if (positiveMod) {
output *= (2 + modifier) / 2;
} else {
output *= 2 / (2 + modifier);
}
}
return this.sendReplyBox('Base ' + statValue + (calcHP ? ' HP ' : ' ') + 'at level ' + level + ' with ' + iv + ' IVs, ' + ev + (nature === 1.1 ? '+' : (nature === 0.9 ? '-' : '')) + ' EVs' + (modifier > 0 && !calcHP ? ' at ' + (positiveMod ? '+' : '-') + modifier : '') + ': ' + Math.floor(output) + '.');
},
statcalchelp: ["/statcalc [level] [base stat] [IVs] [nature] [EVs] [modifier] (only base stat is required) - Calculates what the actual stat of a Pokémon is with the given parameters. For example, '/statcalc lv50 100 30iv positive 252ev scarf' calculates the speed of a base 100 scarfer with HP Ice in Battle Spot, and '/statcalc uninvested 90 neutral' calculates the attack of an uninvested Crobat.",
"!statcalc [level] [base stat] [IVs] [nature] [EVs] [modifier] (only base stat is required) - Shows this information to everyone.",
"Inputing 'hp' as an argument makes it use the formula for HP. Instead of giving nature, '+' and '-' can be appended to the EV amount (e.g. 252+ev) to signify a boosting or inhibiting nature."],
/*********************************************************
* Informational commands
*********************************************************/
uptime: function (target, room, user) {
if (!this.runBroadcast()) return;
let uptime = process.uptime();
let uptimeText;
if (uptime > 24 * 60 * 60) {
let uptimeDays = Math.floor(uptime / (24 * 60 * 60));
uptimeText = uptimeDays + " " + (uptimeDays === 1 ? "day" : "days");
let uptimeHours = Math.floor(uptime / (60 * 60)) - uptimeDays * 24;
if (uptimeHours) uptimeText += ", " + uptimeHours + " " + (uptimeHours === 1 ? "hour" : "hours");
} else {
uptimeText = Tools.toDurationString(uptime * 1000);
}
this.sendReplyBox("Uptime: " + uptimeText + "");
},
groups: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"+ Voice - They can use ! commands like !groups, and talk during moderated chat " +
"% Driver - The above, and they can mute. Global % can also lock users and check for alts " +
"@ Moderator - The above, and they can ban users " +
"& Leader - The above, and they can promote to moderator and force ties " +
"# Room Owner - They are leaders of the room and can almost totally control it " +
"~ Administrator - They can do anything, like change what this message says"
);
},
groupshelp: ["/groups - Explains what the + % @ # & next to people's names mean.",
"!groups - Shows everyone that information. Requires: + % @ # & ~"],
repo: 'opensource',
repository: 'opensource',
git: 'opensource',
opensource: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"Pokémon Showdown is open source: " +
"- Language: JavaScript (Node.js) " +
"- What's new? " +
"- Server source code " +
"- Client source code"
);
},
opensourcehelp: ["/opensource - Links to PS's source code repository.",
"!opensource - Show everyone that information. Requires: + % @ # & ~"],
staff: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox("Pokémon Showdown Staff List");
},
forums: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox("Pokémon Showdown Forums");
},
suggestions: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox("Make a suggestion for Pokémon Showdown");
},
bugreport: 'bugs',
bugs: function (target, room, user) {
if (!this.runBroadcast()) return;
if (room.battle) {
this.sendReplyBox("
");
} else {
this.sendReplyBox(
"Have a replay showcasing a bug on Pokémon Showdown? " +
"- Questions " +
"- Bug Reports"
);
}
},
avatars: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox("You can by clicking on it in the menu in the upper right. Custom avatars are only obtainable by staff.");
},
avatarshelp: ["/avatars - Explains how to change avatars.",
"!avatars - Show everyone that information. Requires: + % @ # & ~"],
introduction: 'intro',
intro: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"New to competitive Pokémon? " +
"- Beginner's Guide to Pokémon Showdown " +
"- An introduction to competitive Pokémon " +
"- What do 'OU', 'UU', etc mean? " +
"- What are the rules for each format? What is 'Sleep Clause'?"
);
},
introhelp: ["/intro - Provides an introduction to competitive Pok\u00e9mon.",
"!intro - Show everyone that information. Requires: + % @ # & ~"],
mentoring: 'smogintro',
smogonintro: 'smogintro',
smogintro: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"Welcome to Smogon's official simulator! The Smogon Info / Intro Hub can help you get integrated into the community. " +
"- Useful Smogon Info " +
"- Tiering FAQ "
);
},
calculator: 'calc',
calc: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"Pokémon Showdown! damage calculator. (Courtesy of Honko) " +
"- Damage Calculator"
);
},
calchelp: ["/calc - Provides a link to a damage calculator",
"!calc - Shows everyone a link to a damage calculator. Requires: + % @ # & ~"],
capintro: 'cap',
cap: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"An introduction to the Create-A-Pokémon project: " +
"- CAP project website and description " +
"- What Pokémon have been made? " +
"- Talk about the metagame here " +
"- Sample XY CAP teams"
);
},
caphelp: ["/cap - Provides an introduction to the Create-A-Pokémon project.",
"!cap - Show everyone that information. Requires: + % @ # & ~"],
gennext: function (target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(
"NEXT (also called Gen-NEXT) is a mod that makes changes to the game: " +
"- README: overview of NEXT " +
"Example replays: " +
"- Zergo vs Mr Weegle Snarf " +
"- NickMP vs Khalogie"
);
},
om: 'othermetas',
othermetas: function (target, room, user) {
if (!this.runBroadcast()) return;
target = toId(target);
let buffer = "";
if (target === 'all' && this.broadcasting) {
return this.sendReplyBox("You cannot broadcast information about all Other Metagames at once.");
}
if (!target || target === 'all') {
buffer += "- Other Metagames Forum ";
buffer += "- Other Metagames Analyses ";
if (!target) return this.sendReplyBox(buffer);
}
let showMonthly = (target === 'all' || target === 'omofthemonth' || target === 'omotm' || target === 'month');
let monthBuffer = "- Other Metagame of the Month";
if (target === 'all') {
// Display OMotM formats, with forum thread links as caption
this.parse('/formathelp omofthemonth');
if (showMonthly) this.sendReply('|raw|
' + monthBuffer + '
');
// Display the rest of OM formats, with OM hub/index forum links as caption
this.parse('/formathelp othermetagames');
return this.sendReply('|raw|
' + buffer + '
');
}
if (showMonthly) {
this.target = 'omofthemonth';
this.run('formathelp');
this.sendReply('|raw|
' + monthBuffer + '
');
} else {
this.run('formathelp');
}
},
othermetashelp: ["/om - Provides links to information on the Other Metagames.",
"!om - Show everyone that information. Requires: + % @ # & ~"],
banlists: 'formathelp',
tier: 'formathelp',
tiers: 'formathelp',
formats: 'formathelp',
tiershelp: 'formathelp',
formatshelp: 'formathelp',
formathelp: function (target, room, user, connection, cmd) {
if (!this.runBroadcast()) return;
if (!target) {
return this.sendReplyBox(
"- Smogon Tiers " +
"- Tiering FAQ " +
"- The banlists for each tier " +
" Type /formatshelp [format|section] to get details about an available format or group of formats."
);
}
let targetId = toId(target);
if (targetId === 'ladder') targetId = 'search';
if (targetId === 'all') targetId = '';
let formatList;
let format = Tools.getFormat(targetId);
if (format.effectType === 'Format') formatList = [targetId];
if (!formatList) {
if (this.broadcasting && (cmd !== 'om' && cmd !== 'othermetas')) return this.sendReply("'" + target + "' is not a format. This command's search mode is too spammy to broadcast.");
formatList = Object.keys(Tools.data.Formats).filter(formatid => Tools.data.Formats[formatid].effectType === 'Format');
}
// Filter formats and group by section
let exactMatch = '';
let sections = {};
let totalMatches = 0;
for (let i = 0; i < formatList.length; i++) {
let format = Tools.getFormat(formatList[i]);
let sectionId = toId(format.section);
if (targetId && !format[targetId + 'Show'] && sectionId !== targetId && format.id === formatList[i] && !format.id.startsWith(targetId)) continue;
totalMatches++;
if (!sections[sectionId]) sections[sectionId] = {name: format.section, formats: []};
sections[sectionId].formats.push(format.id);
if (format.id !== targetId) continue;
exactMatch = sectionId;
break;
}
if (!totalMatches) return this.sendReply("No " + (target ? "matched " : "") + "formats found.");
if (totalMatches === 1) {
let format = Tools.getFormat(Object.values(sections)[0].formats[0]);
let formatType = (format.gameType || "singles");
formatType = formatType.charAt(0).toUpperCase() + formatType.slice(1).toLowerCase();
if (!format.desc) return this.sendReplyBox("No description found for this " + formatType + " " + format.section + " format.");
return this.sendReplyBox(format.desc.join(" "));
}
// Build tables
let buf = [];
for (let sectionId in sections) {
if (exactMatch && sectionId !== exactMatch) continue;
buf.push("
");
},
roomhelp: function (target, room, user) {
if (room.id === 'lobby' || room.battle) return this.sendReply("This command is too spammy for lobby/battles.");
if (!this.runBroadcast()) return;
this.sendReplyBox(
"Room drivers (%) can use: " +
"- /warn OR /k username: warn a user and show the Pokémon Showdown rules " +
"- /mute OR /m username: 7 minute mute " +
"- /hourmute OR /hm username: 60 minute mute " +
"- /unmute username: unmute " +
"- /announce OR /wall message: make an announcement " +
"- /modlog username: search the moderator log of the room " +
"- /modnote note: adds a moderator note that can be read through modlog " +
" " +
"Room moderators (@) can also use: " +
"- /roomban OR /rb username: bans user from the room " +
"- /roomunban username: unbans user from the room " +
"- /roomvoice username: appoint a room voice " +
"- /roomdevoice username: remove a room voice " +
"- /modchat [off/autoconfirmed/+]: set modchat level " +
"- /staffintro intro: sets the staff introduction that will be displayed for all staff joining the room " +
" " +
"Room owners (#) can also use: " +
"- /roomintro intro: sets the room introduction that will be displayed for all users joining the room " +
"- /rules rules link: set the room rules link seen when using /rules " +
"- /roommod, /roomdriver username: appoint a room moderator/driver " +
"- /roomdemod, /roomdedriver username: remove a room moderator/driver " +
"- /roomdeauth username: remove all room auth from a user " +
"- /modchat [%/@/#]: set modchat level " +
"- /declare message: make a large blue declaration to the room " +
"- !htmlbox HTML code: broadcasts a box of HTML code to the room " +
"- !showimage [url], [width], [height]: shows an image to the room " +
" " +
"More detailed help can be found in the roomauth guide " +
" " +
"Tournament Help: " +
"- /tour create format, elimination: Creates a new single elimination tournament in the current room. " +
"- /tour create format, roundrobin: Creates a new round robin tournament in the current room. " +
"- /tour end: Forcibly ends the tournament in the current room " +
"- /tour start: Starts the tournament in the current room " +
" " +
"More detailed help can be found here " +
""
);
},
restarthelp: function (target, room, user) {
if (room.id === 'lobby' && !this.can('lockdown')) return false;
if (!this.runBroadcast()) return;
this.sendReplyBox(
"The server is restarting. Things to know: " +
"- We wait a few minutes before restarting so people can finish up their battles " +
"- The restart itself will take around 0.6 seconds " +
"- Your ladder ranking and teams will not change " +
"- We are restarting to update Pokémon Showdown to a newer version"
);
},
processes: function (target, room, user) {
if (!this.can('lockdown')) return false;
let buf = "" + process.pid + " - Main ";
for (let i in Sockets.workers) {
let worker = Sockets.workers[i];
buf += "" + (worker.pid || worker.process.pid) + " - Sockets " + i + " ";
}
const ProcessManager = require('./../process-manager');
for (let managerData of ProcessManager.cache) {
let i = 0;
let processType = path.basename(managerData[1]);
for (let process of managerData[0].processes) {
buf += "" + process.process.pid + " - " + processType + " " + (i++) + " ";
}
}
this.sendReplyBox(buf);
},
rule: 'rules',
rules: function (target, room, user) {
if (!target) {
if (!this.runBroadcast()) return;
this.sendReplyBox("Please follow the rules: " +
(room.rulesLink ? "- " + Tools.escapeHTML(room.title) + " room rules " : "") +
"- " + (room.rulesLink ? "Global rules" : "Rules") + "");
return;
}
if (!this.can('roommod', null, room)) return;
if (target.length > 100) {
return this.errorReply("Error: Room rules link is too long (must be under 100 characters). You can use a URL shortener to shorten the link.");
}
room.rulesLink = target.trim();
this.sendReply("(The room rules link is now: " + target + ")");
if (room.chatRoomData) {
room.chatRoomData.rulesLink = room.rulesLink;
Rooms.global.writeChatRoomData();
}
},
ruleshelp: ["/rules - Show links to room rules and global rules.",
"!rules - Show everyone links to room rules and global rules. Requires: + % @ # & ~",
"/rules [url] - Change the room rules URL. Requires: # & ~"],
faq: function (target, room, user) {
if (!this.runBroadcast()) return;
target = target.toLowerCase();
let buffer = "";
let matched = false;
if (target === 'all' && this.broadcasting) {
return this.sendReplyBox("You cannot broadcast all FAQs at once.");
}
if (!target || target === 'all') {
matched = true;
buffer += "Frequently Asked Questions ";
}
if (target === 'all' || target === 'elo') {
matched = true;
buffer += "Why did this user gain or lose so many points? ";
}
if (target === 'all' || target === 'doubles' || target === 'triples' || target === 'rotation') {
matched = true;
buffer += "Can I play doubles/triples/rotation battles here? ";
}
if (target === 'all' || target === 'restarts') {
matched = true;
buffer += "Why is the server restarting? ";
}
if (target === 'all' || target === 'star' || target === 'player') {
matched = true;
buffer += 'Why is there this star (★) in front of my username? ';
}
if (target === 'all' || target === 'staff') {
matched = true;
buffer += "Staff FAQ ";
}
if (target === 'all' || target === 'autoconfirmed' || target === 'ac') {
matched = true;
buffer += "A user is autoconfirmed when they have won at least one rated battle and have been registered for a week or longer. ";
}
if (target === 'all' || target === 'customavatar' || target === 'ca') {
matched = true;
buffer += "How can I get a custom avatar? ";
}
if (target === 'all' || target === 'pm' || target === 'msg' || target === 'w') {
matched = true;
buffer += "How can I send a user a private message? ";
}
if (target === 'all' || target === 'challenge' || target === 'chall') {
matched = true;
buffer += "How can I battle a specific user? ";
}
if (target === 'all' || target === 'gxe') {
matched = true;
buffer += "What does GXE mean? ";
}
if (target === 'all' || target === 'coil') {
matched = true;
buffer += "What is COIL? ";
}
if (target === 'all' || target === 'tiering' || target === 'tiers' || target === 'tier') {
matched = true;
buffer += "Tiering FAQ ";
}
if (!matched) {
return this.sendReply("The FAQ entry '" + target + "' was not found. Try /faq for general help.");
}
this.sendReplyBox(buffer);
},
faqhelp: ["/faq [theme] - Provides a link to the FAQ. Add deviation, doubles, randomcap, restart, or staff for a link to these questions. Add all for all of them.",
"!faq [theme] - Shows everyone a link to the FAQ. Add deviation, doubles, randomcap, restart, or staff for a link to these questions. Add all for all of them. Requires: + % @ # & ~"],
analysis: 'smogdex',
strategy: 'smogdex',
smogdex: function (target, room, user) {
if (!this.runBroadcast()) return;
let targets = target.split(',');
let pokemon = Tools.getTemplate(targets[0]);
let item = Tools.getItem(targets[0]);
let move = Tools.getMove(targets[0]);
let ability = Tools.getAbility(targets[0]);
let format = Tools.getFormat(targets[0]);
let atLeastOne = false;
let generation = (targets[1] || 'xy').trim().toLowerCase();
let genNumber = 6;
let extraFormat = Tools.getFormat(targets[2]);
if (generation === 'xy' || generation === 'oras' || generation === '6' || generation === 'six') {
generation = 'xy';
} else if (generation === 'bw' || generation === 'bw2' || generation === '5' || generation === 'five') {
generation = 'bw';
genNumber = 5;
} else if (generation === 'dp' || generation === 'dpp' || generation === '4' || generation === 'four') {
generation = 'dp';
genNumber = 4;
} else if (generation === 'adv' || generation === 'rse' || generation === 'rs' || generation === '3' || generation === 'three') {
generation = 'rs';
genNumber = 3;
} else if (generation === 'gsc' || generation === 'gs' || generation === '2' || generation === 'two') {
generation = 'gs';
genNumber = 2;
} else if (generation === 'rby' || generation === 'rb' || generation === '1' || generation === 'one') {
generation = 'rb';
genNumber = 1;
} else {
generation = 'xy';
}
// Pokemon
if (pokemon.exists) {
atLeastOne = true;
if (genNumber < pokemon.gen) {
return this.sendReplyBox("" + pokemon.name + " did not exist in " + generation.toUpperCase() + "!");
}
if (pokemon.tier === 'CAP') {
generation = 'cap';
this.errorReply("CAP is not currently supported by Smogon Strategic Pokedex.");
}
if (pokemon.battleOnly || pokemon.baseSpecies === 'Keldeo' || pokemon.baseSpecies === 'Genesect') {
pokemon = Tools.getTemplate(pokemon.baseSpecies);
}
let formatName = extraFormat.name;
let formatId = extraFormat.id;
if (formatId === 'doublesou') {
formatId = 'doubles';
} else if (formatId.includes('vgc')) {
formatId = 'vgc' + formatId.slice(-2);
formatName = 'VGC20' + formatId.slice(-2);
} else if (extraFormat.effectType !== 'Format') {
formatName = formatId = '';
}
let speciesid = pokemon.speciesid;
// Special case for Meowstic-M and Hoopa-Unbound
if (speciesid === 'meowstic') speciesid = 'meowsticm';
if (pokemon.tier === 'CAP') {
this.sendReplyBox("" + generation.toUpperCase() + " " + Tools.escapeHTML(formatName) + " " + pokemon.name + " analysis preview, brought to you by Smogon UniversityCAP Project");
} else {
this.sendReplyBox("" + generation.toUpperCase() + " " + Tools.escapeHTML(formatName) + " " + pokemon.name + " analysis, brought to you by Smogon University");
}
}
// Item
if (item.exists && genNumber > 1 && item.gen <= genNumber) {
atLeastOne = true;
this.sendReplyBox("" + generation.toUpperCase() + " " + item.name + " item analysis, brought to you by Smogon University");
}
// Ability
if (ability.exists && genNumber > 2 && ability.gen <= genNumber) {
atLeastOne = true;
this.sendReplyBox("" + generation.toUpperCase() + " " + ability.name + " ability analysis, brought to you by Smogon University");
}
// Move
if (move.exists && move.gen <= genNumber) {
atLeastOne = true;
this.sendReplyBox("" + generation.toUpperCase() + " " + move.name + " move analysis, brought to you by Smogon University");
}
// Format
if (format.id) {
let formatName = format.name;
let formatId = format.id;
if (formatId === 'doublesou') {
formatId = 'doubles';
} else if (formatId.includes('vgc')) {
formatId = 'vgc' + formatId.slice(-2);
formatName = 'VGC20' + formatId.slice(-2);
} else if (format.effectType !== 'Format') {
formatName = formatId = '';
}
if (formatName) {
atLeastOne = true;
this.sendReplyBox("" + generation.toUpperCase() + " " + Tools.escapeHTML(formatName) + " format analysis, brought to you by Smogon University");
}
}
if (!atLeastOne) {
return this.sendReplyBox("Pokémon, item, move, ability, or format not found for generation " + generation.toUpperCase() + ".");
}
},
smogdexhelp: ["/analysis [pokemon], [generation] - Links to the Smogon University analysis for this Pok\u00e9mon in the given generation.",
"!analysis [pokemon], [generation] - Shows everyone this link. Requires: + % @ # & ~"],
veekun: function (target, broadcast, user) {
if (!this.runBroadcast()) return;
let baseLink = 'http://veekun.com/dex/';
let pokemon = Tools.getTemplate(target);
let item = Tools.getItem(target);
let move = Tools.getMove(target);
let ability = Tools.getAbility(target);
let nature = Tools.getNature(target);
let atLeastOne = false;
// Pokemon
if (pokemon.exists) {
atLeastOne = true;
if (pokemon.isNonstandard) return this.sendReply(pokemon.species + ' is not a real Pok\u00e9mon.');
let baseSpecies = pokemon.baseSpecies || pokemon.species;
let forme = pokemon.forme;
// Showdown and Veekun have different naming for this gender difference forme of Meowstic.
if (baseSpecies === 'Meowstic' && forme === 'F') {
forme = 'Female';
}
let link = baseLink + 'pokemon/' + baseSpecies.toLowerCase();
if (forme) {
link += '?form=' + forme.toLowerCase();
}
this.sendReplyBox("" + pokemon.species + " description by Veekun");
}
// Item
if (item.exists) {
atLeastOne = true;
let link = baseLink + 'items/' + item.name.toLowerCase();
this.sendReplyBox("" + item.name + " item description by Veekun");
}
// Ability
if (ability.exists) {
atLeastOne = true;
if (ability.isNonstandard) return this.sendReply(ability.name + ' is not a real ability.');
let link = baseLink + 'abilities/' + ability.name.toLowerCase();
this.sendReplyBox("" + ability.name + " ability description by Veekun");
}
// Move
if (move.exists) {
atLeastOne = true;
if (move.isNonstandard) return this.sendReply(move.name + ' is not a real move.');
let link = baseLink + 'moves/' + move.name.toLowerCase();
this.sendReplyBox("" + move.name + " move description by Veekun");
}
// Nature
if (nature.exists) {
atLeastOne = true;
let link = baseLink + 'natures/' + nature.name.toLowerCase();
this.sendReplyBox("" + nature.name + " nature description by Veekun");
}
if (!atLeastOne) {
return this.sendReplyBox("Pokémon, item, move, ability, or nature not found.");
}
},
veekunhelp: ["/veekun [pokemon] - Links to Veekun website for this pokemon/item/move/ability/nature.",
"!veekun [pokemon] - Shows everyone this link. Requires: + % @ # & ~"],
register: function () {
if (!this.runBroadcast()) return;
this.sendReplyBox('You will be prompted to register upon winning a rated battle. Alternatively, there is a register button in the menu in the upper right.');
},
/*********************************************************
* Miscellaneous commands
*********************************************************/
potd: function (target, room, user) {
if (!this.can('potd')) return false;
Config.potd = target;
Simulator.SimulatorProcess.eval('Config.potd = \'' + toId(target) + '\'');
if (target) {
if (Rooms.lobby) Rooms.lobby.addRaw("
The Pokémon of the Day is now " + target + "! This Pokemon will be guaranteed to show up in random battles.
");
this.logModCommand("The Pok\u00e9mon of the Day was changed to " + target + " by " + user.name + ".");
} else {
if (Rooms.lobby) Rooms.lobby.addRaw("
The Pokémon of the Day was removed! No pokemon will be guaranteed in random battles.
");
this.logModCommand("The Pok\u00e9mon of the Day was removed by " + user.name + ".");
}
},
roll: 'dice',
dice: function (target, room, user) {
if (!target || target.match(/[^d\d\s\-\+HL]/i)) return this.parse('/help dice');
if (!this.runBroadcast()) return;
// ~30 is widely regarded as the sample size required for sum to be a Gaussian distribution.
// This also sets a computation time constraint for safety.
let maxDice = 40;
let diceQuantity = 1;
let diceDataStart = target.indexOf('d');
if (diceDataStart >= 0) {
if (diceDataStart) diceQuantity = Number(target.slice(0, diceDataStart));
target = target.slice(diceDataStart + 1);
if (!Number.isInteger(diceQuantity) || diceQuantity <= 0 || diceQuantity > maxDice) return this.sendReply("The amount of dice rolled should be a natural number up to " + maxDice + ".");
}
let offset = 0;
let removeOutlier = 0;
let modifierData = target.match(/[\-\+]/);
if (modifierData) {
switch (target.slice(modifierData.index).trim().toLowerCase()) {
case '-l':
removeOutlier = -1;
break;
case '-h':
removeOutlier = +1;
break;
default:
offset = Number(target.slice(modifierData.index));
if (isNaN(offset)) return this.parse('/help dice');
if (!Number.isSafeInteger(offset)) return this.errorReply("The specified offset must be an integer up to " + Number.MAX_SAFE_INTEGER + ".");
}
if (removeOutlier && diceQuantity <= 1) return this.errorReply("More than one dice should be rolled before removing outliers.");
target = target.slice(0, modifierData.index);
}
let diceFaces = 6;
if (target.length) {
diceFaces = Number(target);
if (!Number.isSafeInteger(diceFaces) || diceFaces <= 0) {
return this.errorReply("Rolled dice must have a natural amount of faces up to " + Number.MAX_SAFE_INTEGER + ".");
}
}
if (diceQuantity > 1) {
// Make sure that we can deal with high rolls
if (!Number.isSafeInteger(offset < 0 ? diceQuantity * diceFaces : diceQuantity * diceFaces + offset)) {
return this.errorReply("The maximum sum of rolled dice must be lower or equal than " + Number.MAX_SAFE_INTEGER + ".");
}
}
let maxRoll = 0;
let minRoll = Number.MAX_SAFE_INTEGER;
let trackRolls = diceQuantity * (('' + diceFaces).length + 1) <= 60;
let rolls = [];
let rollSum = 0;
for (let i = 0; i < diceQuantity; ++i) {
let curRoll = Math.floor(Math.random() * diceFaces) + 1;
rollSum += curRoll;
if (curRoll > maxRoll) maxRoll = curRoll;
if (curRoll < minRoll) minRoll = curRoll;
if (trackRolls) rolls.push(curRoll);
}
// Apply modifiers
if (removeOutlier > 0) {
rollSum -= maxRoll;
} else if (removeOutlier < 0) {
rollSum -= minRoll;
}
if (offset) rollSum += offset;
// Reply with relevant information
let offsetFragment = "";
if (offset) offsetFragment += (offset > 0 ? "+" + offset : offset);
if (diceQuantity === 1) return this.sendReplyBox("Roll (1 - " + diceFaces + ")" + offsetFragment + ": " + rollSum);
let sumFragment = " Sum" + offsetFragment + (removeOutlier ? " except " + (removeOutlier > 0 ? "highest" : "lowest") : "");
return this.sendReplyBox("" + diceQuantity + " rolls (1 - " + diceFaces + ")" + (trackRolls ? ": " + rolls.join(", ") : "") + sumFragment + ": " + rollSum);
},
dicehelp: ["/dice [max number] - Randomly picks a number between 1 and the number you choose.",
"/dice [number of dice]d[number of sides] - Simulates rolling a number of dice, e.g., /dice 2d4 simulates rolling two 4-sided dice.",
"/dice [number of dice]d[number of sides][+/-][offset] - Simulates rolling a number of dice and adding an offset to the sum, e.g., /dice 2d6+10: two standard dice are rolled; the result lies between 12 and 22.",
"/dice [number of dice]d[number of sides]-[H/L] - Simulates rolling a number of dice with removal of extreme values, e.g., /dice 3d8-L: rolls three 8-sided dice; the result ignores the lowest value."],
pr: 'pickrandom',
pick: 'pickrandom',
pickrandom: function (target, room, user) {
let options = target.split(',');
if (options.length < 2) return this.parse('/help pick');
if (!this.runBroadcast()) return false;
const pickedOption = options[Math.floor(Math.random() * options.length)];
return this.sendReplyBox('We randomly picked: ' + Tools.escapeHTML(pickedOption).trim());
},
pickrandomhelp: ["/pick [option], [option], ... - Randomly selects an item from a list containing 2 or more elements."],
showimage: function (target, room, user) {
if (!target) return this.parse('/help showimage');
if (!this.can('declare', null, room)) return false;
if (!this.runBroadcast()) return;
if (this.room.isPersonal && !this.user.can('announce')) {
return this.errorReply("Images are not allowed in personal rooms.");
}
let targets = target.split(',');
if (targets.length !== 3) {
// Width and height are required because most browsers insert the
// element before width and height are known, and when the
// image is loaded, this changes the height of the chat area, which
// messes up autoscrolling.
return this.parse('/help showimage');
}
let image = targets[0].trim();
if (!image) return this.errorReply('No image URL was provided!');
image = this.canEmbedURI(image);
if (!image) return false;
let width = targets[1].trim();
if (!width) return this.errorReply('No width for the image was provided!');
if (!isNaN(width)) width += 'px';
let height = targets[2].trim();
if (!height) return this.errorReply('No height for the image was provided!');
if (!isNaN(height)) height += 'px';
let unitRegex = /^\d+(?:p[xtc]|%|[ecm]m|ex|in)$/;
if (!unitRegex.test(width)) {
return this.errorReply('"' + width + '" is not a valid width value!');
}
if (!unitRegex.test(height)) {
return this.errorReply('"' + height + '" is not a valid height value!');
}
this.sendReply('|raw|');
},
showimagehelp: ["/showimage [url], [width], [height] - Show an image. " +
"Any CSS units may be used for the width or height (default: px)." +
"Requires: # & ~"],
htmlbox: function (target, room, user, connection, cmd, message) {
if (!target) return this.parse('/help htmlbox');
target = this.canHTML(target);
if (!target) return;
if (user.userid === 'github') {
if (!this.can('announce', null, room)) return;
if (message.charAt(0) === '!') this.broadcasting = true;
} else {
if (!this.can('declare', null, room)) return;
if (!this.runBroadcast('!htmlbox')) return;
}
this.sendReplyBox(target);
},
htmlboxhelp: ["/htmlbox [message] - Displays a message, parsing HTML code contained. Requires: ~ # with global authority"],
};
process.nextTick(() => Tools.includeData());