diff --git a/chat-commands.js b/chat-commands.js index 3afaa6546b71..79b44141af30 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -777,9 +777,7 @@ const commands = { let roomid = target.trim(); if (!roomid) { // allow deleting personal rooms without typing out the room name - if (room.isPersonal && cmd === "deletegroupchat") { - roomid = room.id; - } else { + if (!room.isPersonal || cmd !== "deletegroupchat") { return this.parse(`/help deleteroom`); } } else { @@ -1063,7 +1061,7 @@ const commands = { } if (!this.can('declare')) return false; if (target.length > 80) return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`); - let normalizedTarget = ' ' + target.toLowerCase().replace('[^a-zA-Z0-9]+', ' ').trim() + ' '; + let normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' '; if (normalizedTarget.includes(' welcome ')) { return this.errorReply(`Error: Room description must not contain the word "welcome".`); @@ -1263,6 +1261,7 @@ const commands = { } if (!target) return this.parse('/help roomowner'); target = this.splitTarget(target, true); + if (target) return this.errorReply(`This command does not support specifying a reason.`); let targetUser = this.targetUser; let name = this.targetUsername; let userid = toId(name); @@ -1551,8 +1550,7 @@ const commands = { this.privateModAction(displayMessage); } } - this.add(`|unlink|hide|${userid}`); - if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); + room.hideText([userid, toId(this.inputUsername)]); if (room.isPrivate !== true && room.chatRoomData) { this.globalModlog("ROOMBAN", targetUser, ` by ${user.userid} ${(target ? `: ${target}` : ``)}`); @@ -1737,6 +1735,7 @@ const commands = { unmute: function (target, room, user) { if (!target) return this.parse('/help unmute'); target = this.splitTarget(target); + if (target) return this.errorReply(`This command does not support specifying a reason.`); if (!this.canTalk()) return; if (!this.can('mute', null, room)) return false; @@ -1860,8 +1859,7 @@ const commands = { displayMessage = `(${name}'s ac account: ${acAccount})`; this.privateModAction(displayMessage); } - this.add(`|unlink|hide|${userid}`); - if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); + room.hideText([userid, toId(this.inputUsername)]); const globalReason = (target ? `: ${userReason} ${proof}` : ''); this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.userid}${globalReason}`); @@ -1904,7 +1902,58 @@ const commands = { this.errorReply(`User '${target}' is not locked.`); } }, - unlockhelp: [`/unlock [username] - Unlocks the user. Requires: % @ * & ~`], + unlockname: function (target, room, user) { + if (!target) return this.parse('/help unlock'); + if (!this.can('lock')) return false; + + const userid = toId(target); + const punishment = Punishments.userids.get(userid); + if (!punishment) return this.errorReply("This name isn't locked."); + if (punishment[1] === userid) return this.errorReply(`"${userid}" was specifically locked by a staff member (check the global modlog). Use /unlock if you really want to unlock this name.`); + + Punishments.userids.delete(userid); + Punishments.savePunishments(); + + for (const curUser of Users.findUsers([userid], [])) { + if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) { + curUser.locked = false; + curUser.namelocked = false; + curUser.updateIdentity(); + } + } + this.globalModlog("UNLOCKNAME", userid, ` by ${user.name}`); + this.addModAction(`The name '${target}' was unlocked by ${user.name}.`); + }, + unlockip: function (target, room, user) { + target = target.trim(); + if (!target) return this.parse('/help unlock'); + if (!this.can('ban')) return false; + const range = target.charAt(target.length - 1) === '*'; + if (range && !this.can('rangeban')) return false; + + if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address."); + + const punishment = Punishments.ips.get(target); + if (!punishment) return this.errorReply(`${target} is not a locked/banned IP or IP range.`); + + Punishments.ips.delete(target); + Punishments.savePunishments(); + + for (const curUser of Users.findUsers([], [target])) { + if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) { + curUser.locked = false; + curUser.namelocked = false; + curUser.updateIdentity(); + } + } + this.globalModlog(`UNLOCK${range ? 'RANGE' : 'IP'}`, target, ` by ${user.name}`); + this.addModAction(`${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`); + }, + unlockhelp: [ + `/unlock [username] - Unlocks the user. Requires: % @ * & ~`, + `/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ * & ~`, + `/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ * & ~`, + ], forceglobalban: 'globalban', gban: 'globalban', @@ -1982,8 +2031,7 @@ const commands = { this.privateModAction(displayMessage); } - this.add(`|unlink|hide|${userid}`); - if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); + room.hideText([userid, toId(this.inputUsername)]); const globalReason = (target ? `: ${userReason} ${proof}` : ''); this.globalModlog("BAN", targetUser, ` by ${user.userid}${globalReason}`); @@ -2126,19 +2174,6 @@ const commands = { unrangelock: 'unlockip', rangeunlock: 'unlockip', - unlockip: function (target, room, user) { - target = target.trim(); - if (!target) { - return this.parse('/help unbanip'); - } - if (!this.can('rangeban')) return false; - if (!Punishments.ips.has(target)) { - return this.errorReply(`${target} is not a locked/banned IP or IP range.`); - } - Punishments.ips.delete(target); - this.addModAction(`${user.name} unlocked the ${(target.charAt(target.length - 1) === '*' ? "IP range" : "IP")}: ${target}`); - this.modlog('UNRANGELOCK', null, target); - }, /********************************************************* * Moderating: Other @@ -2224,6 +2259,7 @@ const commands = { if (!this.can('promote')) return; target = this.splitTarget(target, true); + if (target) return this.errorReply(`This command does not support specifying a reason.`); let targetUser = this.targetUser; let userid = toId(this.targetUsername); let name = targetUser ? targetUser.name : this.targetUsername; @@ -2358,6 +2394,32 @@ const commands = { }, announcehelp: [`/announce OR /wall [message] - Makes an announcement. Requires: % @ * # & ~`], + notifyoffrank: 'notifyrank', + notifyrank: function (target, room, user, connection, cmd) { + if (!target) return this.parse(`/help notifyrank`); + if (!this.can('addhtml', null, room)) return false; + if (!this.canTalk()) return; + let [rank, notification] = this.splitOne(target); + if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); + const id = `${room.id}-rank-${Config.groups[rank].id}`; + if (cmd === 'notifyoffrank') { + room.sendRankedUsers(`|tempnotifyoff|${id}`, rank); + } else { + let [title, message] = this.splitOne(notification); + if (!title) title = `${room.title} ${Config.groups[rank].name}+ message!`; + if (!user.can('addhtml')) { + title += ` (notification from ${user.name})`; + } + if (message.length > 300) return this.errorReply(`Notifications should not exceed 300 characters.`); + room.sendRankedUsers(`|tempnotify|${id}|${title}|${message}`, rank); + this.modlog(`NOTIFYRANK`, null, target); + } + }, + notifyrankhelp: [ + `/notifyrank [rank], [title], [message] - Sends a notification to everyone with the specified rank or higher. Requires: # * & ~`, + `/notifyoffrank [rank] - Closes the notification previously sent with /notifyrank [rank]. Requires: # * & ~`, + ], + fr: 'forcerename', forcerename: function (target, room, user) { if (!target) return this.parse('/help forcerename'); @@ -2379,6 +2441,7 @@ const commands = { Ladders.cancelSearches(targetUser); targetUser.resetName(true); targetUser.send(`|nametaken||${user.name} considers your name inappropriate${(reason ? `: ${reason}` : ".")}`); + targetUser.trackRename = targetUser.name; return true; }, forcerenamehelp: [`/forcerename OR /fr [username], [reason] - Forcibly change a user's name and shows them the [reason]. Requires: % @ * & ~`], @@ -2454,20 +2517,18 @@ const commands = { if (!(user.can('lock') || localPunished)) return this.errorReply(`User ${name} is neither locked nor muted/banned from this room.`); if (targetUser && (cmd === 'hidealtstext' || cmd === 'hidetextalts' || cmd === 'hidealttext')) { - this.addModAction(`${name}'s alts' messages were cleared from ${room.title} by ${user.name}.`); + room.sendByUser(user, `${name}'s alts messages were cleared from ${room.title} by ${user.name}.`); + this.modlog('HIDEALTSTEXT', targetUser, null, {noip: 1}); - this.add(`|unlink|hide|${userid}`); - const alts = targetUser.getAltUsers(true); - for (const alt of alts) { - this.add(`|unlink|hide|${alt.getLastId()}`); - } - for (const prevName in targetUser.prevNames) { - this.add(`|unlink|hide|${targetUser.prevNames[prevName]}`); - } + room.hideText([ + userid, + ...Object.keys(targetUser.prevNames), + ...targetUser.getAltUsers(true).map(user => user.getLastId()), + ]); } else { - this.addModAction(`${name}'s messages were cleared from ${room.title} by ${user.name}.`); + room.sendByUser(user, `${name}'s messages were cleared from ${room.title} by ${user.name}.`); this.modlog('HIDETEXT', targetUser || userid, null, {noip: 1, noalts: 1}); - this.add(`|unlink|hide|${userid}`); + room.hideText([userid]); } }, hidetexthelp: [ @@ -2540,7 +2601,12 @@ const commands = { } return true; }, - blacklisthelp: [`/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`], + blacklisthelp: [ + `/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`, + `/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`, + `/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`, + `/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`, + ], battleban: function (target, room, user, connection) { if (!target) return this.parse(`/help battleban`); @@ -3290,7 +3356,7 @@ const commands = { // errors can occur while rebasing or popping the stash; make sure to recover try { this.sendReply(`Rebasing...`); - [code, stdout, stderr] = await exec(`git rebase FETCH_HEAD`); + [code] = await exec(`git rebase FETCH_HEAD`); if (code) { // conflict while rebasing await exec(`git rebase --abort`); @@ -3299,7 +3365,7 @@ const commands = { if (stashedChanges) { this.sendReply(`Restoring saved changes...`); - [code, stdout, stderr] = await exec(`git stash pop`); + [code] = await exec(`git stash pop`); if (code) { // conflict while popping stash await exec(`git reset HEAD .`); @@ -3802,12 +3868,8 @@ const commands = { let targetUser = Users.getExact(target); if (!targetUser) return this.errorReply(`User '${target}' not found.`); - target = targetUser ? targetUser.userid : ''; - - if (target) { - room.battle.win(targetUser); - this.modlog('FORCEWIN', target); - } + room.battle.win(targetUser); + this.modlog('FORCEWIN', targetUser.userid); }, forcewinhelp: [ `/forcetie - Forces the current match to end in a tie. Requires: & ~`, @@ -3901,6 +3963,7 @@ const commands = { '!accept': true, accept: function (target, room, user, connection) { target = this.splitTarget(target); + if (target) return this.popupReply(`This command does not support specifying multiple users`); const targetUser = this.targetUser || this.pmTarget; if (!targetUser) return this.popupReply(`User "${this.targetUsername}" not found.`); Ladders.acceptChallenge(connection, targetUser); diff --git a/chat-formatter.js b/chat-formatter.js index 70bdd54b7a4a..98577e8cf1d6 100644 --- a/chat-formatter.js +++ b/chat-formatter.js @@ -64,7 +64,7 @@ class TextFormatter { // filter links first str = str.replace(linkRegex, uri => { - let fulluri = uri; + let fulluri; if (/^[a-z0-9.]+@/ig.test(uri)) { fulluri = 'mailto:' + uri; } else { diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js new file mode 100644 index 000000000000..c1b247e96d26 --- /dev/null +++ b/chat-plugins/chat-monitor.js @@ -0,0 +1,357 @@ +'use strict'; + +const FS = require('../lib/fs'); + +const MONITOR_FILE = 'config/chat-plugins/chat-monitor.tsv'; +const WRITE_THROTTLE_TIME = 5 * 60 * 1000; + +/** @type {{[k: string]: string[]}} */ +let filterKeys = Chat.filterKeys = Object.assign(Chat.filterKeys, {publicwarn: ['PUBLIC', 'WARN', 'Filtered in public'], warn: ['EVERYWHERE', 'WARN', 'Filtered'], autolock: ['EVERYWHERE', 'AUTOLOCK', 'Autolock'], namefilter: ['NAMES', 'WARN', 'Filtered in usernames'], wordfilter: ['EVERYWHERE', 'FILTERTO', 'Filtered to a different phrase']}); +/** @type {{[k: string]: ([(string | RegExp), string, string?, number][])}} */ +let filterWords = Chat.filterWords; + +setImmediate(() => { + for (const key in filterKeys) { + if (!filterWords[key]) filterWords[key] = []; + } + + /* + * Columns Location and Punishment use keywords. Possible values: + * + * Location: EVERYWHERE, PUBLIC, NAMES + * Punishment: AUTOLOCK, WARN, FILTERTO + */ + FS(MONITOR_FILE).readIfExists().then(data => { + const lines = data.split('\n'); + loop: for (const line of lines) { + if (!line || line === '\r') continue; + let [location, word, punishment, reason, times, ...rest] = line.split('\t').map(param => param.trim()); + if (location === 'Location') continue; + if (!(location && word && punishment)) continue; + + for (const key in filterKeys) { + if (filterKeys[key][0] === location && filterKeys[key][1] === punishment) { + if (punishment === 'FILTERTO') { + const filterTo = rest[0]; + filterWords[key].push([new RegExp(word, 'ig'), reason, filterTo, parseInt(times) || 0]); + continue loop; + } else { + filterWords[key].push([word, reason, null, parseInt(times) || 0]); + continue loop; + } + } + } + throw new Error(`Unrecognized [location, punishment] pair for filter word entry: ${[location, word, punishment, reason, times]}`); + } + }); +}); + +/** + * @param {string} location + * @param {[(string | RegExp), string, string?, number]} word + * @param {string} punishment + */ +function renderEntry(location, word, punishment) { + const str = word[0] instanceof RegExp ? String(word[0]).slice(1, -3) : word[0]; + return `${location}\t${str}\t${punishment}\t${word[1]}\t${word[3]}${word[2] ? `\t${word[2]}` : ''}\r\n`; +} + +/** + * @param {boolean} force + */ +function saveFilters(force = false) { + FS(MONITOR_FILE).writeUpdate(() => { + let buf = 'Location\tWord\tPunishment\tReason\tTimes\r\n'; + for (const key in filterKeys) { + buf += filterWords[key].map(word => renderEntry(filterKeys[key][0], word, filterKeys[key][1])).join(''); + } + return buf; + }, {throttle: force ? 0 : WRITE_THROTTLE_TIME}); +} + +/** @typedef {(this: CommandContext, target: string, room: ChatRoom, user: User, connection: Connection, cmd: string, message: string) => (void)} ChatHandler */ +/** @typedef {(this: CommandContext, message: string, user: User, room: ChatRoom?, connection: Connection, targetUser: User?) => (string | boolean)} ChatFilter */ +/** @typedef {(name: string, user: User) => (string)} NameFilter */ +/** @typedef {{[k: string]: ChatHandler | string | true | string[] | ChatCommands}} ChatCommands */ + +/** @type {ChatFilter} */ +let chatfilter = function (message, user, room) { + let lcMessage = message.replace(/\u039d/g, 'N').toLowerCase().replace(/[\u200b\u007F\u00AD]/g, '').replace(/\u03bf/g, 'o').replace(/\u043e/g, 'o').replace(/\u0430/g, 'a').replace(/\u0435/g, 'e').replace(/\u039d/g, 'e'); + lcMessage = lcMessage.replace(/__|\*\*|``|\[\[|\]\]/g, ''); + + for (let i = 0; i < filterWords.autolock.length; i++) { + let [line, reason] = filterWords.autolock[i]; + let matched = false; + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (line.startsWith('\\b') || line.endsWith('\\b')) { + matched = new RegExp(line).test(lcMessage); + } else { + matched = lcMessage.includes(line); + } + if (matched) { + if ((room && ((room.chatRoomData && room.id.endsWith('staff')) || room.id.startsWith('help-'))) || user.isStaff || (this.pmTarget && this.pmTarget.isStaff)) return `${message} __[would be locked: ${line}${reason ? ` (${reason})` : ''}]__`; + message = message.replace(/(https?):\/\//g, '$1__:__//'); + message = message.replace(/\./g, '__.__'); + if (room) { + Punishments.autolock(user, room, 'ChatMonitor', `Filtered phrase: ${line}`, `<${room.id}> ${user.name}: ${message}${reason ? ` __(${reason})__` : ''}`, true); + } else { + this.errorReply(`Please do not say '${line}'.`); + } + filterWords.autolock[i][3]++; + saveFilters(); + return false; + } + } + for (let i = 0; i < filterWords.warn.length; i++) { + let [line, reason] = filterWords.warn[i]; + let matched = false; + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (line.startsWith('\\b') || line.endsWith('\\b')) { + matched = new RegExp(line).test(lcMessage); + } else { + matched = lcMessage.includes(line); + } + if (matched) { + if ((room && ((room.chatRoomData && room.id.endsWith('staff')) || room.id.startsWith('help-'))) || user.isStaff || (this.pmTarget && this.pmTarget.isStaff)) return `${message} __[would be filtered: ${line}${reason ? ` (${reason})` : ''}]__`; + this.errorReply(`Please do not say '${line}'.`); + filterWords.warn[i][3]++; + saveFilters(); + return false; + } + } + if ((room && room.isPrivate !== true) || !room) { + for (let i = 0; i < filterWords.publicwarn.length; i++) { + let [line, reason] = filterWords.publicwarn[i]; + let matched = false; + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (line.startsWith('\\b') || line.endsWith('\\b')) { + matched = new RegExp(line).test(lcMessage); + } else { + matched = lcMessage.includes(line); + } + if (matched) { + if ((room && ((room.chatRoomData && room.id.endsWith('staff')) || room.id.startsWith('help-'))) || user.isStaff || (this.pmTarget && this.pmTarget.isStaff)) return `${message} __[would be filtered in public: ${line}${reason ? ` (${reason})` : ''}]__`; + this.errorReply(`Please do not say '${line}'.`); + filterWords.publicwarn[i][3]++; + saveFilters(); + return false; + } + } + } + if (!(room && room.chatRoomData && room.id.endsWith('staff'))) { + for (let line of filterWords.wordfilter) { + const regex = line[0]; + if (typeof regex === 'string') continue; + let match = regex.exec(message); + while (match) { + let filtered = line[2] || ''; + if (match[0] === match[0].toUpperCase()) filtered = filtered.toUpperCase(); + if (match[0][0] === match[0][0].toUpperCase()) filtered = `${filtered ? filtered[0].toUpperCase() : ''}${filtered.slice(1)}`; + message = message.replace(match[0], filtered); + line[3]++; + saveFilters(); + match = regex.exec(message); + } + } + } + + return message; +}; + +/** @type {NameFilter} */ +let namefilter = function (name, user) { + let id = toId(name); + if (Chat.namefilterwhitelist.has(id)) return name; + if (id === user.trackRename) return ''; + let lcName = name.replace(/\u039d/g, 'N').toLowerCase().replace(/[\u200b\u007F\u00AD]/g, '').replace(/\u03bf/g, 'o').replace(/\u043e/g, 'o').replace(/\u0430/g, 'a').replace(/\u0435/g, 'e').replace(/\u039d/g, 'e'); + // Remove false positives. + lcName = lcName.replace('herapist', '').replace('grape', '').replace('scrape', ''); + + for (let [line] of filterWords.autolock) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + Punishments.autolock(user, Rooms('staff'), `NameMonitor`, `inappropriate name: ${name}`, `using an inappropriate name: ${name}`, false, true); + return ''; + } + } + for (let [line] of filterWords.warn) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + user.trackRename = name; + return ''; + } + } + for (let [line] of filterWords.publicwarn) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + user.trackRename = name; + return ''; + } + } + for (let line of filterWords.namefilter) { + const word = String(line[0]); + let matched; + if (word.startsWith('\\b') || word.endsWith('\\b')) { + matched = new RegExp(word).test(lcName); + } else { + matched = lcName.includes(word); + } + if (matched) { + user.trackRename = name; + line[3]++; + saveFilters(); + return ''; + } + } + + if (user.trackRename) { + Monitor.log(`[NameMonitor] Username used: ${name} (forcerenamed from ${user.trackRename})`); + user.trackRename = ''; + } + return name; +}; + +/** @typedef {(query: string[], user: User, connection: Connection) => (string | null | void)} PageHandler */ +/** @typedef {{[k: string]: PageHandler | PageTable}} PageTable */ + +/** @type {PageTable} */ +const pages = { + filters(query, user, connection) { + if (!user.named) return Rooms.RETRY_AFTER_LOGIN; + let buf = `|title|Filters\n|pagehtml|

Filters

`; + if (!user.can('lock')) { + return buf + `

Access denied

`; + } + let content = ``; + for (const key in filterKeys) { + content += `

${filterKeys[key][2]} [${key}]

`; + if (filterWords[key].length) { + content += filterWords[key].map(([str, reason, filterTo, hits]) => { + let entry = ''; + if (filterTo) { + entry = `${str} ⇒ ${filterTo}`; + } else { + entry = `${str}`; + } + return `${entry}${hits}`; + }).join(''); + } + } + + if (Chat.namefilterwhitelist.size) { + content += `

Whitelisted names

`; + for (const [val] of Chat.namefilterwhitelist) { + content += `${val}`; + } + } + if (!content) { + buf += `

There are no filtered words.

`; + } else { + buf += `${content}
`; + } + buf += ``; + return buf; + }, +}; +exports.pages = pages; + +/** @type {ChatCommands} */ +let commands = { + filters: 'filter', + filter: { + add: function (target, room, user) { + if (!this.can('updateserver')) return false; + + let [list, ...rest] = target.split(','); + list = toId(list); + + if (!list || !rest.length) return this.errorReply("Syntax: /filter add list, word, reason"); + + if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); + + if (filterKeys[list][1] === 'FILTERTO') { + let [word, filterTo, ...reasonParts] = rest; + if (!filterTo) return this.errorReply(`Syntax for word filters: /filter add ${list}, regex, filter to, reason`); + word = word.trim(); + filterTo = filterTo.trim(); + let reason = reasonParts.join(',').trim(); + + let regex; + try { + regex = new RegExp(word, 'ig'); // eslint-disable-line no-unused-vars + } catch (e) { + return this.errorReply(e.message.startsWith('Invalid regular expression: ') ? e.message : `Invalid regular expression: /${word}/: ${e.message}`); + } + + filterWords[list].push([regex, reason, filterTo, 0]); + this.globalModlog(`ADDFILTER`, null, `'${String(regex)} => ${filterTo}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); + saveFilters(true); + return this.sendReply(`'${String(regex)} => ${filterTo}' was added to the ${list} list.`); + } else { + let [word, ...reasonParts] = rest; + word = word.trim(); + let reason = reasonParts.join(',').trim(); + if (filterWords[list].some(val => val[0] === word)) return this.errorReply(`${word} is already added to the ${list} list.`); + filterWords[list].push([word, reason, null, 0]); + this.globalModlog(`ADDFILTER`, null, `'${word}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); + saveFilters(true); + return this.sendReply(`'${word}' was added to the ${list} list.`); + } + }, + remove: function (target, room, user) { + if (!this.can('updateserver')) return false; + + let [list, ...words] = target.split(',').map(param => param.trim()); + list = toId(list); + + if (!list || !words.length) return this.errorReply("Syntax: /filter remove list, words"); + + if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); + + if (filterKeys[list][1] === 'FILTERTO') { + const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -3) === val).length); + if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -3))); + this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); + saveFilters(); + return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); + } else { + const notFound = words.filter(val => !filterWords[list].filter(entry => entry[0] === val).length); + if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]))); // This feels wrong + this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); + saveFilters(); + return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); + } + }, + '': 'view', + list: 'view', + view: function (target, room, user) { + this.parse(`/join view-filters`); + }, + help: function (target, room, user) { + this.parse(`/help filter`); + }, + }, + filterhelp: [ + `- /filter add list, word, reason - Adds a word to the given filter list. Requires: ~`, + `- /filter remove list, words - Removes words from the given filter list. Requires: ~`, + `- /filter view - Opens the list of filtered words. Requires: % @ * & ~`, + ], + allowname: function (target, room, user) { + if (!this.can('forcerename')) return false; + target = toId(target); + if (!target) return this.errorReply(`Syntax: /allowname username`); + Chat.namefilterwhitelist.set(target, user.name); + + const msg = `${target} was allowed as a username by ${user.name}.`; + const staffRoom = Rooms('staff'); + const upperStaffRoom = Rooms('upperstaff'); + if (staffRoom) staffRoom.add(msg).update(); + if (upperStaffRoom) upperStaffRoom.add(msg).update(); + }, +}; + +exports.commands = commands; +exports.chatfilter = chatfilter; +exports.namefilter = namefilter; diff --git a/chat-plugins/datasearch.js b/chat-plugins/datasearch.js index 29ec6f207972..2fd2da8c0c1c 100644 --- a/chat-plugins/datasearch.js +++ b/chat-plugins/datasearch.js @@ -755,7 +755,7 @@ function runMovesearch(target, cmd, canAll, message) { let allCategories = ['physical', 'special', 'status']; let allContestTypes = ['beautiful', 'clever', 'cool', 'cute', 'tough']; let allProperties = ['basePower', 'accuracy', 'priority', 'pp']; - let allFlags = ['authentic', 'bite', 'bullet', 'contact', 'defrost', 'powder', 'protect', 'pulse', 'punch', 'secondary', 'snatch', 'sound']; + let allFlags = ['authentic', 'bite', 'bullet', 'contact', 'dance', 'defrost', 'powder', 'protect', 'pulse', 'punch', 'secondary', 'snatch', 'sound']; let allStatus = ['psn', 'tox', 'brn', 'par', 'frz', 'slp']; let allVolatileStatus = ['flinch', 'confusion', 'partiallytrapped']; let allBoosts = ['hp', 'atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; @@ -1424,6 +1424,8 @@ function runLearn(target, cmd) { format = Dex.getFormat(targetid); formatid = targetid; formatName = format.name; + targets.shift(); + continue; } if (targetid.startsWith('gen') && parseInt(targetid.charAt(3))) { gen = parseInt(targetid.slice(3)); @@ -1440,12 +1442,14 @@ function runLearn(target, cmd) { } break; } - if (!formatid) format = new Dex.Data.Format(format); - if (!formatid) formatid = 'gen' + gen + 'ou'; - if (!formatName) formatName = 'Gen ' + gen; + if (!formatName) { + format = new Dex.Data.Format(format, {mod: `gen${gen}`}); + formatName = `Gen ${gen}`; + if (format.requirePentagon) formatName += ' Pentagon'; + } let lsetData = {set: {}, sources: [], sourcesBefore: gen}; - const validator = TeamValidator(formatid); + const validator = TeamValidator(format); let template = validator.dex.getTemplate(targets.shift()); let move = {}; let all = (cmd === 'learnall'); diff --git a/chat-plugins/info.js b/chat-plugins/info.js index ed8cb5a92971..8e46855242f1 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -84,16 +84,24 @@ const commands = { } buf += '
'; if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) { - let prevNames = Object.keys(targetUser.prevNames).join(", "); + let prevNames = Object.keys(targetUser.prevNames).map(userid => { + const punishment = Punishments.userids.get(userid); + return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.userid ? ` as ${punishment[1]}` : ''})` : ''); + }).join(", "); if (prevNames) buf += Chat.html`
Previous names: ${prevNames}`; for (const targetAlt of targetUser.getAltUsers(true)) { if (!targetAlt.named && !targetAlt.connected) continue; if (targetAlt.group === '~' && user.group !== '~') continue; - buf += Chat.html`
Alt: ${targetAlt.name}`; + const punishment = Punishments.userids.get(targetAlt.userid); + const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''; + buf += Chat.html`
Alt: ${targetAlt.name}${punishMsg}`; if (!targetAlt.connected) buf += ` (offline)`; - prevNames = Object.keys(targetAlt.prevNames).join(", "); + prevNames = Object.keys(targetAlt.prevNames).map(userid => { + const punishment = Punishments.userids.get(userid); + return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''); + }).join(", "); if (prevNames) buf += `
Previous names: ${prevNames}`; } if (targetUser.namelocked) { @@ -406,13 +414,12 @@ const commands = { target = sep[0].trim(); let targetId = toId(target); if (!targetId) return this.parse('/help data'); - let targetNum = parseInt(targetId); + let targetNum = parseInt(target); if (!isNaN(targetNum) && '' + targetNum === target) { for (let p in Dex.data.Pokedex) { let pokemon = Dex.getTemplate(p); if (pokemon.num === targetNum) { target = pokemon.species; - targetId = pokemon.id; break; } } @@ -552,6 +559,7 @@ const commands = { if (move.flags['powder']) details["✓ Powder"] = ""; if (move.flags['reflectable']) details["✓ Bounceable"] = ""; if (move.flags['gravity'] && mod.gen >= 4) details["✗ Suppressed by Gravity"] = ""; + if (move.flags['dance'] && mod.gen >= 7) details["✓ Dance move"] = ""; if (mod.gen >= 7) { if (move.zMovePower) { @@ -814,46 +822,54 @@ const commands = { bestCoverage[type] = -5; } - for (const arg of targets) { - let move = arg.trim(); - if (toId(move) === mod.currentMod) continue; - move = move.charAt(0).toUpperCase() + move.slice(1).toLowerCase(); - if (move === 'Table' || move === 'All') { + for (let arg of targets) { + arg = toId(arg); + + // arg is the gen? + if (arg === mod.currentMod) continue; + + // arg is 'table' or 'all'? + if (arg === 'table' || arg === 'all') { if (this.broadcasting) return this.sendReplyBox("The full table cannot be broadcast."); dispTable = true; continue; } + // arg is a type? + let argType = arg.charAt(0).toUpperCase() + arg.slice(1); let eff; - if (move in mod.data.TypeChart) { - sources.push(move); + if (argType in mod.data.TypeChart) { + sources.push(argType); for (let type in bestCoverage) { - if (!mod.getImmunity(move, type) && !move.ignoreImmunity) continue; - eff = mod.getEffectiveness(move, type); + if (!mod.getImmunity(argType, type)) continue; + eff = mod.getEffectiveness(argType, type); if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } - move = mod.getMove(move); - if (move.exists && move.gen <= mod.gen) { - if (!move.basePower && !move.basePowerCallback) continue; - if (move.id === 'thousandarrows') hasThousandArrows = true; - sources.push(move); - for (let type in bestCoverage) { - if (move.id === "struggle") { - eff = 0; - } else { - if (!mod.getImmunity(move.type, type) && !move.ignoreImmunity) continue; - let baseMod = mod.getEffectiveness(move, type); - let moveMod = move.onEffectiveness && move.onEffectiveness.call(mod, baseMod, type, move); - eff = typeof moveMod === 'number' ? moveMod : baseMod; - } - if (eff > bestCoverage[type]) bestCoverage[type] = eff; - } - continue; + + // arg is a move? + let move = mod.getMove(arg); + if (!move.exists) { + return this.errorReply(`Type or move '${arg}' not found.`); + } else if (move.gen > mod.gen) { + return this.errorReply(`Move '${arg}' is not available in Gen ${mod.gen}.`); } - return this.errorReply(`No type or move '${arg}' found${Dex.gen > mod.gen ? ` in Gen ${mod.gen}` : ""}.`); + if (!move.basePower && !move.basePowerCallback) continue; + if (move.id === 'thousandarrows') hasThousandArrows = true; + sources.push(move); + for (let type in bestCoverage) { + if (move.id === "struggle") { + eff = 0; + } else { + if (!mod.getImmunity(move.type, type) && !move.ignoreImmunity) continue; + let baseMod = mod.getEffectiveness(move, type); + let moveMod = move.onEffectiveness && move.onEffectiveness.call(mod, baseMod, type, move); + eff = typeof moveMod === 'number' ? moveMod : baseMod; + } + if (eff > bestCoverage[type]) bestCoverage[type] = eff; + } } if (sources.length === 0) return this.errorReply("No moves using a type table for determining damage were specified."); if (sources.length > 4) return this.errorReply("Specify a maximum of 4 moves or types."); @@ -1528,7 +1544,7 @@ const commands = { break; } - if (!totalMatches) return this.errorReply("No " + (target ? "matched " : "") + "formats found."); + if (!totalMatches) return this.errorReply("No matched formats found."); if (totalMatches === 1) { let rules = []; let rulesetHtml = ''; @@ -1674,11 +1690,27 @@ const commands = { rule: 'rules', rules: function (target, room, user) { if (!target) { + const languageTable = { + portuguese: ['Por favor siga as regras:', 'pages/rules-pt', 'Regras Globais', room ? `Regras da sala ${room.title}` : ``], + spanish: ['Por favor sigue las reglas:', 'pages/rules-es', 'Reglas Globales', room ? `Reglas de la sala ${room.title}` : ``], + italian: ['Per favore, rispetta le seguenti regole:', 'pages/rules-it', 'Regole Globali', room ? `Regole della room ${room.title}` : ``], + french: ['Veuillez suivre ces règles:', 'pages/rules-fr', 'Règles Générales', room ? `Règles de la room ${room.title}` : ``], + simplifiedchinese: ['请遵守规则:', 'pages/rules-zh', '全站规则', room ? `${room.title}房间规则` : ``], + traditionalchinese: ['請遵守規則:', 'pages/rules-tw', '全站規則', room ? `${room.title}房間規則` : ``], + japanese: ['ルールを守ってください:', 'pages/rules-ja', '全部屋共通ルール', room ? `${room.title}部屋のルール` : ``], + hindi: ['कृपया इन नियमों का पालन करें:', 'pages/rules-hi', 'सामान्य नियम', room ? `${room.title} Room के नियम` : ``], + turkish: ['Lütfen kurallara uyun:', 'pages/rules-tr', 'Genel kurallar', room ? `${room.title} odası kuralları` : ``], + dutch: ['Volg de regels:', 'pages/rules-nl', 'Globale Regels ', room ? `Regels van de ${room.title} room` : ``], + german: ['Bitte befolgt die Regeln:', 'pages/rules-de', 'Globale Regeln', room ? `Regeln des ${room.title} Raumes` : ``], + english: ['Please follow the rules:', 'rules', 'Global Rules', room ? `${room.title} room rules` : ``], + }; if (!this.runBroadcast()) return; + const globalRulesLink = `https://pokemonshowdown.com/${languageTable[room && room.language ? room.language : 'english'][1]}`; + const globalRulesLinkText = languageTable[room && room.language ? room.language : 'english'][2]; this.sendReplyBox( - `Please follow the rules:
` + - (room && room.rulesLink ? Chat.html`- ${room.title} room rules
` : ``) + - `- ${room && room.rulesLink ? "Global rules" : "Rules"}` + `${room ? languageTable[room.language || 'english'][0] + '
' : ``}` + + (room && room.rulesLink ? Chat.html`- ${languageTable[room.language || 'english'][3]}
` : ``) + + `- ${globalRulesLinkText}` ); return; } @@ -1837,11 +1869,23 @@ const commands = { } else if (extraFormat.effectType !== 'Format') { formatName = formatId = ''; } + const supportedLanguages = { + spanish: 'es', + french: 'fr', + italian: 'ita', + german: 'ger', + portuguese: 'por', + }; let speciesid = pokemon.speciesid; // Special case for Meowstic-M if (speciesid === 'meowstic') speciesid = 'meowsticm'; if (pokemon.tier === 'CAP') { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis preview, brought to you by Smogon University CAP Project`); + } else if (formatId === 'ou' && generation === 'sm' && room && room.language in supportedLanguages) { + // Limited support for translated analysis + // Translated analysis do not support automatic redirects from a speciesid to the proper page + let pageid = pokemon.name.toLowerCase().replace(' ', '_'); + this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } else { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } @@ -2169,7 +2213,7 @@ const commands = { }, 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) { + htmlbox: function (target, room, user) { if (!target) return this.parse('/help htmlbox'); target = this.canHTML(target); if (!target) return; @@ -2181,8 +2225,12 @@ const commands = { this.sendReplyBox(target); }, - addhtmlbox: function (target, room, user, connection, cmd, message) { - if (!target) return this.parse('/help htmlbox'); + htmlboxhelp: [ + `/htmlbox [message] - Displays a message, parsing HTML code contained.`, + `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: * # & ~`, + ], + addhtmlbox: function (target, room, user, connection, cmd) { + if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; target = this.canHTML(target); if (!target) return; @@ -2194,9 +2242,26 @@ const commands = { this.addBox(target); }, - htmlboxhelp: [ - `/htmlbox [message] - Displays a message, parsing HTML code contained.`, - `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, + addhtmlboxhelp: [ + `/addhtmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: * & ~`, + ], + addrankhtmlbox: function (target, room, user, connection, cmd) { + if (!target) return this.parse('/help ' + cmd); + if (!this.canTalk()) return; + let [rank, html] = this.splitOne(target); + if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); + html = this.canHTML(html); + if (!html) return; + if (!this.can('addhtml', null, room)) return; + + if (!user.can('addhtml')) { + html += Chat.html`
[${user.name}]
`; + } + + this.room.sendRankedUsers(`|html|
${html}
`, rank); + }, + addrankhtmlboxhelp: [ + `/addrankhtmlbox [rank], [message] - Shows everyone with the specified rank or higher a message, parsing HTML code contained. Requires: * & ~`, ], changeuhtml: 'adduhtml', adduhtml: function (target, room, user, connection, cmd) { @@ -2213,13 +2278,40 @@ const commands = { html += Chat.html`
[${user.name}]
`; } - this.add(`|uhtml${(cmd === 'changeuhtml' ? 'change' : '')}|${name}|${html}`); + html = `|uhtml${(cmd === 'changeuhtml' ? 'change' : '')}|${name}|${html}`; + this.add(html); }, adduhtmlhelp: [ - `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained.`, + `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained. Requires: * & ~`, ], changeuhtmlhelp: [ - `/changeuhtml [name], [message] - Changes a message previously shown with /adduhtml`, + `/changeuhtml [name], [message] - Changes the message previously shown with /adduhtml [name]. Requires: * & ~`, + ], + changerankuhtml: 'addrankuhtml', + addrankuhtml: function (target, room, user, connection, cmd) { + if (!target) return this.parse('/help ' + cmd); + if (!this.canTalk()) return; + + let [rank, uhtml] = this.splitOne(target); + if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); + let [name, html] = this.splitOne(uhtml); + name = toId(name); + html = this.canHTML(html); + if (!html) return; + if (!this.can('addhtml', null, room)) return; + + if (!user.can('addhtml')) { + html += Chat.html`
[${user.name}]
`; + } + + html = `|uhtml${(cmd === 'changerankuhtml' ? 'change' : '')}|${name}|${html}`; + this.room.sendRankedUsers(html, rank); + }, + addrankuhtmlhelp: [ + `/addrankuhtml [rank], [name], [message] - Shows everyone with the specified rank or higher a message that can change, parsing HTML code contained. Requires: * & ~`, + ], + changerankuhtmlhelp: [ + `/changerankuhtml [rank], [name], [message] - Changes the message previously shown with /addrankuhtml [rank], [name]. Requires: * & ~`, ], }; diff --git a/chat-plugins/mafia-data.js b/chat-plugins/mafia-data.js index 88af2f73cff0..724624b64ddd 100644 --- a/chat-plugins/mafia-data.js +++ b/chat-plugins/mafia-data.js @@ -1,5 +1,5 @@ 'use strict'; -/** @typedef {{name: string, plural: string, id: string, color?: string, memo: string[], image?: string}} MafiaAlignment */ +/** @typedef {{name: string, plural: string, id: string, color?: string, buttonColor?: string, memo: string[], image?: string}} MafiaAlignment */ /** @typedef {{[k: string]: MafiaAlignment | string}} MafiaAlignments */ /** @typedef {{name: string, id: string, memo: string[], alignment?: string, image?: string}} MafiaRole */ @@ -23,6 +23,7 @@ const alignments = { plural: `Town`, id: `town`, color: `#060`, + buttonColor: `#0A0`, memo: [`You are aligned with the Town. You win when all threats to the Town are eliminated and at least one Town-aligned player is still alive, or nothing can prevent the same.`], image: ``, }, diff --git a/chat-plugins/mafia.js b/chat-plugins/mafia.js index 8bdf8af40c10..d87fe2c1ea01 100644 --- a/chat-plugins/mafia.js +++ b/chat-plugins/mafia.js @@ -161,9 +161,14 @@ class MafiaPlayer extends Rooms.RoomGamePlayer { this.IDEA = null; } - getRole() { + /** + * @param {boolean} button + */ + getRole(button = false) { if (!this.role) return; - return `${this.role.safeName}`; + let color = MafiaData.alignments[this.role.alignment].color; + if (button && MafiaData.alignments[this.role.alignment].buttonColor) color = MafiaData.alignments[this.role.alignment].buttonColor; + return `${this.role.safeName}`; } updateHtmlRoom() { @@ -175,115 +180,6 @@ class MafiaPlayer extends Rooms.RoomGamePlayer { } } -// Parses a single role into an object -/** - * - * @param {string} roleString - * @return {MafiaParsedRole} - */ -function parseRole(roleString) { - /** @type {MafiaRole} */ - let role = { - name: roleString.split(' ').map(p => toId(p) === 'solo' ? '' : p).join(' '), - safeName: '', // MAKE SURE THESE ARE SET BELOW - id: '', - image: '', - memo: ['During the Day, you may vote for whomever you want lynched.'], - alignment: '', - }; - roleString = roleString.replace(/\s*\(.*?\)\s*/g, ' '); - let target = roleString.toLowerCase().replace(/[^\w\d\s]/g, '').split(' '); - let problems = []; - role.safeName = Chat.escapeHTML(role.name); - role.id = toId(role.name); - for (let key in MafiaData.roles) { - if (key.includes('_')) { - let roleKey = target.slice().map(toId).join('_'); - if (roleKey.includes(key)) { - let originalKey = key; - if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; - if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; - if (MafiaData.roles[key].alignment) { - if (role.alignment && role.alignment !== MafiaData.roles[key].alignment) { - // A role cant have multiple alignments - problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.roles[key].alignment} or ${role.alignment})`); - break; - } - role.alignment = MafiaData.roles[key].alignment; - } - if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); - let index = roleKey.split('_').indexOf(originalKey.split('_')[0]); - target.splice(index, originalKey.split('_').length); - } - } else if (target.includes(key)) { - let index = target.indexOf(key); - if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; - if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; - if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); - target.splice(index, 1); - } - } - // Add modifiers - for (let key in MafiaData.modifiers) { - if (key.includes('_')) { - let roleKey = target.slice().map(toId).join('_'); - if (roleKey.includes(key)) { - if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; - if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; - if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); - let index = roleKey.split('_').indexOf(key.split('_')[0]); - target.splice(index, key.split('_').length); - } - } else if (key === 'xshot') { - // Special case for X-Shot modifier - for (let [i, xModifier] of target.entries()) { - if (toId(xModifier).endsWith('shot')) { - let num = parseInt(toId(xModifier).substring(0, toId(xModifier).length - 4)); - if (isNaN(num)) continue; - let memo = MafiaData.modifiers.xshot.memo.slice(); - memo = memo.map((/** @type {string} */m) => m.replace(/X/g, num.toString())); - role.memo = role.memo.concat(memo); - target.splice(i, 1); - i--; - } - } - } else if (target.includes(key)) { - let index = target.indexOf(key); - if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; - if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; - if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); - target.splice(index, 1); - } - } - // Determine the role's alignment - for (let [j, targetId] of target.entries()) { - let id = toId(targetId); - if (MafiaData.alignments[id]) { - if (typeof MafiaData.alignments[id] === 'string') id = MafiaData.alignments[id]; - if (role.alignment && role.alignment !== MafiaData.alignments[id].id) { - // A role cant have multiple alignments - problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.alignments[id].id} or ${role.alignment})`); - break; - } - role.alignment = MafiaData.alignments[id].id; - role.memo = role.memo.concat(MafiaData.alignments[id].memo); - if (!role.image && MafiaData.alignments[id].image) role.image = MafiaData.alignments[id].image; - target.splice(j, 1); - j--; - } - } - if (!role.alignment) { - // Default to town - role.alignment = 'town'; - role.memo = role.memo.concat(MafiaData.alignments.town.memo); - } - // Handle anything that is unknown - if (target.length) { - role.memo.push(`To learn more about your role, PM the host.`); - } - return {role, problems}; -} - class MafiaTracker extends Rooms.RoomGame { /** * @param {ChatRoom} room @@ -303,6 +199,8 @@ class MafiaTracker extends Rooms.RoomGame { this.hostid = host.userid; this.host = Chat.escapeHTML(host.name); + /** @type {string[]} */ + this.cohosts = []; /** @type {{[userid: string]: MafiaPlayer}} */ this.players = Object.create(null); @@ -404,9 +302,10 @@ class MafiaTracker extends Rooms.RoomGame { * @param {User} user * @param {string} roleString * @param {boolean} force + * @param {boolean} reset * @return {void} */ - setRoles(user, roleString, force = false) { + setRoles(user, roleString, force = false, reset = false) { let roles = (/** @type {string[]} */roleString.split(',').map(x => x.trim())); if (roles.length === 1) { // Attempt to set roles from a theme @@ -426,14 +325,7 @@ class MafiaTracker extends Rooms.RoomGame { } else if (roles.length > this.playerCount) { user.sendTo(this.room, `|error|You have provided too many roles, ${roles.length - this.playerCount} ${Chat.plural(roles.length - this.playerCount, 'roles', 'role')} will not be assigned.`); } - if (this.originalRoles.length) { - // Reset roles - this.originalRoles = []; - this.originalRoleString = ''; - this.roles = []; - this.roleString = ''; - } - if (this.IDEA.data) this.IDEA.data = null; + if (force) { this.originalRoles = roles.map(r => { return { @@ -450,6 +342,8 @@ class MafiaTracker extends Rooms.RoomGame { this.roleString = this.originalRoleString; return this.sendRoom(`The roles have been set.`); } + + let newRoles = []; /** @type {string[]} */ let problems = []; /** @type {string[]} */ @@ -459,29 +353,141 @@ class MafiaTracker extends Rooms.RoomGame { for (const string of roles) { const roleId = string.toLowerCase().replace(/[^\w\d\s]/g, ''); if (roleId in cache) { - this.originalRoles.push(Object.assign(Object.create(null), cache[roleId])); + newRoles.push(Object.assign(Object.create(null), cache[roleId])); } else { - const role = parseRole(string); + const role = MafiaTracker.parseRole(string); if (role.problems.length) problems = problems.concat(role.problems); if (alignments.indexOf(role.role.alignment) === -1) alignments.push(role.role.alignment); cache[roleId] = role.role; - this.originalRoles.push(role.role); + newRoles.push(role.role); } } if (alignments.length < 2 && alignments[0] !== 'solo') problems.push(`There must be at least 2 different alignments in a game!`); if (problems.length) { - this.originalRoles = []; for (const problem of problems) { user.sendTo(this.room, `|error|${problem}`); } - return user.sendTo(this.room, `|error|To forcibly set the roles, use /mafia forcesetroles`); + return user.sendTo(this.room, `|error|To forcibly set the roles, use /mafia force${reset ? "re" : ""}setroles`); } + + this.IDEA.data = null; + + this.originalRoles = newRoles; this.roles = this.originalRoles.slice(); this.originalRoleString = this.originalRoles.slice().map(r => `${r.safeName}`).join(', '); this.roleString = this.originalRoleString; - this.phase = 'locked'; + if (!reset) this.phase = 'locked'; this.updatePlayers(); - this.sendRoom(`The roles have been set.`); + this.sendRoom(`The roles have been ${reset ? 're' : ''}set.`); + if (reset) this.distributeRoles(); + } + + /** + * Parses a single role into an object + * @param {string} roleString + * @return {MafiaParsedRole} + */ + static parseRole(roleString) { + /** @type {MafiaRole} */ + let role = { + name: roleString.split(' ').map(p => toId(p) === 'solo' ? '' : p).join(' '), + safeName: '', // MAKE SURE THESE ARE SET BELOW + id: '', + image: '', + memo: ['During the Day, you may vote for whomever you want lynched.'], + alignment: '', + }; + roleString = roleString.replace(/\s*\(.*?\)\s*/g, ' '); + let target = roleString.toLowerCase().replace(/[^\w\d\s]/g, '').split(' '); + let problems = []; + role.safeName = Chat.escapeHTML(role.name); + role.id = toId(role.name); + for (let key in MafiaData.roles) { + if (key.includes('_')) { + let roleKey = target.slice().map(toId).join('_'); + if (roleKey.includes(key)) { + let originalKey = key; + if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; + if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; + if (MafiaData.roles[key].alignment) { + if (role.alignment && role.alignment !== MafiaData.roles[key].alignment) { + // A role cant have multiple alignments + problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.roles[key].alignment} or ${role.alignment})`); + break; + } + role.alignment = MafiaData.roles[key].alignment; + } + if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); + let index = roleKey.split('_').indexOf(originalKey.split('_')[0]); + target.splice(index, originalKey.split('_').length); + } + } else if (target.includes(key)) { + let index = target.indexOf(key); + if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; + if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; + if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); + target.splice(index, 1); + } + } + // Add modifiers + for (let key in MafiaData.modifiers) { + if (key.includes('_')) { + let roleKey = target.slice().map(toId).join('_'); + if (roleKey.includes(key)) { + if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; + if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; + if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); + let index = roleKey.split('_').indexOf(key.split('_')[0]); + target.splice(index, key.split('_').length); + } + } else if (key === 'xshot') { + // Special case for X-Shot modifier + for (let [i, xModifier] of target.entries()) { + if (toId(xModifier).endsWith('shot')) { + let num = parseInt(toId(xModifier).substring(0, toId(xModifier).length - 4)); + if (isNaN(num)) continue; + let memo = MafiaData.modifiers.xshot.memo.slice(); + memo = memo.map((/** @type {string} */m) => m.replace(/X/g, num.toString())); + role.memo = role.memo.concat(memo); + target.splice(i, 1); + i--; + } + } + } else if (target.includes(key)) { + let index = target.indexOf(key); + if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; + if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; + if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); + target.splice(index, 1); + } + } + // Determine the role's alignment + for (let [j, targetId] of target.entries()) { + let id = toId(targetId); + if (MafiaData.alignments[id]) { + if (typeof MafiaData.alignments[id] === 'string') id = MafiaData.alignments[id]; + if (role.alignment && role.alignment !== MafiaData.alignments[id].id) { + // A role cant have multiple alignments + problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.alignments[id].id} or ${role.alignment})`); + break; + } + role.alignment = MafiaData.alignments[id].id; + role.memo = role.memo.concat(MafiaData.alignments[id].memo); + if (!role.image && MafiaData.alignments[id].image) role.image = MafiaData.alignments[id].image; + target.splice(j, 1); + j--; + } + } + if (!role.alignment) { + // Default to town + role.alignment = 'town'; + role.memo = role.memo.concat(MafiaData.alignments.town.memo); + } + // Handle anything that is unknown + if (target.length) { + role.memo.push(`To learn more about your role, PM the host.`); + } + return {role, problems}; } /** @@ -505,9 +511,8 @@ class MafiaTracker extends Rooms.RoomGame { if (Object.keys(this.roles).length < this.playerCount) return user.sendTo(this.room, `You have not provided enough roles for the players.`); } this.started = true; - this.played = Object.keys(this.players); this.sendRoom(`The game of ${this.title} is starting!`, {declare: true}); - this.played.push(this.hostid); + // MafiaTracker#played gets set in distributeRoles this.distributeRoles(); this.day(null, true); if (this.IDEA.data) this.room.add(`|html|
IDEA discards:${this.IDEA.discardsHtml}
`).update(); @@ -517,15 +522,17 @@ class MafiaTracker extends Rooms.RoomGame { * @return {void} */ distributeRoles() { - if (this.phase !== 'locked' || !Object.keys(this.roles).length) return; - this.sendRoom(`The roles are being distributed...`); let roles = Dex.shuffle(this.roles.slice()); - for (let p in this.players) { - let role = roles.shift(); - this.players[p].role = role; - let u = Users(p); - if (u && u.connected) u.send(`>${this.room.id}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`); + if (roles.length) { + for (let p in this.players) { + let role = roles.shift(); + this.players[p].role = role; + let u = Users(p); + if (u && u.connected) u.send(`>${this.room.id}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`); + } } + this.dead = {}; + this.played = [this.hostid, ...this.cohosts, ...Object.keys(this.players)]; this.sendRoom(`The roles have been distributed.`, {declare: true}); this.updatePlayers(); } @@ -580,11 +587,14 @@ class MafiaTracker extends Rooms.RoomGame { if (this.phase !== 'day') return; if (this.timer) this.setDeadline(0, true); this.phase = 'night'; - let host = Users(this.hostid); - if (host && host.connected) host.send(`>${this.room.id}\n|notify|It's night in your game of Mafia!`); + for (const hostid of [...this.cohosts, this.hostid]) { + let host = Users(hostid); + if (host && host.connected) host.send(`>${this.room.id}\n|notify|It's night in your game of Mafia!`); + } this.sendRoom(`Night ${this.dayNum}. PM the host your action, or idle.`, {declare: true}); const hasPlurality = this.getPlurality(); if (!early && hasPlurality) this.sendRoom(`Plurality is on ${this.players[hasPlurality] ? this.players[hasPlurality].name : 'No Lynch'}`); + if (!early) this.sendRoom(`|raw|
${this.lynchBox()}
`); this.updatePlayers(); } @@ -628,6 +638,7 @@ class MafiaTracker extends Rooms.RoomGame { if (this.getHammerValue(target) <= lynch.trueCount) { // HAMMER this.sendRoom(`Hammer! ${target === 'nolynch' ? 'Nobody' : Chat.escapeHTML(name)} was lynched!`, {declare: true}); + this.sendRoom(`|raw|
${this.lynchBox()}
`); if (target !== 'nolynch') this.eliminate(this.players[target], 'kill'); this.night(true); return; @@ -665,6 +676,25 @@ class MafiaTracker extends Rooms.RoomGame { player.updateHtmlRoom(); } + /** + * Returns HTML code that contains information on the current lynch vote. + * @return {string} + */ + lynchBox() { + if (!this.started) return `The game has not started yet.`; + let buf = `Lynches (Hammer: ${this.hammerCount})
`; + const plur = this.getPlurality(); + const list = Object.keys(this.lynches).sort((a, b) => { + if (a === plur) return -1; + if (b === plur) return 1; + return this.lynches[b].count - this.lynches[a].count; + }); + for (const key of list) { + buf += `${this.lynches[key].count}${plur === key ? '*' : ''} ${this.players[key] ? this.players[key].safeName : 'No Lynch'} (${this.lynches[key].lynchers.map(a => this.players[a] ? this.players[a].safeName : a).join(', ')})
`; + } + return buf; + } + /** * @param {User} user * @param {string} target @@ -947,6 +977,7 @@ class MafiaTracker extends Rooms.RoomGame { this.roleString = ''; } if (this.subs.includes(targetUser.userid)) this.subs.splice(this.subs.indexOf(targetUser.userid), 1); + this.played.push(targetUser.userid); this.players[targetUser.userid] = player; this.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added to the game by ${Chat.escapeHTML(user.name)}!`, {declare: true}); } @@ -1008,13 +1039,20 @@ class MafiaTracker extends Rooms.RoomGame { sub(player, replacement) { let oldPlayer = this.players[player]; if (!oldPlayer) return; // should never happen - if (oldPlayer.lynching) this.unlynch(oldPlayer.userid, true); const newUser = Users(replacement); if (!newUser) return; // should never happen let newPlayer = this.makePlayer(newUser); newPlayer.role = oldPlayer.role; newPlayer.IDEA = oldPlayer.IDEA; + if (oldPlayer.lynching) { + // Dont change plurality + let lynch = this.lynches[oldPlayer.lynching]; + lynch.lynchers.splice(lynch.lynchers.indexOf(oldPlayer.userid, 1)); + lynch.lynchers.push(newPlayer.userid); + newPlayer.lynching = oldPlayer.lynching; + oldPlayer.lynching = ''; + } this.players[newPlayer.userid] = newPlayer; this.players[oldPlayer.userid].destroy(); delete this.players[oldPlayer.userid]; @@ -1086,7 +1124,7 @@ class MafiaTracker extends Rooms.RoomGame { } this.IDEA.data = { - name: `${Chat.escapeHTML(this.host)}'s IDEA`, + name: `${this.host}'s IDEA`, // already escaped untrusted: true, roles: roleList, picks, @@ -1231,7 +1269,7 @@ class MafiaTracker extends Rooms.RoomGame { // if there's only one option, it's their role, parse it properly let roleName = ''; if (this.IDEA.data.picks.length === 1) { - const role = parseRole(player.IDEA.picks[this.IDEA.data.picks[0]]); + const role = MafiaTracker.parseRole(player.IDEA.picks[this.IDEA.data.picks[0]]); player.role = role.role; if (role.problems.length && !this.IDEA.data.untrusted) this.sendRoom(`Problems found when parsing IDEA role ${player.IDEA.picks[this.IDEA.data.picks[0]]}. Please report this to a mod.`); } else { @@ -1248,7 +1286,7 @@ class MafiaTracker extends Rooms.RoomGame { if (!this.IDEA.data.untrusted) { for (const pick of role) { if (pick.substr(0, 10) === 'alignment:') { - const parsedRole = parseRole(pick.substr(9)); + const parsedRole = MafiaTracker.parseRole(pick.substr(9)); if (parsedRole.problems.length) this.sendRoom(`Problems found when parsing IDEA role ${pick}. Please report this to a mod.`); player.role.alignment = parsedRole.role.alignment; } @@ -1295,11 +1333,13 @@ class MafiaTracker extends Rooms.RoomGame { * @return {void} */ updateHost() { - const host = Users(this.hostid); - if (!host || !host.connected) return; - if (this.ended) return host.send(`>view-mafia-${this.room.id}\n|deinit`); - const buf = Chat.pages.mafia([this.room.id], host); - host.send(`>view-mafia-${this.room.id}\n|init|html\n${buf}`); + for (const hostid of [...this.cohosts, this.hostid]) { + const host = Users(hostid); + if (!host || !host.connected) return; + if (this.ended) return host.send(`>view-mafia-${this.room.id}\n|deinit`); + const buf = Chat.pages.mafia([this.room.id], host); + host.send(`>view-mafia-${this.room.id}\n|init|html\n${buf}`); + } } /** @@ -1348,10 +1388,11 @@ class MafiaTracker extends Rooms.RoomGame { if (!this.room.users[user.userid]) return `${targetString} not in the room.`; if (this.players[user.userid]) return `${targetString} already in the game.`; if (this.hostid === user.userid) return `${targetString} the host.`; + if (this.cohosts.includes(user.userid)) return `${targetString} a cohost.`; if (!force) { for (const alt of user.getAltUsers(true)) { if (this.players[alt.userid]) return `${self ? `You already have` : `${user.userid} already has`} an alt in the game.`; - if (this.hostid === alt.userid) return `${self ? `You have` : `${user.userid} has`} an alt as the game host.`; + if (this.hostid === alt.userid || this.cohosts.includes(alt.userid)) return `${self ? `You have` : `${user.userid} has`} an alt as a game host.`; } } return false; @@ -1421,10 +1462,12 @@ class MafiaTracker extends Rooms.RoomGame { const subIndex = this.hostRequestedSub.indexOf(user.userid); if (subIndex !== -1) { this.hostRequestedSub.splice(subIndex, 1); - this.sendUser(this.hostid, `${user.userid} has spoken and been removed from the host sublist.`); + for (const hostid of [...this.cohosts, this.hostid]) { + this.sendUser(hostid, `${user.userid} has spoken and been removed from the host sublist.`); + } } - if (user.isStaff || (this.room.auth && this.room.auth[user.userid] && this.room.auth[user.userid] !== '+') || this.hostid === user.userid || !this.started) return false; + if (user.isStaff || (this.room.auth && this.room.auth[user.userid] && this.room.auth[user.userid] !== '+') || this.hostid === user.userid || this.cohosts.includes(user.userid) || !this.started) return false; if (!this.players[user.userid] && (!this.dead[user.userid] || !this.dead[user.userid].treestump)) return `You cannot talk while a game of ${this.title} is going on.`; if (this.phase === 'night') return `You cannot talk at night.`; return false; @@ -1498,8 +1541,10 @@ class MafiaTracker extends Rooms.RoomGame { logs.plays[month][player]++; } if (!logs.hosts[month]) logs.hosts[month] = {}; - if (!logs.hosts[month][this.hostid]) logs.hosts[month][this.hostid] = 0; - logs.hosts[month][this.hostid]++; + for (const hostid of [...this.cohosts, this.hostid]) { + if (!logs.hosts[month][hostid]) logs.hosts[month][hostid] = 0; + logs.hosts[month][hostid]++; + } writeFile(LOGS_FILE, logs); } if (this.timer) { @@ -1537,7 +1582,7 @@ const pages = { if (!room || !room.users[user.userid] || !room.game || room.game.gameid !== 'mafia' || room.game.ended) return `|deinit`; const game = /** @type {MafiaTracker} */ (room.game); const isPlayer = user.userid in game.players; - const isHost = user.userid === game.hostid; + const isHost = user.userid === game.hostid || game.cohosts.includes(user.userid); let buf = `|title|${game.title}\n|pagehtml|
`; buf += ``; buf += `

${game.title}

Host: ${game.host}

`; @@ -1559,7 +1604,7 @@ const pages = { if (i === selectedIndex) { buf += ``; } else { - buf += ``; + buf += ``; } } buf += `
`; @@ -1567,7 +1612,7 @@ const pages = { buf += `

`; buf += `

Role details:

`; for (const role of IDEA.originalChoices) { - const roleObject = parseRole(role).role; + const roleObject = MafiaTracker.parseRole(role).role; buf += `

${role}`; buf += `
    ${roleObject.memo.map(m => `
  • ${m}
  • `).join('')}
`; buf += `
`; @@ -1655,7 +1700,7 @@ const pages = { for (let p in game.players) { let player = game.players[p]; buf += `

`; - buf += `${player.safeName} (${player.role ? player.getRole() : ''})${game.lynchModifiers[p] !== undefined ? `(lynches worth ${game.getLynchValue(p)})` : ''}`; + buf += `${player.safeName} (${player.role ? player.getRole(true) : ''})${game.lynchModifiers[p] !== undefined ? `(lynches worth ${game.getLynchValue(p)})` : ''}`; buf += ` `; buf += ` `; buf += ` `; @@ -1766,8 +1811,10 @@ const commands = { if (room.game) return this.errorReply(`There is already a game of ${room.game.title} in progress in this room.`); if (!user.can('broadcast', null, room)) return this.errorReply(`/mafia ${cmd} - Access denied.`); + let nextHost = false; if (room.id === 'mafia') { if (cmd === 'nexthost') { + nextHost = true; if (!hostQueue.length) return this.errorReply(`Nobody is on the host queue.`); let skipped = []; do { @@ -1789,7 +1836,7 @@ const commands = { } if (!this.targetUser || !this.targetUser.connected) return this.errorReply(`The user "${this.targetUsername}" was not found.`); - if (this.targetUser.userid !== user.userid && !this.can('mute', null, room)) return false; + if (!nextHost && this.targetUser.userid !== user.userid && !this.can('mute', null, room)) return false; if (!room.users[this.targetUser.userid]) return this.errorReply(`${this.targetUser.name} is not in this room, and cannot be hosted.`); if (room.id === 'mafia' && isHostBanned(this.targetUser.userid)) return this.errorReply(`${this.targetUser.name} is banned from hosting games.`); @@ -1815,8 +1862,10 @@ const commands = { if (!room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`); if (room.id !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`); const args = target.split(',').map(toId); - if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0]) && !this.can('broadcast', null, room)) { - return false; + if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0])) { + const permission = (user.userid === args[1]) ? 'broadcast' : 'mute'; + if (['forceadd', 'add'].includes(args[0]) && !this.can(permission, null, room)) return; + if (['remove', 'del', 'delete'].includes(args[0]) && user.userid !== args[1] && !this.can('mute', null, room)) return; } else { if (!this.runBroadcast()) return false; } @@ -1901,7 +1950,7 @@ const commands = { playercap: function (target, room, user) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia playercap - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (game.phase !== 'signups') return this.errorReply(`Signups are already closed.`); if (toId(target) === 'none') target = '20'; const num = parseInt(target); @@ -1922,7 +1971,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia close - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (game.phase !== 'signups') return user.sendTo(targetRoom, `|error|Signups are already closed.`); if (game.playerCount < 2) return user.sendTo(targetRoom, `|error|You need at least 2 players to start.`); game.phase = 'locked'; @@ -1944,7 +1993,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia closedsetup - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const action = toId(args.join('')); if (!['on', 'off'].includes(action)) return this.parse('/help mafia closedsetup'); if (game.started) return user.sendTo(targetRoom, `|error|You can't ${action === 'on' ? 'enable' : 'disable'} closed setup because the game has already started.`); @@ -1966,7 +2015,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia reveal - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const action = toId(args.join('')); if (!['on', 'off'].includes(action)) return this.parse('/help mafia reveal'); if ((action === 'off' && game.noReveal) || (action === 'on' && !game.noReveal)) return user.sendTo(targetRoom, `|error|Revealing of roles is already ${game.noReveal ? 'disabled' : 'enabled'}.`); @@ -1976,25 +2025,33 @@ const commands = { }, revealhelp: [`/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ * # & ~`], + resetroles: 'setroles', + forceresetroles: 'setroles', forcesetroles: 'setroles', setroles: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); - if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(game.phase === 'signups' ? `You need to close signups first.` : `The game has already started.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; + const reset = cmd.includes('reset'); + if (reset) { + if (game.phase !== 'day' && game.phase !== 'night') return this.errorReply(`The game has not started yet.`); + } else { + if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(game.phase === 'signups' ? `You need to close signups first.` : `The game has already started.`); + } if (!target) return this.parse('/help mafia setroles'); - game.setRoles(user, target, cmd === 'forcesetroles'); + game.setRoles(user, target, cmd.includes('force'), reset); }, setroleshelp: [ - `/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player.`, - `/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`, + `/mafia setroles [comma separated roles] - Set the roles for a game of mafia. You need to provide one role per player.`, + `/mafia forcesetroles [comma separated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`, + `/mafia resetroles [comma separated roles] - Reset the roles in an ongoing game.`, ], idea: function (target, room, user) { if (!room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('broadcast', null, room) || (!user.can('mute', null, room) && game.hostid !== user.userid)) return this.errorReply(`/mafia idea - Access denied.`); + if (!user.can('broadcast', null, room) || (!user.can('mute', null, room) && game.hostid !== user.userid && !game.cohosts.includes(user.userid))) return this.errorReply(`/mafia idea - Access denied.`); if (game.started) return this.errorReply(`You cannot start an IDEA after the game has started.`); if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(`You need to close the signups first.`); game.ideaInit(user, toId(target)); @@ -2050,7 +2107,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia idereroll - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; game.ideaDistributeRoles(user); }, idearerollhelp: [`/mafia ideareroll - rerolls the roles for the current IDEA module. Requires host % @ * # & ~`], @@ -2073,7 +2130,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia start - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; game.start(user); }, starthelp: [`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`], @@ -2092,7 +2149,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (cmd === 'night') { game.night(); } else { @@ -2167,7 +2224,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; let action = toId(args.shift()); if (!action) return this.parse(`/help mafia selflynch`); if (this.meansYes(action)) { @@ -2198,7 +2255,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia kill - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const player = game.players[toId(args.join(''))]; if (!player) return user.sendTo(targetRoom, `|error|"${args.join(',')}" is not a living player.`); if (game.phase === 'IDEApicking') return this.errorReply(`You cannot add or remove players while IDEA roles are being picked.`); // needs to be here since eliminate doesn't pass the user @@ -2225,7 +2282,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia revive - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!toId(args.join(''))) return this.parse('/help mafia revive'); for (const targetUser of args) { game.revive(user, toId(targetUser), cmd === 'forceadd'); @@ -2238,13 +2295,13 @@ const commands = { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); target = toId(target); - if (!user.can('mute', null, room) && game.hostid !== user.userid && target) return this.errorReply(`/mafia deadline - Access denied.`); + if (target && game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (target === 'off') { return game.setDeadline(0); } else { const num = parseInt(target); if (isNaN(num)) { - if (game.hostid === user.userid && this.cmdToken === "!") { + if ((game.hostid === user.userid || game.cohosts.includes(user.userid)) && this.cmdToken === "!") { const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); if (room && room.lastBroadcast === broadcastMessage && room.lastBroadcastTime >= Date.now() - 20 * 1000) { @@ -2273,7 +2330,7 @@ const commands = { applyhammermodifier: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); const [player, mod] = target.split(','); if (cmd === 'applyhammermodifier') { @@ -2286,7 +2343,7 @@ const commands = { clearhammermodifiers: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); if (cmd === 'clearhammermodifiers') { game.clearHammerModifiers(user); @@ -2339,7 +2396,7 @@ const commands = { hammer: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); const hammer = parseInt(target); if ((isNaN(hammer) || hammer < 1) && cmd.toLowerCase() !== `resethammer`) return this.errorReply(`${target} is not a valid hammer count.`); @@ -2356,8 +2413,8 @@ const commands = { } }, hammerhelp: [ - `/mafia hammer (hammer) - sets the hammer count to (hammer) and resets lynches`, - `/mafia shifthammer (hammer) - sets the hammer count to (hammer) without resetting lynches`, + `/mafia hammer [hammer] - sets the hammer count to [hammer] and resets lynches`, + `/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting lynches`, `/mafia resethammer - sets the hammer to the default, resetting lynches`, ], @@ -2371,20 +2428,20 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (cmd === 'enablenl') { game.setNoLynch(user, true); } else { game.setNoLynch(user, false); } }, - enablenlhelp: [`/mafia enablenl OR /mafia disablenl - Allows or disallows players abstain from lynching. Requires host % @ # & ~`], + enablenlhelp: [`/mafia [enablenl|disablenl] - Allows or disallows players abstain from lynching. Requires host % @ # & ~`], lynches: function (target, room, user) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); if (!game.started) return this.errorReply(`The game of mafia has not started yet.`); - if (game.hostid === user.userid && this.cmdToken === "!") { + if ((game.hostid === user.userid || game.cohosts.includes(user.userid)) && this.cmdToken === "!") { const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); if (room && room.lastBroadcast === broadcastMessage && room.lastBroadcastTime >= Date.now() - 20 * 1000) { @@ -2398,24 +2455,14 @@ const commands = { } if (!this.runBroadcast()) return false; - let buf = `Lynches (Hammer: ${game.hammerCount})
`; - const plur = game.getPlurality(); - const list = Object.keys(game.lynches).sort((a, b) => { - if (a === plur) return -1; - if (b === plur) return 1; - return game.lynches[b].count - game.lynches[a].count; - }); - for (const key of list) { - buf += `${game.lynches[key].count}${plur === key ? '*' : ''} ${game.players[key] ? game.players[key].safeName : 'No Lynch'} (${game.lynches[key].lynchers.map(a => game.players[a] ? game.players[a].safeName : a).join(', ')})
`; - } - this.sendReplyBox(buf); + this.sendReplyBox(game.lynchBox()); }, pl: 'players', players: function (target, room, user) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (game.hostid === user.userid && this.cmdToken === "!") { + if ((game.hostid === user.userid || game.cohosts.includes(user.userid)) && this.cmdToken === "!") { const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); if (room && room.lastBroadcast === broadcastMessage && room.lastBroadcastTime >= Date.now() - 20 * 1000) { @@ -2458,6 +2505,15 @@ const commands = { this.sendReplyBox(`${showOrl ? `Original Rolelist: ` : `Rolelist: `}${roleString}`); }, + playerroles: function (target, room, user) { + if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); + const game = /** @type {MafiaTracker} */ (room.game); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid)) return this.errorReply(`Only the host can view roles.`); + if (!game.started) return this.errorReply(`The game has not started.`); + const players = [...Object.values(game.players), ...Object.values(game.dead)]; + this.sendReplyBox(players.map(p => `${p.safeName}: ${p.role ? p.role.safeName : 'No role'}`).join('
')); + }, + spectate: 'view', view: function (target, room, user, connection) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); @@ -2508,7 +2564,7 @@ const commands = { game.players[user.userid].updateHtmlRoom(); game.nextSub(); } else { - if (game.hostid === user.userid) return user.sendTo(targetRoom, `|error|The host cannot sub out of the game.`); + if (game.hostid === user.userid || game.cohosts.includes(user.userid)) return user.sendTo(targetRoom, `|error|The host cannot sub out of the game.`); if (!game.subs.includes(user.userid)) return user.sendTo(targetRoom, `|error|You are not on the sub list.`); game.subs.splice(game.subs.indexOf(user.userid), 1); // Update spectator's view @@ -2516,7 +2572,7 @@ const commands = { } break; case 'next': - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for force substituting a player.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; let toSub = args.shift(); if (!(toId(toSub) in game.players)) return user.sendTo(targetRoom, `|error|${toSub} is not in the game.`); if (!game.subs.length) { @@ -2528,7 +2584,7 @@ const commands = { } break; case 'remove': - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for force substituting a player.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const toRemove = toId(args.shift()); const toRemoveIndex = game.subs.indexOf(toRemove); if (toRemoveIndex === -1) return user.sendTo(room, `|error|${toRemove} is not on the sub list.`); @@ -2536,7 +2592,7 @@ const commands = { user.sendTo(room, `${toRemove} has been removed from the sublist`); break; case 'unrequest': - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for removing a player's sub request.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const toUnrequest = toId(args.shift()); const userIndex = game.requestedSub.indexOf(toUnrequest); const hostIndex = game.hostRequestedSub.indexOf(toUnrequest); @@ -2551,7 +2607,7 @@ const commands = { } break; default: - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for force substituting a player.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const toSubOut = action; const toSubIn = toId(args.shift()); if (!(toSubOut in game.players)) return user.sendTo(targetRoom, `|error|${toSubOut} is not in the game.`); @@ -2587,6 +2643,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (this.meansYes(toId(args.join('')))) { if (game.autoSub) return user.sendTo(targetRoom, `|error|Automatic subbing of players is already enabled.`); game.autoSub = true; @@ -2602,38 +2659,69 @@ const commands = { }, autosubhelp: [`/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Requires host % @ * # & ~`], + cohost: 'subhost', + forcecohost: 'subhost', forcesubhost: 'subhost', subhost: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); if (!this.canTalk()) return; - if (!target) return this.parse('/help mafia subhost'); + if (!target) return this.parse(`/help mafia ${cmd}`); if (!this.can('mute', null, room)) return false; this.splitTarget(target, false); let targetUser = this.targetUser; if (!targetUser || !targetUser.connected) return this.errorReply(`The user "${this.targetUsername}" was not found.`); if (!room.users[targetUser.userid]) return this.errorReply(`${targetUser.name} is not in this room, and cannot be hosted.`); if (game.hostid === targetUser.userid) return this.errorReply(`${targetUser.name} is already the host.`); - if (targetUser.userid in game.players) return this.errorReply(`You cannot subhost to a user in the game.`); + if (game.cohosts.includes(targetUser.userid)) return this.errorReply(`${targetUser.name} is already a cohost.`); + if (targetUser.userid in game.players) return this.errorReply(`The host cannot be ingame.`); if (targetUser.userid in game.dead) { - if (cmd !== 'forcesubhost') return this.errorReply(`${targetUser.name} could potentially be revived. To subhost anyway, use /mafia forcesubhost ${target}.`); + if (!cmd.includes('force')) return this.errorReply(`${targetUser.name} could potentially be revived. To continue anyway, use /mafia force${cmd} ${target}.`); + if (game.dead[targetUser.userid].lynching) game.unlynch(targetUser.userid); game.dead[targetUser.userid].destroy(); delete game.dead[targetUser.userid]; } - const oldHostid = game.hostid; - const oldHost = Users(game.hostid); - if (oldHost) oldHost.send(`>view-mafia-${room.id}\n|deinit`); - if (game.subs.includes(targetUser.userid)) game.subs.splice(game.subs.indexOf(targetUser.userid), 1); - const queueIndex = hostQueue.indexOf(targetUser.userid); - if (queueIndex > -1) hostQueue.splice(queueIndex, 1); - game.host = Chat.escapeHTML(targetUser.name); - game.hostid = targetUser.userid; - game.played.push(targetUser.userid); - targetUser.send(`>view-mafia-${room.id}\n|init|html\n${Chat.pages.mafia([room.id], targetUser)}`); - game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been substituted as the new host, replacing ${oldHostid}.`, {declare: true}); - this.modlog('MAFIASUBHOST', targetUser, `replacing ${oldHostid}`, {noalts: true, noip: true}); + if (cmd.includes('cohost')) { + game.cohosts.push(targetUser.userid); + game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true}); + targetUser.send(`>view-mafia-${room.id}\n|init|html\n|${Chat.pages.mafia([room.id], targetUser)}`); + this.modlog('MAFIACOHOST', targetUser, null, {noalts: true, noip: true}); + } else { + const oldHostid = game.hostid; + const oldHost = Users(game.hostid); + if (oldHost) oldHost.send(`>view-mafia-${room.id}\n|deinit`); + if (game.subs.includes(targetUser.userid)) game.subs.splice(game.subs.indexOf(targetUser.userid), 1); + const queueIndex = hostQueue.indexOf(targetUser.userid); + if (queueIndex > -1) hostQueue.splice(queueIndex, 1); + game.host = Chat.escapeHTML(targetUser.name); + game.hostid = targetUser.userid; + game.played.push(targetUser.userid); + targetUser.send(`>view-mafia-${room.id}\n|init|html\n${Chat.pages.mafia([room.id], targetUser)}`); + game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been substituted as the new host, replacing ${oldHostid}.`, {declare: true}); + this.modlog('MAFIASUBHOST', targetUser, `replacing ${oldHostid}`, {noalts: true, noip: true}); + } }, subhosthelp: [`/mafia subhost [user] - Substitues the user as the new game host.`], + cohosthelp: [`/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions.`], + + uncohost: 'removecohost', + removecohost: function (target, room, user) { + if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); + const game = /** @type {MafiaTracker} */ (room.game); + if (!this.canTalk()) return; + if (!target) return this.parse('/help mafia subhost'); + if (!this.can('mute', null, room)) return false; + target = toId(target); + + const cohostIndex = game.cohosts.indexOf(target); + if (cohostIndex < 0) { + if (game.hostid === target) return this.errorReply(`${target} is the host, not a cohost. Use /mafia subhost to replace them.`); + return this.errorReply(`${target} is not a cohost.`); + } + game.cohosts.splice(cohostIndex, 1); + game.sendRoom(`${target} was removed as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true}); + this.modlog('MAFIAUNCOHOST', target, null, {noalts: true, noip: true}); + }, '!end': true, end: function (target, room, user) { @@ -2644,7 +2732,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('broadcast', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia end - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('broadcast', null, room)) return; game.end(); this.room = targetRoom; this.modlog('MAFIAEND', null); @@ -2865,7 +2953,7 @@ const commands = { }, hostbanhelp: [ `/mafia hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ * # & ~`, - `/mafia (un)hostban [user] - Unbans a user from hosting games. Requires % @ * # & ~`, + `/mafia unhostban [user] - Unbans a user from hosting games. Requires % @ * # & ~`, ], disable: function (target, room, user) { @@ -2898,47 +2986,101 @@ const commands = { }, enablehelp: [`/mafia enable - Enables mafia in this room. Requires # & ~`], }, - mafiahelp: [ - `Commands for the Mafia plugin:`, - `/mafia host [user] - Create a game of Mafia with [user] as the host. Requires + % @ * # & ~`, - `/mafia nexthost - Host the next user in the host queue. Requires + % @ * # & ~`, - `/mafia queue [add|remove|view], [user] - Add: Adds the user to the queue. Requires + % @ * # & ~. Remove: Removes the user from the queue. Requires + % @ * # & ~. View: Shows the upcoming users who are going to host.`, - `/mafia join - Join the game.`, - `/mafia leave - Leave the game. Can only be done while signups are open.`, - `/mafia close - Closes signups for the current game. Requires: host % @ * # & ~`, - `/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ * # & ~`, - `/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ * # & ~`, - `/mafia selflynch [on|hammer|off] - Allows players to self lynch themselves either at hammer or anytime. Requires host % @ * # & ~`, - `/mafia enablenl OR /mafia disablenl - Allows or disallows players abstain from lynching. Requires host % @ # & ~`, - `/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player.`, - `/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`, - `/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`, - `/mafia day - Move to the next game day. Requires host % @ * # & ~`, - `/mafia night - Move to the next game night. Requires host % @ * # & ~`, - `/mafia extend (minutes) - Return to the previous game day. If (minutes) is provided, set the deadline for (minutes) minutes. Requires host % @ * # & ~`, - `/mafia lynch [player|nolynch] - Vote to lynch the specified player or to not lynch anyone.`, - `/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`, - `/mafia kill [player] - Kill a player, eliminating them from the game. Requires host % @ * # & ~`, - `/mafia treestump [player] - Kills a player, but allows them to talk during the day still.`, - `/mafia spirit [player] - Kills a player, but allows them to vote on the lynch still.`, - `/mafia spiritstump [player] - Kills a player, but allows them to talk during the day, and vote on the lynch.`, - `/mafia kick [player] - Kicks a player from the game without revealing their role.`, - `/mafia revive [player] - Revive a player who died or add a new player to the game. Requires host % @ * # & ~`, - `/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`, - `/mafia sub in - Request to sub into the game, or cancel a request to sub out.`, - `/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`, - `/mafia sub next [player] - Forcibly sub [player] out of the game. Requires host % @ * # & ~`, - `/mafia subhost [user] - Substitues the user as the new game host.`, - `/mafia end - End the current game of mafia. Requires host % @ * # & ~`, - `/mafia data [alignment|role|modifier|theme] - Get information on a mafia alignment, role, modifier, or theme.`, - `/mafia win (points) [user1], [user2], [user3], ... - Award the specified users points to the mafia leaderboard for this month. The amount of points can be negative to take points. Defaults to 10 points.`, - `/mafia mvp [user1], [user2], ... - Gives a MVP point and 5 leaderboard points to the users specified.`, - `/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 5 leaderboard points from the users specified.`, - `/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`, - `/mafia [hostlost|playlogs] - View the host logs or play logs for the current or last month. Requires % @ * # & ~`, - `/mafia disable - Disables mafia in this room. Requires # & ~`, - `/mafia enable - Enables mafia in this room. Requires # & ~`, - ], + mafiahelp: function (target, room, user) { + if (!this.runBroadcast()) return; + let buf = `Commands for the Mafia Plugin
Most commands are used through buttons in the game screen.

`; + buf += `
General Commands`; + buf += [ + `
General Commands for the Mafia Plugin:
`, + `/mafia host [user] - Create a game of Mafia with [user] as the host. Roomvoices can only host themselves. Requires + % @ * # & ~`, + `/mafia nexthost - Host the next user in the host queue. Only works in the Mafia Room. Requires + % @ * # & ~`, + `/mafia forcehost [user] - Bypass the host queue and host [user]. Only works in the Mafia Room. Requires % @ * # & ~`, + `/mafia sub in - Request to sub into the game, or cancel a request to sub out.`, + `/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`, + `/mafia spectate - Spectate the game of mafia.`, + `/mafia ideadiscards - Shows the discarded roles list for an IDEA module.`, + `/mafia lynches - Display the current lynch count, and whos lynching who.`, + `/mafia players - Display the current list of players, will highlight players.`, + `/mafia [rl|orl] - Display the role list or the original role list for the current game.`, + `/mafia data [alignment|role|modifier|theme] - Get information on a mafia alignment, role, modifier, or theme.`, + `/mafia subhost [user] - Substitues the user as the new game host. Requires % @ * # & ~`, + `/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions. Requires % @ * # & ~`, + `/mafia uncohost [user] - Remove [user]'s cohost status. Requires % @ * # & ~`, + `/mafia disable - Disables mafia in this room. Requires # & ~`, + `/mafia enable - Enables mafia in this room. Requires # & ~`, + ].join('
'); + buf += `
Player Commands`; + buf += [ + `
Commands that players can use:
`, + `/mafia join - Join the game.`, + `/mafia leave - Leave the game. Can only be done while signups are open.`, + `/mafia lynch [player|nolynch] - Vote to lynch the specified player or to not lynch anyone.`, + `/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`, + `/mafia deadline - View the deadline for the current game.`, + `/mafia sub in - Request to sub into the game, or cancel a request to sub out.`, + `/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`, + `/mafia ideapick [selection], [role] - Selects a role from an IDEA module`, + ].join('
'); + buf += `
Host Commands`; + buf += [ + `
Commands for game hosts and Cohosts to use:
`, + `/mafia playercap [cap|none]- Limit the number of players able to join the game. Player cap cannot be more than 20 or less than 2. Requires: host % @ # & ~`, + `/mafia close - Closes signups for the current game. Requires: host % @ * # & ~`, + `/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ * # & ~`, + `/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ * # & ~`, + `/mafia selflynch [on|hammer|off] - Allows players to self lynch themselves either at hammer or anytime. Requires host % @ * # & ~`, + `/mafia [enablenl|disablenl] - Allows or disallows players abstain from lynching. Requires host % @ # & ~`, + `/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player. Requires host % @ # & ~`, + `/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set. Requires host % @ # & ~`, + `/mafia idea [idea] - starts an IDEA module. Requires + % @ * # & ~, voices can only start for themselves`, + `/mafia ideareroll - rerolls the current IDEA module. Requires host % @ * # & ~`, + `/mafia customidea choices, picks (new line here, shift+enter)`, + `(comma or newline separated rolelist) - Starts an IDEA module with custom roles. Requires % @ # & ~`, + `/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`, + `/mafia day - Move to the next game day. Requires host % @ * # & ~`, + `/mafia night - Move to the next game night. Requires host % @ * # & ~`, + `/mafia extend (minutes) - Return to the previous game day. If (minutes) is provided, set the deadline for (minutes) minutes. Requires host % @ * # & ~`, + `/mafia kill [player] - Kill a player, eliminating them from the game. Requires host % @ * # & ~`, + `/mafia treestump [player] - Kills a player, but allows them to talk during the day still. Requires host % @ * # & ~`, + `/mafia spirit [player] - Kills a player, but allows them to vote on the lynch still. Requires host % @ * # & ~`, + `/mafia spiritstump [player] - Kills a player, but allows them to talk during the day, and vote on the lynch. Requires host % @ * # & ~`, + `/mafia kick [player] - Kicks a player from the game without revealing their role. Requires host % @ * # & ~`, + `/mafia revive [player] - Revive a player who died or add a new player to the game. Requires host % @ * # & ~`, + `/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`, + `/mafia sub next, [player] - Forcibly sub [player] out of the game. Requires host % @ * # & ~`, + `/mafia sub remove, [user] - Forcibly remove [user] from the sublist. Requres host % @ * # & ~`, + `/mafia sub unrequest, [player] - Remove's a player's request to sub out of the game. Requires host % @ * # & ~`, + `/mafia sub [player], [user] - Forcibly sub [player] for [user]. Requires host % @ * # & ~`, + `/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Defaults to yes. Requires host % @ * # & ~`, + `/mafia [love|hate] [player] - Makes it take 1 more (love) or less (hate) lynch to hammer [player]. Requires host % @ * # & ~`, + `/mafia [unlove|unhate] [player] - Removes loved or hated status from [player]. Requires host % @ * # & ~`, + `/mafia [mayor|voteless] [player] - Makes [player]'s' lynch worth 2 votes (mayor) or makes [player]'s lynch worth 0 votes (voteless). Requires host % @ * # & ~`, + `/mafia [unmayor|unvoteless] [player] - Removes mayor or voteless status from [player]. Requires host % @ * # & ~`, + `/mafia hammer [hammer] - sets the hammer count to [hammer] and resets lynches`, + `/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting lynches`, + `/mafia resethammer - sets the hammer to the default, resetting lynches`, + `/mafia playerroles - View all the player's roles in chat. Requires host`, + `/mafia end - End the current game of mafia. Requires host % @ * # & ~`, + ].join('
'); + buf += `
Mafia Room Specific Commands`; + buf += [ + `
Commands that are only useable in the Mafia Room:
`, + `/mafia queue add, [user] - Adds the user to the host queue. Requires % @ * # & ~.`, + `/mafia queue remove, [user] - Removes the user from the queue. You can remove yourself regardless of rank. Requires % @ * # & ~.`, + `/mafia queue - Shows the list of users who are in queue to host.`, + `/mafia win (points) [user1], [user2], [user3], ... - Award the specified users points to the mafia leaderboard for this month. The amount of points can be negative to take points. Defaults to 10 points.`, + `/mafia winfaction (points), [faction] - Award the specified points to all the players in the given faction. Requires % @ * # & ~`, + `/mafia mvp [user1], [user2], ... - Gives a MVP point and 10 leaderboard points to the users specified.`, + `/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 10 leaderboard points from the users specified.`, + `/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`, + `/mafia [hostlost|playlogs] - View the host logs or play logs for the current or last month. Requires % @ * # & ~`, + `/mafia hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ * # & ~`, + `/mafia unhostban [user] - Unbans a user from hosting games. Requires % @ * # & ~`, + ].join('
'); + buf += `
`; + + return this.sendReplyBox(buf); + }, }; module.exports = { diff --git a/chat-plugins/modlog.js b/chat-plugins/modlog.js index 79a543a991a1..6e0ac24d7931 100644 --- a/chat-plugins/modlog.js +++ b/chat-plugins/modlog.js @@ -38,7 +38,7 @@ class SortedLimitedLengthList { return this.list.slice(); } - tryInsert(element) { + insert(element) { let insertedAt = -1; for (let i = this.list.length - 1; i >= 0; i--) { if (element.localeCompare(this.list[i]) < 0) { @@ -54,9 +54,7 @@ class SortedLimitedLengthList { if (insertedAt < 0) this.list.splice(0, 0, element); if (this.list.length > this.maxSize) { this.list.pop(); - if (insertedAt === this.list.length) return false; } - return true; } } @@ -76,7 +74,7 @@ function checkRipgrepAvailability() { return Config.ripgrepmodlog; } -function getMoreButton(room, search, useExactSearch, lines, maxLines) { +function getMoreButton(roomid, search, useExactSearch, lines, maxLines) { let newLines = 0; for (let increase of MORE_BUTTON_INCREMENTS) { if (increase > lines) { @@ -88,7 +86,7 @@ function getMoreButton(room, search, useExactSearch, lines, maxLines) { return ''; // don't show a button if no more pre-set increments are valid or if the amount of results is already below the max } else { if (useExactSearch) search = Chat.escapeHTML(`"${search}"`); - return `
`; + return `
`; } } @@ -144,8 +142,7 @@ async function checkRoomModlog(path, regex, results) { let line; while ((line = await fileStream.readLine()) !== null) { if (!regex || regex.test(line)) { - const insertionSuccessful = results.tryInsert(line); - if (!insertionSuccessful) break; + results.insert(line); } } fileStream.destroy(); @@ -160,17 +157,17 @@ function runRipgrepModlog(paths, regexString, results) { return results; } for (const fileName of stdout.toString().split('\n').reverse()) { - if (fileName) results.tryInsert(fileName); + if (fileName) results.insert(fileName); } return results; } -function prettifyResults(resultArray, room, searchString, exactSearch, addModlogLinks, hideIps, maxLines) { +function prettifyResults(resultArray, roomid, searchString, exactSearch, addModlogLinks, hideIps, maxLines) { if (resultArray === null) { return "The modlog query has crashed."; } let roomName; - switch (room) { + switch (roomid) { case 'all': roomName = "all rooms"; break; @@ -178,13 +175,13 @@ function prettifyResults(resultArray, room, searchString, exactSearch, addModlog roomName = "all public rooms"; break; default: - roomName = `room ${room}`; + roomName = `room ${roomid}`; } if (!resultArray.length) { return `|popup|No moderator actions containing ${searchString} found on ${roomName}.` + (exactSearch ? "" : " Add quotes to the search parameter to search for a phrase, rather than a user."); } - const title = `[${room}]` + (searchString ? ` ${searchString}` : ``); + const title = `[${roomid}]` + (searchString ? ` ${searchString}` : ``); let lines = resultArray.length; let curDate = ''; resultArray.unshift(''); @@ -218,15 +215,17 @@ function prettifyResults(resultArray, room, searchString, exactSearch, addModlog return `${date}[${timestamp}] (${thisRoomID})${Chat.escapeHTML(line.slice(parenIndex + 1))}`; }).join(`
`); let preamble; - const modlogid = room + (searchString ? '-' + Dashycode.encode(searchString) : ''); + const modlogid = roomid + (searchString ? '-' + Dashycode.encode(searchString) : ''); if (searchString) { const searchStringDescription = (exactSearch ? `containing the string "${searchString}"` : `matching the username "${searchString}"`); - preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n|pagehtml|

The last ${Chat.count(lines, "logged actions")} ${searchStringDescription} on ${roomName}.` + + preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n` + + `|pagehtml|

The last ${Chat.count(lines, "logged actions")} ${searchStringDescription} on ${roomName}.` + (exactSearch ? "" : " Add quotes to the search parameter to search for a phrase, rather than a user."); } else { - preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n|pagehtml|

The last ${Chat.count(lines, "lines")} of the Moderator Log of ${roomName}.`; + preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n` + + `|pagehtml|

The last ${Chat.count(lines, "lines")} of the Moderator Log of ${roomName}.`; } - let moreButton = getMoreButton(room, searchString, exactSearch, lines, maxLines); + let moreButton = getMoreButton(roomid, searchString, exactSearch, lines, maxLines); return `${preamble}${resultString}${moreButton}

`; } diff --git a/chat-plugins/room-events.js b/chat-plugins/room-events.js index 7abb0fe71f33..bb0d0dc1ba96 100644 --- a/chat-plugins/room-events.js +++ b/chat-plugins/room-events.js @@ -94,11 +94,71 @@ exports.commands = { help: function (target, room, user) { return this.parse('/help roomevents'); }, + sortby: function (target, room, user) { + // preconditions + if (!room.chatRoomData) return this.errorReply("This command is unavailable in temporary rooms."); + if (!room.events || !Object.keys(room.events).length) { + return this.errorReply("There are currently no planned upcoming events for this room."); + } + if (!this.can('ban', null, room)) return false; + + // declare variables + let multiplier = 1; + let columnName = ""; + let delimited = target.split(target.includes('|') ? '|' : ','); + let sortable = Object.values(room.events); + + // id tokens + if (delimited.length === 1) { + columnName = target; + } else { + let order = ""; + [columnName, order] = delimited; + order = toId(order); + multiplier = (order === 'desc') ? -1 : 1; + } + + // sort the array by the appropriate column name + columnName = toId(columnName); + switch (columnName) { + case "date": + case "eventdate": + sortable.sort((a, b) => { return (toId(a.date) < toId(b.date)) ? -1 * multiplier : (toId(b.date) < toId(a.date)) ? 1 * multiplier : 0; }); + break; + case "desc": + case "description": + case "eventdescription": + sortable.sort((a, b) => { return (toId(a.desc) < toId(b.desc)) ? -1 * multiplier : (toId(b.desc) < toId(a.desc)) ? 1 * multiplier : 0; }); + break; + case "eventname": + case "name": + sortable.sort((a, b) => { return (toId(a.eventName) < toId(b.eventName)) ? -1 * multiplier : (toId(b.eventName) < toId(a.eventName)) ? 1 * multiplier : 0; }); + break; + default: + return this.errorReply("No or invalid column name specified. Please use one of: date, eventdate, desc, description, eventdescription, eventname, name."); + } + + // rebuild the room.events object + room.events = {}; + for (const sortedObj of sortable) { + const eventId = toId(sortedObj.eventName); + room.events[eventId] = sortedObj; + } + room.chatRoomData.events = room.events; + + // build communication string + const resultString = `sorted by column:` + columnName + + ` in ${multiplier === 1 ? "ascending" : "descending"} order` + + `${delimited.length === 1 ? " (by default)" : ""}`; + this.modlog('ROOMEVENT', null, resultString); + return this.sendReply(resultString); + }, }, roomeventshelp: [ `/roomevents - Displays a list of upcoming room-specific events.`, `/roomevents add [event name] | [event date/time] | [event description] - Adds a room event. Requires: @ # & ~`, `/roomevents remove [event name] - Deletes an event. Requires: @ # & ~`, `/roomevents view [event name] - Displays information about a specific event.`, + `/roomevents sortby [column name] | [asc/desc (optional)] - Sorts events table by column name and an optional argument to ascending or descending order. Ascending order is default`, ], }; diff --git a/chat-plugins/roomsettings.js b/chat-plugins/roomsettings.js index b5d3e735c7ea..ef1c5f1dac27 100644 --- a/chat-plugins/roomsettings.js +++ b/chat-plugins/roomsettings.js @@ -157,8 +157,20 @@ class RoomSettings { return `${this.button('Mafia enabled', null, 'mafia enable')} ${this.button('off', true)}`; } } + language() { + const languageList = ['Portuguese', 'Spanish', 'Italian', 'French', 'Simplified Chinese', 'Traditional Chinese', 'Japanese', 'Hindi', 'Turkish', 'Dutch', 'German']; + if (!this.user.can('editroom', null, this.room)) return this.button(this.room.language ? this.room.language : 'English', true); + + let languageOutput = []; + languageOutput.push(this.button(`English`, !this.room.language, 'roomlanguage english')); + for (let language of languageList) { + languageOutput.push(this.button(`${language}`, this.room.language === toId(language), `roomlanguage ${toId(language)}`)); + } + return languageOutput.join(' '); + } generateDisplay(user, room, connection) { let output = Chat.html`
Room Settings for ${this.room.title}
`; + output += `Language:
${this.language()}
`; output += `Modchat:
${this.modchat()}
`; output += `Modjoin:
${this.modjoin()}
`; output += `Stretch filter:
${this.stretching()}
`; @@ -349,6 +361,43 @@ exports.commands = { `/modjoin [sync|off] - Sets modjoin. Only users who can speak in modchat can join this room. Requires: \u2606 # & ~`, ], + roomlanguage: function (target, room, user) { + const languageTable = { + __proto__: null, + portuguese: 'Portuguese', + spanish: 'Spanish', + italian: 'Italian', + french: 'French', + simplifiedchinese: 'Simplified Chinese', + traditionalchinese: 'Traditional Chinese', + japanese: 'Japanese', + hindi: 'Hindi', + turkish: 'Turkish', + dutch: 'Dutch', + german: 'German', + // Listed as "false" under room.language + english: 'English', + }; + if (!target) return this.sendReply(`This room's primary language is ${languageTable[room.language] || 'English'}`); + if (!this.can('editroom', null, room)) return false; + + let targetLanguage = toId(target); + if (!(targetLanguage in languageTable)) return this.errorReply(`"${target}" is not a supported language.`); + + room.language = targetLanguage === 'english' ? false : targetLanguage; + + if (room.chatRoomData) { + room.chatRoomData.language = room.language; + Rooms.global.writeChatRoomData(); + } + this.modlog(`LANGUAGE`, null, languageTable[targetLanguage]); + this.sendReply(`The room's language has been set to ${languageTable[targetLanguage]}`); + }, + roomlanguagehelp: [ + `/roomlanguage [language] - Sets the the language for the room, which changes language of a few commands. Requires # & ~`, + `Supported Languages: English, Spanish, Italian, French, Simplified Chinese, Traditional Chinese, Japanese, Hindi, Turkish, Dutch, German.`, + ], + slowchat: function (target, room, user) { if (!target) { const slowchatSetting = (room.slowchat || "OFF"); @@ -361,14 +410,12 @@ exports.commands = { if (this.meansNo(target)) { if (!room.slowchat) return this.errorReply(`Slow chat is already disabled in this room.`); room.slowchat = false; - this.add("|raw|
Slow chat was disabled!
There is no longer a set minimum time between messages.
"); } else if (targetInt) { if (!user.can('bypassall') && room.userCount < SLOWCHAT_USER_REQUIREMENT) return this.errorReply(`This room must have at least ${SLOWCHAT_USER_REQUIREMENT} users to set slowchat; it only has ${room.userCount} right now.`); if (room.slowchat === targetInt) return this.errorReply(`Slow chat is already set to ${room.slowchat} seconds in this room.`); if (targetInt < SLOWCHAT_MINIMUM) targetInt = SLOWCHAT_MINIMUM; if (targetInt > SLOWCHAT_MAXIMUM) targetInt = SLOWCHAT_MAXIMUM; room.slowchat = targetInt; - this.add(`|raw|
Slow chat was enabled!
Messages must have at least ${room.slowchat} seconds between them.
`); } else { return this.parse("/help slowchat"); } diff --git a/chat-plugins/scavenger-games.js b/chat-plugins/scavenger-games.js index 67f635b96ec9..9a34feb3169e 100644 --- a/chat-plugins/scavenger-games.js +++ b/chat-plugins/scavenger-games.js @@ -526,7 +526,10 @@ class Incognito extends ScavGame { value = toId(value); let player = hunt.players[user.userid]; - if (player.completed) return player.sendRoom(`That may or may not be the right answer - if you aren't confident, you can try again!`); + if (player.completed) { + if (!this.blind) return; + return player.sendRoom(`That may or may not be the right answer - if you aren't confident, you can try again!`); + } hunt.validatePlayer(player); diff --git a/chat-plugins/scavengers.js b/chat-plugins/scavengers.js index 4f36c9d3aa47..5349819fb3d0 100644 --- a/chat-plugins/scavengers.js +++ b/chat-plugins/scavengers.js @@ -522,7 +522,7 @@ class ScavengerHunt extends Rooms.RoomGame { } let uniqueConnections = this.getUniqueConnections(Users(player.userid)); - if (uniqueConnections > 1) { + if (uniqueConnections > 1 && this.room.scavmod && this.room.scavmod.ipcheck) { // multiple users on one alt player.sendRoom("You have been caught for attempting a hunt with multiple connections on your account. Staff has been notified."); @@ -1373,6 +1373,46 @@ let commands = { this.privateModAction(`(${user.name} has ${(change > 0 ? 'given' : 'taken')} one infraction point ${(change > 0 ? 'to' : 'from')} '${targetId}'.)`); this.modlog(`SCAV ${this.cmd.toUpperCase()}`, user); }, + + modsettings: { + '': 'update', + 'update': function (target, room, user) { + if (!this.can('declare', null, room) || room.id !== 'scavengers') return false; + let settings = room.scavmod || {}; + + this.sendReply(`|uhtml${this.cmd === 'update' ? 'change' : ''}|scav-modsettings|
Scavenger Moderation Settings:

` + + ` Multiple connection verification: ${settings.ipcheck ? 'ON' : 'OFF'}` + + `
`); + }, + + 'ipcheck': function (target, room, user) { + if (!this.can('declare', null, room) || room.id !== 'scavengers') return false; + + let settings = scavsRoom.scavmod || {}; + target = toId(target); + + let setting = { + 'on': true, + 'off': false, + 'toggle': !settings.ipcheck, + }; + + if (!(target in setting)) return this.sendReply('Invalid setting - ON, OFF, TOGGLE'); + + settings.ipcheck = setting[target]; + room.scavmod = settings; + + if (scavsRoom.chatRoomData) { + scavsRoom.chatRoomData.scavmod = scavsRoom.scavmod; + Rooms.global.writeChatRoomData(); + } + + this.privateModAction(`(${user.name} has set multiple connections verification to ${setting[target] ? 'ON' : 'OFF'}.)`); + this.modlog('SCAV MODSETTINGS IPCHECK', null, setting[target] ? 'ON' : 'OFF'); + + this.parse('/scav modsettings update'); + }, + }, }; exports.commands = { diff --git a/chat-plugins/trivia.js b/chat-plugins/trivia.js index f5782bb11187..c1ed24625ab9 100644 --- a/chat-plugins/trivia.js +++ b/chat-plugins/trivia.js @@ -543,10 +543,26 @@ class Trivia extends Rooms.RoomGame { */ verifyAnswer(tarAnswer) { return this.curAnswers.some(answer => ( - (answer === tarAnswer) || (answer.length > 5 && Dex.levenshtein(tarAnswer, answer) < 3) + (answer === tarAnswer) || (Dex.levenshtein(tarAnswer, answer) <= this.maxLevenshteinAllowed(answer.length)) )); } + /** + * Return the maximum Levenshtein distance that is allowable for answers of the given length. + * @param {number} answerLength + * @return {number} + */ + maxLevenshteinAllowed(answerLength) { + if (answerLength > 5) { + return 2; + } + + if (answerLength > 4) { + return 1; + } + + return 0; + } /** * This is a noop here since it'd defined properly by mode subclasses later * on. This calculates the points a correct responder earns, which is diff --git a/chat-plugins/uno.js b/chat-plugins/uno.js index e3eef77235b5..c9265b48ddac 100644 --- a/chat-plugins/uno.js +++ b/chat-plugins/uno.js @@ -29,6 +29,14 @@ const textColors = { const textShadow = 'text-shadow: 1px 0px black, -1px 0px black, 0px -1px black, 0px 1px black, 2px -2px black;'; +/** @typedef {'Green' | 'Yellow' | 'Red' | 'Blue' | 'Black'} Color */ +/** @typedef {{value: string, color: Color, changedColor?: Color, name: string}} Card */ + +/** + * @param {Card} card + * @param {boolean} fullsize + * @return {string} + */ function cardHTML(card, fullsize) { let surface = card.value.replace(/[^A-Z0-9+]/g, ""); let background = rgbGradients[card.color]; @@ -37,26 +45,55 @@ function cardHTML(card, fullsize) { return ``; } +/** + * @return {Card[]} + */ function createDeck() { + /** @type {Color[]} */ const colors = ['Red', 'Blue', 'Green', 'Yellow']; const values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'Reverse', 'Skip', '+2']; - let basic = []; + let basic = /** @type {Card[]} */ ([]); for (const color of colors) { basic.push(...values.map(v => { - return {value: v, color: color, name: color + " " + v}; + /** @type {Card} */ + let c = {value: v, color: color, name: color + " " + v}; + return c; })); } - return [...basic, ...basic, // two copies of the basic stuff (total 96) - ...[0, 1, 2, 3].map(v => ({color: colors[v], value: '0', name: colors[v] + ' 0'})), // the 4 0s - ...[0, 1, 2, 3].map(v => ({color: 'Black', value: 'Wild', name: 'Wild'})), // wild cards - ...[0, 1, 2, 3].map(v => ({color: 'Black', value: '+4', name: "Wild +4"})), // wild +4 cards + return [ + // two copies of the basic stuff (total 96) + ...basic, + ...basic, + // The four 0s + ...[0, 1, 2, 3].map(v => { + /** @type {Card} */ + let c = {color: colors[v], value: '0', name: colors[v] + ' 0'}; + return c; + }), + // Wild cards + ...[0, 1, 2, 3].map(v => { + /** @type {Card} */ + let c = {color: 'Black', value: 'Wild', name: 'Wild'}; + return c; + }), + // Wild +4 cards + ...[0, 1, 2, 3].map(v => { + /** @type {Card} */ + let c = {color: 'Black', value: '+4', name: 'Wild +4'}; + return c; + }), ]; // 108 cards } -class UNOgame extends Rooms.RoomGame { +class UnoGame extends Rooms.RoomGame { + /** + * @param {ChatRoom} room + * @param {number} cap + * @param {boolean} suppressMessages + */ constructor(room, cap, suppressMessages) { super(room); @@ -66,34 +103,50 @@ class UNOgame extends Rooms.RoomGame { room.gameNumber = 1; } - cap = parseInt(cap) || 6; - if (cap < 2) cap = 2; - + /** @type {number} */ this.playerCap = cap; this.allowRenames = true; + /** @type {number} */ this.maxTime = maxTime; + /** @type {NodeJS.Timer?} */ + this.timer = null; + /** @type {NodeJS.Timer?} */ this.autostartTimer = null; + /** @type {string} */ this.gameid = 'uno'; this.title = 'UNO'; + /** @type {string} */ this.state = 'signups'; - this.currentPlayer = null; + /** @type {string} */ + this.currentPlayerid = ''; + /** @type {Card[]} */ this.deck = Dex.shuffle(createDeck()); + /** @type {Card[]} */ this.discards = []; + /** @type {Card?} */ this.topCard = null; + /** @type {string?} */ + this.awaitUno = null; + /** @type {string?} */ + this.unoId = null; this.direction = 1; this.suppressMessages = suppressMessages || false; - this.spectators = {}; + this.spectators = Object.create(null); this.sendToRoom(`|uhtml|uno-${this.room.gameNumber}|

A new game of UNO is starting!


Or use /uno join to join the game.

${(this.suppressMessages ? `

Game messages will be shown to only players. If you would like to spectate the game, use /uno spectate

` : '')}
`, true); } onUpdateConnection() {} + /** + * @param {User} user + * @param {Connection} connection + */ onConnect(user, connection) { if (this.state === 'signups') { connection.sendTo(this.room, `|uhtml|uno-${this.room.gameNumber}|

A new game of UNO is starting!


Or use /uno join to join the game.

${(this.suppressMessages ? `

Game messages will be shown to only players. If you would like to spectate the game, use /uno spectate

` : '')}
`); @@ -102,6 +155,9 @@ class UNOgame extends Rooms.RoomGame { } } + /** + * @return {false | void} + */ onStart() { if (this.playerCount < 2) return false; if (this.autostartTimer) clearTimeout(this.autostartTimer); @@ -127,6 +183,10 @@ class UNOgame extends Rooms.RoomGame { this.nextTurn(true); } + /** + * @param {User} user + * @return {boolean} + */ joinGame(user) { if (this.state === 'signups' && this.addPlayer(user)) { this.sendToRoom(`${user.name} has joined the game of UNO.`); @@ -135,6 +195,10 @@ class UNOgame extends Rooms.RoomGame { return false; } + /** + * @param {User} user + * @return {boolean} + */ leaveGame(user) { if (this.state === 'signups' && this.removePlayer(user)) { this.sendToRoom(`${user.name} has left the game of UNO.`); @@ -143,11 +207,22 @@ class UNOgame extends Rooms.RoomGame { return false; } - // overwrite the default makePlayer so it makes a UNOgamePlayer instead. + /** + * Overwrite the default makePlayer so it makes an UnoGamePlayer instead. + * @param {User} user + * @return {UnoGamePlayer} + */ makePlayer(user) { - return new UNOgamePlayer(user, this); + return new UnoGamePlayer(user, this); } + /** + * @param {User} user + * @param {string} oldUserid + * @param {boolean} isJoining + * @param {boolean} isForceRenamed + * @return {false | void} + */ onRename(user, oldUserid, isJoining, isForceRenamed) { if (!(oldUserid in this.players) || user.userid === oldUserid) return false; if (!user.named && !isForceRenamed) { @@ -163,39 +238,52 @@ class UNOgame extends Rooms.RoomGame { this.players[user.userid].userid = user.userid; if (this.awaitUno && this.awaitUno === oldUserid) this.awaitUno = user.userid; - if (this.currentPlayer && this.currentPlayer === oldUserid) this.currentPlayer = user.userid; + if (this.currentPlayerid === oldUserid) this.currentPlayerid = user.userid; } + /** + * @param {string} userid + * @return {string | false} + */ eliminate(userid) { if (!(userid in this.players)) return false; let name = this.players[userid].name; if (this.playerCount === 2) { - this.removePlayer({userid: userid}); + this.removePlayer(this.players[userid]); this.onWin(this.players[Object.keys(this.players)[0]]); return name; } // handle current player... - if (userid === this.currentPlayer) { + if (userid === this.currentPlayerid) { if (this.state === 'color') { + if (!this.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } this.topCard.changedColor = this.discards[1].changedColor || this.discards[1].color; this.sendToRoom(`|raw|${Chat.escapeHTML(name)} has not picked a color, the color will stay as ${this.topCard.changedColor}.`); } - clearTimeout(this.timer); + if (this.timer) clearTimeout(this.timer); this.nextTurn(); } + if (this.awaitUno === userid) this.awaitUno = null; // put that player's cards into the discard pile to prevent cards from being permanently lost this.discards.push(...this.players[userid].hand); - this.removePlayer({userid: userid}); + this.removePlayer(this.players[userid]); return name; } - sendToRoom(msg, overrideSuppress) { + /** + * @param {string} msg + * @param {boolean} [overrideSuppress] + */ + sendToRoom(msg, overrideSuppress = false) { if (!this.suppressMessages || overrideSuppress) { this.room.add(msg).update(); } else { @@ -213,15 +301,22 @@ class UNOgame extends Rooms.RoomGame { } } + /** + * @param {boolean} [showCards] + * @return {string[]} + */ getPlayers(showCards) { let playerList = Object.keys(this.players); if (!showCards) { return playerList.sort().map(id => Chat.escapeHTML(this.players[id].name)); } if (this.direction === -1) playerList = playerList.reverse(); - return playerList.map(id => `${(this.currentPlayer && this.currentPlayer === id ? '' : '')}${Chat.escapeHTML(this.players[id].name)} (${this.players[id].hand.length}) ${(this.currentPlayer && this.currentPlayer === id ? '' : "")}`); + return playerList.map(id => `${(this.currentPlayerid === id ? '' : '')}${Chat.escapeHTML(this.players[id].name)} (${this.players[id].hand.length}) ${(this.currentPlayerid === id ? '' : "")}`); } + /** + * @return {Promise} + */ onAwaitUno() { return new Promise((resolve, reject) => { if (!this.awaitUno) return resolve(); @@ -237,40 +332,46 @@ class UNOgame extends Rooms.RoomGame { }); } + /** + * @param {boolean} [starting] + */ nextTurn(starting) { this.onAwaitUno() .then(() => { if (!starting) this.onNextPlayer(); - clearTimeout(this.timer); - let player = this.players[this.currentPlayer]; + if (this.timer) clearTimeout(this.timer); + let player = this.players[this.currentPlayerid]; this.sendToRoom(`|c:|${(Math.floor(Date.now() / 1000))}|~|${player.name}'s turn.`); this.state = 'play'; - if (player.cardLock) delete player.cardLock; + if (player.cardLock) player.cardLock = null; player.sendDisplay(); this.timer = setTimeout(() => { this.sendToRoom(`${player.name} has been automatically disqualified.`); - this.eliminate(this.currentPlayer); + this.eliminate(this.currentPlayerid); }, this.maxTime * 1000); }); } onNextPlayer() { // if none is set - if (!this.currentPlayer) { + if (!this.currentPlayerid) { let userList = Object.keys(this.players); - this.currentPlayer = userList[Math.floor(this.playerCount * Math.random())]; + this.currentPlayerid = userList[Math.floor(this.playerCount * Math.random())]; } - this.currentPlayer = this.getNextPlayer(); + this.currentPlayerid = this.getNextPlayer(); } + /** + * @return {string} + */ getNextPlayer() { let userList = Object.keys(this.players); - let player = userList[(userList.indexOf(this.currentPlayer) + this.direction)]; + let player = userList[(userList.indexOf(this.currentPlayerid) + this.direction)]; if (!player) { player = this.direction === 1 ? userList[0] : userList[this.playerCount - 1]; @@ -278,33 +379,44 @@ class UNOgame extends Rooms.RoomGame { return player; } - onDraw(user) { - if (this.currentPlayer !== user.userid || this.state !== 'play') return false; - if (this.players[user.userid].cardLock) return true; + /** + * @param {UnoGamePlayer} player + * @return {boolean | void} + */ + onDraw(player) { + if (this.currentPlayerid !== player.userid || this.state !== 'play') return false; + if (player.cardLock) return true; this.onCheckUno(); - this.sendToRoom(`${user.name} has drawn a card.`); - let player = this.players[user.userid]; + this.sendToRoom(`${player.name} has drawn a card.`); - let card = this.onDrawCard(user, 1, true); + let card = this.onDrawCard(player, 1); player.sendDisplay(); player.cardLock = card[0].name; } - onPlay(user, cardName) { - if (this.currentPlayer !== user.userid || this.state !== 'play') return false; - let player = this.players[user.userid]; + /** + * @param {UnoGamePlayer} player + * @param {string} cardName + * @return {false | string | void} + */ + onPlay(player, cardName) { + if (this.currentPlayerid !== player.userid || this.state !== 'play') return false; let card = player.hasCard(cardName); if (!card) return "You do not have that card."; // check for legal play + if (!this.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } if (player.cardLock && player.cardLock !== cardName) return `You can only play ${player.cardLock} after drawing.`; if (card.color !== 'Black' && card.color !== (this.topCard.changedColor || this.topCard.color) && card.value !== this.topCard.value) return `You cannot play this card - you can only play: Wild cards, ${(this.topCard.changedColor ? 'and' : '')} ${(this.topCard.changedColor || this.topCard.color)} cards${this.topCard.changedColor ? "" : ` and ${this.topCard.value}'s`}.`; if (card.value === '+4' && !player.canPlayWildFour()) return "You cannot play Wild +4 when you still have a card with the same color as the top card."; - clearTimeout(this.timer); // reset the autodq timer. + if (this.timer) clearTimeout(this.timer); // reset the autodq timer. this.onCheckUno(); @@ -315,7 +427,7 @@ class UNOgame extends Rooms.RoomGame { // update the unoId here, so when the display is sent to the player when the play is made if (player.hand.length === 1) { - this.awaitUno = user.userid; + this.awaitUno = player.userid; this.unoId = Math.floor(Math.random() * 100).toString(); } @@ -334,6 +446,10 @@ class UNOgame extends Rooms.RoomGame { if (this.state === 'play') this.nextTurn(); } + /** + * @param {string} value + * @param {boolean} [initialize] + */ onRunEffect(value, initialize) { const colorDisplay = `|uhtml|uno-hand|
`; @@ -345,46 +461,55 @@ class UNOgame extends Rooms.RoomGame { break; case 'Skip': this.onNextPlayer(); - this.sendToRoom(this.players[this.currentPlayer].name + "'s turn has been skipped."); + this.sendToRoom(this.players[this.currentPlayerid].name + "'s turn has been skipped."); break; case '+2': this.onNextPlayer(); - this.sendToRoom(this.players[this.currentPlayer].name + " has been forced to draw 2 cards."); - this.onDrawCard({userid: this.currentPlayer}, 2); + this.sendToRoom(this.players[this.currentPlayerid].name + " has been forced to draw 2 cards."); + this.onDrawCard(this.players[this.currentPlayerid], 2); break; case '+4': - this.players[this.currentPlayer].sendRoom(colorDisplay); + this.players[this.currentPlayerid].sendRoom(colorDisplay); this.state = 'color'; // apply to the next in line, since the current player still has to choose the color let next = this.getNextPlayer(); this.sendToRoom(this.players[next].name + " has been forced to draw 4 cards."); - this.onDrawCard({userid: next}, 4); + this.onDrawCard(this.players[next], 4); this.isPlusFour = true; this.timer = setTimeout(() => { - this.sendToRoom(`${this.players[this.currentPlayer].name} has been automatically disqualified.`); - this.eliminate(this.currentPlayer); + this.sendToRoom(`${this.players[this.currentPlayerid].name} has been automatically disqualified.`); + this.eliminate(this.currentPlayerid); }, this.maxTime * 1000); break; case 'Wild': - this.players[this.currentPlayer].sendRoom(colorDisplay); + this.players[this.currentPlayerid].sendRoom(colorDisplay); this.state = 'color'; this.timer = setTimeout(() => { - this.sendToRoom(`${this.players[this.currentPlayer].name} has been automatically disqualified.`); - this.eliminate(this.currentPlayer); + this.sendToRoom(`${this.players[this.currentPlayerid].name} has been automatically disqualified.`); + this.eliminate(this.currentPlayerid); }, this.maxTime * 1000); break; } if (initialize) this.onNextPlayer(); } - onSelectcolor(user, color) { - if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || user.userid !== this.currentPlayer || this.state !== 'color') return false; + /** + * @param {UnoGamePlayer} player + * @param {Color} color + * @return {false | void} + */ + onSelectColor(player, color) { + if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || player.userid !== this.currentPlayerid || this.state !== 'color') return false; + if (!this.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } this.topCard.changedColor = color; this.sendToRoom(`The color has been changed to ${color}.`); - clearTimeout(this.timer); + if (this.timer) clearTimeout(this.timer); // send the display of their cards again - this.players[user.userid].sendDisplay(); + player.sendDisplay(); if (this.isPlusFour) { this.isPlusFour = false; @@ -394,64 +519,86 @@ class UNOgame extends Rooms.RoomGame { this.nextTurn(); } - onDrawCard(user, count) { - if (!(user.userid in this.players)) return false; - let drawnCards = this.drawCard(count); + /** + * @param {UnoGamePlayer} player + * @param {number} count + * @return {Card[]} + */ + onDrawCard(player, count) { + if (typeof count === 'string') count = parseInt(count); + if (!count || isNaN(count) || count < 1) count = 1; + let drawnCards = /** @type {Card[]} */ (this.drawCard(count)); - let player = this.players[user.userid]; player.hand.push(...drawnCards); player.sendRoom(`|raw|You have drawn the following card${Chat.plural(drawnCards)}: ${drawnCards.map(card => `${card.name}`).join(', ')}.`); return drawnCards; } + /** + * @param {number} count + * @return {Card[]} + */ drawCard(count) { - count = parseInt(count); - if (!count || count < 1) count = 1; - let drawnCards = []; + if (typeof count === 'string') count = parseInt(count); + if (!count || isNaN(count) || count < 1) count = 1; + let drawnCards = /** @type {Card[]} */ ([]); for (let i = 0; i < count; i++) { if (!this.deck.length) { this.deck = this.discards.length ? Dex.shuffle(this.discards) : Dex.shuffle(createDeck()); // shuffle the cards back into the deck, or if there are no discards, add another deck into the game. this.discards = []; // clear discard pile } - drawnCards.push(this.deck.pop()); + drawnCards.push(this.deck[this.deck.length - 1]); + this.deck.pop(); } return drawnCards; } - onUno(user, unoId) { + /** + * @param {UnoGamePlayer} player + * @param {string} unoId + * @return {false | void} + */ + onUno(player, unoId) { // uno id makes spamming /uno uno impossible - if (this.unoId !== unoId || user.userid !== this.awaitUno) return false; - this.sendToRoom(Chat.html`|raw|UNO! ${user.name} is down to their last card!`); - delete this.awaitUno; - delete this.unoId; + if (this.unoId !== unoId || player.userid !== this.awaitUno) return false; + this.sendToRoom(Chat.html`|raw|UNO! ${player.name} is down to their last card!`); + this.awaitUno = null; + this.unoId = null; } onCheckUno() { if (this.awaitUno) { // if the previous player hasn't hit UNO before the next player plays something, they are forced to draw 2 cards; - if (this.awaitUno !== this.currentPlayer) { + if (this.awaitUno !== this.currentPlayerid) { this.sendToRoom(`${this.players[this.awaitUno].name} forgot to say UNO! and is forced to draw 2 cards.`); - this.onDrawCard({userid: this.awaitUno}, 2); + this.onDrawCard(this.players[this.awaitUno], 2); } - delete this.awaitUno; - delete this.unoId; + this.awaitUno = null; + this.unoId = null; } } + /** + * @param {User} user + * @return {false | void} + */ onSendHand(user) { if (!(user.userid in this.players) || this.state === 'signups') return false; this.players[user.userid].sendDisplay(); } + /** + * @param {UnoGamePlayer} player + */ onWin(player) { this.sendToRoom(Chat.html`|raw|
Congratulations to ${player.name} for winning the game of UNO!
`, true); this.destroy(); } destroy() { - clearTimeout(this.timer); + if (this.timer) clearTimeout(this.timer); if (this.autostartTimer) clearTimeout(this.autostartTimer); this.sendToRoom(`|uhtmlchange|uno-${this.room.gameNumber}|
The game of UNO has ended.
`, true); @@ -463,23 +610,43 @@ class UNOgame extends Rooms.RoomGame { } } -class UNOgamePlayer extends Rooms.RoomGamePlayer { +class UnoGamePlayer extends Rooms.RoomGamePlayer { + /** + * @param {User} user + * @param {UnoGame} game + */ constructor(user, game) { super(user, game); - this.hand = []; + this.hand = /** @type {Card[]} */ ([]); + this.game = game; + /** @type {string?} */ + this.cardLock = null; } + /** + * @return {boolean} + */ canPlayWildFour() { + if (!this.game.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } let color = (this.game.topCard.changedColor || this.game.topCard.color); if (this.hand.some(c => c.color === color)) return false; return true; } + /** + * @param {string} cardName + */ hasCard(cardName) { - return this.hand.find(c => c.name === cardName); + return this.hand.find(card => card.name === cardName); } + /** + * @param {string} cardName + */ removeCard(cardName) { for (const [i, card] of this.hand.entries()) { if (card.name === cardName) { @@ -489,6 +656,9 @@ class UNOgamePlayer extends Rooms.RoomGamePlayer { } } + /** + * @return {string[]} + */ buildHand() { return this.hand.sort((a, b) => a.color.localeCompare(b.color) || a.value.localeCompare(b.value)) .map((c, i) => cardHTML(c, i === this.hand.length - 1)); @@ -501,19 +671,27 @@ class UNOgamePlayer extends Rooms.RoomGamePlayer { let pass = ''; let uno = ``; + if (!this.game.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } let top = `Top Card: ${this.game.topCard.name}`; // clear previous display and show new display this.sendRoom("|uhtmlchange|uno-hand|"); this.sendRoom( - `|uhtml|uno-hand|
${this.game.currentPlayer === this.userid ? `` : ""}` + + `|uhtml|uno-hand|
${hand}
${top}
${this.game.currentPlayerid === this.userid ? `` : ""}` + `
${hand}
${top}
${players}
` + - `${this.game.currentPlayer === this.userid ? `
${draw}${pass}
${uno}
` : ""}
` + `${this.game.currentPlayerid === this.userid ? `
${draw}${pass}
${uno}
` : ""}
` ); } } -exports.commands = { +/** @typedef {(this: CommandContext, target: string, room: ChatRoom, user: User, connection: Connection, cmd: string, message: string) => (void)} ChatHandler */ +/** @typedef {{[k: string]: { [k: string]: ChatHandler | string | true | string[] | ChatCommands} | string[]}} ChatCommands */ + +/** @type {ChatCommands} */ +const commands = { uno: { // roomowner commands off: 'disable', @@ -558,15 +736,19 @@ exports.commands = { let suppressMessages = cmd.includes('private') || !(cmd.includes('public') || room.id === 'gamecorner'); - room.game = new UNOgame(room, target, suppressMessages); + let cap = parseInt(target); + if (isNaN(cap)) cap = 6; + if (cap < 2) cap = 2; + room.game = new UnoGame(room, cap, suppressMessages); this.privateModAction(`(A game of UNO was created by ${user.name}.)`); this.modlog('UNO CREATE'); }, start: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno' || room.game.state !== 'signups') return this.errorReply("There is no UNO game in signups phase in this room."); - if (room.game.onStart()) { + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno' || game.state !== 'signups') return this.errorReply("There is no UNO game in signups phase in this room."); + if (game.onStart()) { this.privateModAction(`(The game of UNO was started by ${user.name}.)`); this.modlog('UNO START'); } @@ -584,36 +766,36 @@ exports.commands = { timer: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room."); let amount = parseInt(target); if (!amount || amount < 5 || amount > 300) return this.errorReply("The amount must be a number between 5 and 300."); - room.game.maxTime = amount; - if (room.game.timer) { - clearTimeout(room.game.timer); - room.game.timer = setTimeout(() => { - room.game.eliminate(room.game.currentPlayer); - }, amount * 1000); - } + game.maxTime = amount; + if (game.timer) clearTimeout(game.timer); + game.timer = setTimeout(() => { + game.eliminate(game.currentPlayerid); + }, amount * 1000); this.addModAction(`${user.name} has set the UNO automatic disqualification timer to ${amount} seconds.`); this.modlog('UNO TIMER', null, `${amount} seconds`); }, autostart: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (toId(target) === 'off') { - if (!room.game.autostartTimer) return this.errorReply("There is no autostart timer running on."); + if (!game.autostartTimer) return this.errorReply("There is no autostart timer running on."); this.addModAction(`${user.name} has turned off the UNO autostart timer.`); - clearTimeout(room.game.autostartTimer); + clearTimeout(game.autostartTimer); return; } const amount = parseInt(target); if (!amount || amount < 30 || amount > 600) return this.errorReply("The amount must be a number between 30 and 600 seconds."); - if (room.game.state !== 'signups') return this.errorReply("The game of UNO has already started."); - if (room.game.autostartTimer) clearTimeout(room.game.autostartTimer); - room.game.autostartTimer = setTimeout(() => { - room.game.onStart(); + if (game.state !== 'signups') return this.errorReply("The game of UNO has already started."); + if (game.autostartTimer) clearTimeout(game.autostartTimer); + game.autostartTimer = setTimeout(() => { + game.onStart(); }, amount * 1000); this.addModAction(`${user.name} has set the UNO autostart timer to ${amount} seconds.`); }, @@ -621,9 +803,10 @@ exports.commands = { dq: 'disqualify', disqualify: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let disqualified = room.game.eliminate(toId(target)); + let disqualified = game.eliminate(toId(target)); if (disqualified === false) return this.errorReply(`Unable to disqualify ${target}.`); this.privateModAction(`(${user.name} has disqualified ${disqualified} from the UNO game.)`); this.modlog('UNO DQ', toId(target)); @@ -631,68 +814,101 @@ exports.commands = { }, // player/user commands - j: 'join', - join: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + j: 'unojoin', + // TypeScript doesn't like 'join' being defined as a function + join: 'unojoin', + unojoin: function (target, room, user) { + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.canTalk()) return false; - if (!room.game.joinGame(user)) return this.errorReply("Unable to join the game."); + if (!game.joinGame(user)) return this.errorReply("Unable to join the game."); return this.sendReply("You have joined the game of UNO."); }, l: 'leave', leave: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (!room.game.leaveGame(user)) return this.errorReply("Unable to leave the game."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + if (!game.leaveGame(user)) return this.errorReply("Unable to leave the game."); return this.sendReply("You have left the game of UNO."); }, play: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let error = room.game.onPlay(user, target); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + let error = game.onPlay(player, target); if (error) this.errorReply(error); }, draw: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let error = room.game.onDraw(user); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + let error = game.onDraw(player); if (error) return this.errorReply("You have already drawn a card this turn."); }, pass: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (room.game.currentPlayer !== user.userid) return this.errorReply("It is currently not your turn."); - if (!room.game.players[user.userid].cardLock) return this.errorReply("You cannot pass until you draw a card."); - if (room.game.state === 'color') return this.errorReply("You cannot pass until you choose a color."); - - room.game.sendToRoom(`${user.name} has passed.`); - room.game.nextTurn(); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + if (game.currentPlayerid !== user.userid) return this.errorReply("It is currently not your turn."); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + if (!player.cardLock) return this.errorReply("You cannot pass until you draw a card."); + if (game.state === 'color') return this.errorReply("You cannot pass until you choose a color."); + + game.sendToRoom(`${user.name} has passed.`); + game.nextTurn(); }, color: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return false; - room.game.onSelectcolor(user, target); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return false; + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + /** @type {Color} */ + let color; + if (target === 'Red' || target === 'Green' || target === 'Blue' || target === 'Yellow' || target === 'Black') { + color = target; + } else { + return this.errorReply(`"${target}" is not a valid color.`); + } + game.onSelectColor(player, color); }, uno: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return false; - room.game.onUno(user, target); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return false; + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + game.onUno(player, target); }, // information commands '': 'hand', hand: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.parse("/help uno"); - room.game.onSendHand(user); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.parse("/help uno"); + game.onSendHand(user); }, players: 'getusers', users: 'getusers', getplayers: 'getusers', getusers: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.runBroadcast()) return false; - this.sendReplyBox(`Players (${room.game.playerCount}):
${room.game.getPlayers().join(', ')}`); + this.sendReplyBox(`Players (${game.playerCount}):
${game.getPlayers().join(', ')}`); }, help: function (target, room, user) { @@ -701,38 +917,41 @@ exports.commands = { // suppression commands suppress: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.can('minigame', null, room)) return; target = toId(target); let state = target === 'on' ? true : target === 'off' ? false : undefined; - if (state === undefined) return this.sendReply(`Suppression of UNO game messages is currently ${(room.game.suppressMessages ? 'on' : 'off')}.`); - if (state === room.game.suppressMessages) return this.errorReply(`Suppression of UNO game messages is already ${(room.game.suppressMessages ? 'on' : 'off')}.`); + if (state === undefined) return this.sendReply(`Suppression of UNO game messages is currently ${(game.suppressMessages ? 'on' : 'off')}.`); + if (state === game.suppressMessages) return this.errorReply(`Suppression of UNO game messages is already ${(game.suppressMessages ? 'on' : 'off')}.`); - room.game.suppressMessages = state; + game.suppressMessages = state; this.addModAction(`${user.name} has turned ${(state ? 'on' : 'off')} suppression of UNO game messages.`); this.modlog('UNO SUPRESS', null, (state ? 'ON' : 'OFF')); }, spectate: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (!room.game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); - if (user.userid in room.game.spectators) return this.errorReply("You are already spectating this game."); + if (!game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); + if (user.userid in game.spectators) return this.errorReply("You are already spectating this game."); - room.game.spectators[user.userid] = 1; + game.spectators[user.userid] = 1; this.sendReply("You are now spectating this private UNO game."); }, unspectate: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (!room.game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); - if (!(user.userid in room.game.spectators)) return this.errorReply("You are currently not spectating this game."); + if (!game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); + if (!(user.userid in game.spectators)) return this.errorReply("You are currently not spectating this game."); - delete room.game.spectators[user.userid]; + delete game.spectators[user.userid]; this.sendReply("You are no longer spectating this private UNO game."); }, }, @@ -750,3 +969,5 @@ exports.commands = { `/uno suppress [on|off] - Toggles suppression of game messages.`, ], }; + +exports.commands = commands; diff --git a/chat.js b/chat.js index 1449280685e0..0233d2cc7a1d 100644 --- a/chat.js +++ b/chat.js @@ -165,8 +165,17 @@ Chat.namefilter = function (name, user) { // \u534d\u5350 swastika // \u2a0d crossed integral (f) name = name.replace(/[\u00a1\u2580-\u2590\u25A0\u25Ac\u25AE\u25B0\u2a0d\u534d\u5350]/g, ''); + // e-mail address if (name.includes('@') && name.includes('.')) return ''; + + // url + if (/[a-z0-9]\.(com|net|org)/.test(name)) name = name.replace(/\./g, ''); + + // Limit the amount of symbols allowed in usernames to 4 maximum, and disallow (R) and (C) from being used in the middle of names. + let nameSymbols = name.replace(/[^\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2090-\u23FA\u2500-\u2BD1]+/g, ''); + // \u00ae\u00a9 (R) (C) + if (nameSymbols.length > 4 || /[^a-z0-9][a-z0-9][^a-z0-9]/.test(name.toLowerCase() + ' ') || /[\u00ae\u00a9].*[a-zA-Z0-9]/.test(name)) name = name.replace(/[\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2190-\u23FA\u2500-\u2BD1\u2E80-\u32FF\u3400-\u9FFF\uF900-\uFAFF\uFE00-\uFE6F]+/g, '').replace(/[^A-Za-z0-9]{2,}/g, ' ').trim(); } name = name.replace(/^[^A-Za-z0-9]+/, ""); // remove symbols from start @@ -624,20 +633,22 @@ class CommandContext { } /** * @param {string} action - * @param {string | User} user + * @param {string | User?} user * @param {string} note */ globalModlog(action, user, note) { let buf = `(${this.room.id}) ${action}: `; - if (typeof user === 'string') { - buf += `[${user}]`; - } else { - let userid = user.getLastId(); - buf += `[${userid}]`; - if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; - const alts = user.getAltUsers(false, true).map(user => user.getLastId()).join('], ['); - if (alts.length) buf += ` alts:[${alts}]`; - buf += ` [${user.latestIp}]`; + if (user) { + if (typeof user === 'string') { + buf += `[${user}]`; + } else { + let userid = user.getLastId(); + buf += `[${userid}]`; + if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; + const alts = user.getAltUsers(false, true).slice(1).map(user => user.getLastId()).join('], ['); + if (alts.length) buf += ` alts:[${alts}]`; + buf += ` [${user.latestIp}]`; + } } buf += note; @@ -660,7 +671,7 @@ class CommandContext { buf += `[${userid}]`; if (!options.noalts) { if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; - const alts = user.getAltUsers(false, true).map(user => user.getLastId()).join('], ['); + const alts = user.getAltUsers(false, true).slice(1).map(user => user.getLastId()).join('], ['); if (alts.length) buf += ` alts:[${alts}]`; } if (!options.noip) buf += ` [${user.latestIp}]`; @@ -922,7 +933,7 @@ class CommandContext { return false; } if (!this.checkBanwords(room, message) && !user.can('mute', null, room)) { - this.errorReply("Your message contained banned words."); + this.errorReply("Your message contained banned words in this room."); return false; } @@ -1587,3 +1598,11 @@ Chat.stringify = function (value, depth = 0) { Chat.formatText = require('./chat-formatter').formatText; Chat.linkRegex = require('./chat-formatter').linkRegex; Chat.updateServerLock = false; + +// Used (and populated) by ChatMonitor. +/** @type {{[k: string]: string[]}} */ +Chat.filterKeys = {}; +/** @type {{[k: string]: string[]}} */ +Chat.filterWords = {}; +/** @type {Map} */ +Chat.namefilterwhitelist = new Map(); diff --git a/config/datacenters.csv b/config/datacenters.csv index 61aaff888323..10f01595105d 100644 --- a/config/datacenters.csv +++ b/config/datacenters.csv @@ -8,7 +8,7 @@ 5.39.216.0,5.39.223.255,HostKey,http://www.hostkey.com/ 5.44.26.144,5.44.26.159,PEER 1,http://www.peer1.com/ 5.45.72.0,5.45.75.255,3NT UK,http://3nt.com/ -5.62.46.0,5.62.47.255,Privax LTD,http://www.privax.com/ +5.62.0.0,5.62.63.255,Privax LTD,http://www.privax.com/ 5.77.32.0,5.77.63.255,eukhost.com,http://eukhost.com/ 5.79.0.0,5.79.7.255,Rackspace,http://www.rackspace.com/ 5.79.16.0,5.79.24.255,Rackspace,http://www.rackspace.com/ @@ -859,7 +859,7 @@ 69.175.0.0,69.175.127.255,SingleHop,http://www.singlehop.com/ 69.176.80.0,69.176.95.255,ethr.net,http://ethr.net/ 69.192.0.0,69.192.255.255,Akamai,http://akamai.com/ -69.194.0.0,69.194.127.255,Peak Web Hosting,http://www.peakwebhosting.com/ +69.194.7.0,69.194.7.255,Peak Web Hosting,http://www.peakwebhosting.com/ 69.194.128.0,69.194.143.255,SWITCH Communications Group LLC,http://www.switchnap.com/ 69.194.160.0,69.194.175.255,Fortress Integrated Technologies,http://fortressitx.com/ 69.194.224.0,69.194.239.255,180Servers,http://www.180servers.com/ @@ -1014,6 +1014,7 @@ 77.111.244.0,77.111.247.255,Opera Mini Proxy,https://www.opera.com/ 77.120.108.128,77.120.108.191,Volia Datacenter,http://www.hosting-service.com.ua 77.222.40.0,77.222.43.255,SpaceWeb,https://sweb.ru/ +77.234.40.0,77.234.47.255,Avast Cloud,https://www.avast.com 77.235.32.0,77.235.63.255,EuroVPS,http://eurovps.com/ 77.237.228.0,77.237.231.255,servhost.de,http://servhost.de/ 77.237.241.128,77.237.241.255,webhoster.de,http://webhoster.de/ @@ -2391,7 +2392,7 @@ 192.110.160.0,192.110.167.255,Input Output Flood LLC,http://ioflood.com/ 192.111.128.0,192.111.143.255,Total Server Solutions,http://totalserversolutions.com/ 192.119.64.0,192.119.127.255,hostwinds.com,http://www.hostwinds.com/ -192.119.144.0,192.119.159.255,avantehosting.net,https://avantehosting.net/ +192.119.160.0,192.119.175.255,madgenius.com,http://www.madgenius.com/ 192.124.170.0,192.124.219.255,demos,http://demos.ru/ 192.129.128.0,192.129.255.255,hostwinds.com,http://www.hostwinds.com/ 192.138.16.0,192.138.23.255,wiredtree,http://www.wiredtree.com/ @@ -2608,7 +2609,7 @@ 198.50.96.0,198.50.127.255,iWeb Technologies Inc.,http://iweb.com/ 198.50.128.0,198.50.255.255,OVH,http://www.ovh.co.uk/ 198.52.96.0,198.52.127.255,multacom.com,http://multacom.com/ -198.52.128.0,198.52.255.255,avantehosting.net,https://avantehosting.net/ +198.52.192.0,198.52.255.255,avantehosting.net,https://avantehosting.net/ 198.55.96.0,198.55.127.255,QuadraNet,http://quadranet.com/ 198.58.96.0,198.58.127.255,Linode,http://www.linode.com/ 198.61.128.0,198.61.255.255,Rackspace,http://www.rackspace.com/ @@ -2746,7 +2747,7 @@ 199.193.248.0,199.193.255.255,Enzu Inc,https://www.enzu.com/ 199.195.116.0,199.195.119.255,A2 Hosting,http://www.a2hosting.com/ 199.195.128.0,199.195.131.255,fiberhub.com,http://www.fiberhub.com/ -199.195.156.0,199.195.159.255,avantehosting.net,https://avantehosting.net/ +199.195.156.0,199.195.159.255,softsyshosting.com,https://www.softsyshosting.com 199.195.192.0,199.195.199.255,Midphase,http://www.midphase.com/ 199.195.212.0,199.195.215.255,Forta Trust,http://www.fortatrust.com/ 199.204.44.0,199.204.47.255,vexxhost web hosting,http://vexxhost.com/ diff --git a/config/formats.js b/config/formats.js index cd8142843d53..06ca50e67b0a 100644 --- a/config/formats.js +++ b/config/formats.js @@ -60,7 +60,7 @@ let Formats = [ name: "[Gen 7] UU", threads: [ `• UU Metagame Discussion`, - `• UU Viability Rankings`, + `• UU Viability Rankings`, `• UU Sample Teams`, ], @@ -104,9 +104,6 @@ let Formats = [ mod: 'gen7', ruleset: ['[Gen 7] NU'], banlist: ['NU', 'PUBL'], - onBegin: function () { - if (this.rated) this.add('html', `
PU is currently suspecting Pyroar! For information on how to participate check out the suspect thread.
`); - }, }, { name: "[Gen 7] LC", @@ -195,19 +192,28 @@ let Formats = [ requirePentagon: true, }, { - name: "[Gen 7] Battle Spot Special 11", - threads: [`• Battle Spot Special 11`], + name: "[Gen 7] Battle Spot Special 12", + threads: [`• Battle Spot Special 12`], mod: 'gen7', - forcedLevel: 100, + forcedLevel: 50, teamLength: { validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Species Clause', 'Item Clause', 'Nickname Clause', 'Team Preview', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased', 'Mewtwo', 'Lugia', 'Ho-Oh', 'Kyogre', 'Groudon', 'Rayquaza', 'Dialga', 'Palkia', 'Giratina', - 'Arceus', 'Reshiram', 'Zekrom', 'Kyurem', 'Xerneas', 'Yveltal', 'Solgaleo', 'Lunala', 'Necrozma-Dusk-Mane', 'Necrozma-Dawn-Wings', + ruleset: ['Pokemon', 'Standard GBU'], + banlist: ['Articuno', 'Zapdos', 'Moltres', 'Raikou', 'Entei', 'Suicune', 'Regirock', 'Regice', 'Registeel', 'Latias', + 'Latios', 'Uxie', 'Mesprit', 'Azelf', 'Heatran', 'Regigigas', 'Cresselia', 'Cobalion', 'Terrakion', 'Virizion', + 'Tornadus', 'Thundurus', 'Landorus', 'Type: Null', 'Silvally', 'Tapu Koko', 'Tapu Lele', 'Tapu Bulu', 'Tapu Fini', + 'Nihilego', 'Buzzwole', 'Pheromosa', 'Xurkitree', 'Celesteela', 'Kartana', 'Guzzlord', 'Poipole', 'Naganadel', + 'Stakataka', 'Blacephalon', ], + onValidateSet: function (set, format) { + if (set.item) { + let item = this.getItem(set.item); + if (item.megaStone) return [`${set.name || set.species} has ${item.name}, which is banned in ${format.name}.`]; + } + }, }, { name: "[Gen 7] Custom Game", @@ -334,6 +340,10 @@ let Formats = [ }, { name: "[Gen 7] VGC 2019 Ultra Series", + threads: [ + `• VGC 2019 Discussion`, + `• VGC 2019 Viability Rankings`, + ], mod: 'gen7', gameType: 'doubles', @@ -358,6 +368,11 @@ let Formats = [ }, { name: "[Gen 7] VGC 2018", + threads: [ + `• VGC 2018 Discussion`, + `• VGC 2018 Viability Rankings`, + `• VGC 2018 Sample Teams`, + ], mod: 'gen7', gameType: 'doubles', @@ -478,79 +493,135 @@ let Formats = [ column: 2, }, { - name: "[Gen 7] Tier Shift", - desc: `Pokémon get +10 to each stat per tier below OU they are in. UU gets +10, RU +20, NU +30, and PU +40.`, + name: "[Gen 7] Sketchmons", + desc: `Pokémon can learn one of any move they don't normally learn, barring the few that are banned.`, threads: [ - `• Tier Shift`, + `• Sketchmons`, ], mod: 'gen7', ruleset: ['[Gen 7] OU'], - banlist: ['Damp Rock', 'Deep Sea Tooth', 'Eviolite'], - onModifyTemplate: function (template, pokemon) { - let tsTemplate = Object.assign({}, template); - const boosts = {'UU': 10, 'RUBL': 10, 'RU': 20, 'NUBL': 20, 'NU': 30, 'PUBL': 30, 'PU': 40, 'NFE': 40, 'LC Uber': 40, 'LC': 40}; - let tier = tsTemplate.tier; - if (pokemon && pokemon.set.item) { - let item = this.getItem(pokemon.set.item); - if (item.megaEvolves === tsTemplate.species) tier = this.getTemplate(item.megaStone).tier; - } - if (tier.charAt(0) === '(') tier = tier.slice(1, -1); - let boost = (tier in boosts) ? boosts[tier] : 0; - if (boost > 0 && pokemon && (pokemon.set.ability === 'Drizzle' || pokemon.set.item === 'Mewnium Z')) boost = 0; - if (boost > 10 && pokemon && pokemon.set.moves.includes('auroraveil')) boost = 10; - if (boost > 20 && pokemon && pokemon.set.ability === 'Drought') boost = 20; - tsTemplate.baseStats = Object.assign({}, tsTemplate.baseStats); - // `Dex` needs to be used in /data, `this` needs to be used in battles - const clampRange = this && this.clampIntRange ? this.clampIntRange : Dex.clampIntRange; - for (let statName in tsTemplate.baseStats) { + banlist: ['Kartana', 'Porygon-Z', 'Battle Bond'], + restrictedMoves: [ + 'Belly Drum', 'Celebrate', 'Chatter', 'Conversion', 'Extreme Speed', "Forest's Curse", 'Geomancy', 'Happy Hour', 'Hold Hands', + 'Lovely Kiss', 'Purify', 'Quiver Dance', 'Shell Smash', 'Shift Gear', 'Sketch', 'Spore', 'Sticky Web', 'Trick-or-Treat', + ], + checkLearnset: function (move, template, lsetData, set) { + let problem = this.checkLearnset(move, template, lsetData, set); + if (!problem) return null; + const restrictedMoves = this.format.restrictedMoves || []; + if (move.isZ || restrictedMoves.includes(move.name)) return problem; + // @ts-ignore + if (set.sketchMove) return {type: 'oversketched', maxSketches: 1}; + // @ts-ignore + set.sketchMove = move.id; + return null; + }, + onValidateTeam: function (team, format, teamHas) { + let sketches = {}; + for (const set of team) { // @ts-ignore - tsTemplate.baseStats[statName] = clampRange(tsTemplate.baseStats[statName] + boost, 1, 255); + if (set.sketchMove) { + // @ts-ignore + if (!sketches[set.sketchMove]) { + // @ts-ignore + sketches[set.sketchMove] = 1; + } else { + // @ts-ignore + sketches[set.sketchMove]++; + } + } } - return tsTemplate; + let overSketched = Object.keys(sketches).filter(move => sketches[move] > 1); + if (overSketched.length) return overSketched.map(move => `You are limited to 1 of ${this.getMove(move).name} by Sketch Clause. (You have sketched ${this.getMove(move).name} ${sketches[move]} times.)`); }, }, { - name: "[Gen 7] Benjamin Butterfree", - desc: `Pokémon that faint reincarnate as their prevo, but without the moves they can't learn.`, + name: "[Gen 7] Partners in Crime", + desc: `Doubles-based metagame where both active ally Pokémon share abilities and moves.`, threads: [ - `• Benjamin Butterfree`, + `• Partners in Crime`, ], - mod: 'gen7', - ruleset: ['[Gen 7] OU'], - banlist: ['Blissey'], - onBeforeFaint: function (pokemon) { - let prevo = pokemon.baseTemplate.isMega ? this.getTemplate(pokemon.baseTemplate.baseSpecies).prevo : pokemon.baseTemplate.prevo; - if (!prevo || pokemon.set.ability === 'Battle Bond') return; - let template = this.getTemplate(pokemon.set.species); + mod: 'pic', + gameType: 'doubles', + ruleset: ['[Gen 7] Doubles OU', 'Sleep Clause Mod'], + banlist: ['Kangaskhanite', 'Mawilite', 'Medichamite', 'Huge Power', 'Imposter', 'Normalize', 'Pure Power', 'Wonder Guard', 'Mimic', 'Sketch', 'Transform'], + onSwitchInPriority: 2, + onSwitchIn: function (pokemon) { + if (this.p1.active.every(ally => ally && !ally.fainted)) { + let p1a = this.p1.active[0], p1b = this.p1.active[1]; + if (p1a.ability !== p1b.ability) { + let p1aInnate = 'ability' + p1b.ability; + p1a.volatiles[p1aInnate] = {id: p1aInnate, target: p1a}; + let p1bInnate = 'ability' + p1a.ability; + p1b.volatiles[p1bInnate] = {id: p1bInnate, target: p1b}; + } + } + if (this.p2.active.every(ally => ally && !ally.fainted)) { + let p2a = this.p2.active[0], p2b = this.p2.active[1]; + if (p2a.ability !== p2b.ability) { + let p2a_innate = 'ability' + p2b.ability; + p2a.volatiles[p2a_innate] = {id: p2a_innate, target: p2a}; + let p2b_innate = 'ability' + p2a.ability; + p2b.volatiles[p2b_innate] = {id: p2b_innate, target: p2b}; + } + } + let ally = pokemon.side.active.find(ally => ally && ally !== pokemon && !ally.fainted); + if (ally && ally.ability !== pokemon.ability) { + // @ts-ignore + if (!pokemon.innate) { + // @ts-ignore + pokemon.innate = 'ability' + ally.ability; + // @ts-ignore + delete pokemon.volatiles[pokemon.innate]; + // @ts-ignore + pokemon.addVolatile(pokemon.innate); + } + // @ts-ignore + if (!ally.innate) { + // @ts-ignore + ally.innate = 'ability' + pokemon.ability; + // @ts-ignore + delete ally.volatiles[ally.innate]; + // @ts-ignore + ally.addVolatile(ally.innate); + } + } + }, + onSwitchOut: function (pokemon) { // @ts-ignore - let abilitySlot = Object.keys(template.abilities).find(slot => template.abilities[slot] === pokemon.set.ability); - template = this.getTemplate(prevo); + if (pokemon.innate) { + // @ts-ignore + pokemon.removeVolatile(pokemon.innate); + // @ts-ignore + delete pokemon.innate; + } + let ally = pokemon.side.active.find(ally => ally && ally !== pokemon && !ally.fainted); + // @ts-ignore + if (ally && ally.innate) { + // @ts-ignore + ally.removeVolatile(ally.innate); + // @ts-ignore + delete ally.innate; + } + }, + onFaint: function (pokemon) { // @ts-ignore - if (!template.abilities[abilitySlot]) abilitySlot = '0'; - pokemon.faintQueued = false; - pokemon.hp = pokemon.maxhp; - if (Object.values(pokemon.boosts).find(boost => boost !== 0)) { - pokemon.clearBoosts(); - this.add('-clearboost', pokemon); + if (pokemon.innate) { + // @ts-ignore + pokemon.removeVolatile(pokemon.innate); + // @ts-ignore + delete pokemon.innate; } - pokemon.formeChange(template, this.getFormat(), true, '', abilitySlot); - this.add('-message', `${pokemon.name} has devolved into ${template.species}!`); - pokemon.cureStatus(true); - let newHP = Math.floor(Math.floor(2 * pokemon.template.baseStats['hp'] + pokemon.set.ivs['hp'] + Math.floor(pokemon.set.evs['hp'] / 4) + 100) * pokemon.level / 100 + 10); - pokemon.maxhp = pokemon.hp = newHP; - this.add('-heal', pokemon, pokemon.getHealth, '[silent]'); - let learnset = template.learnset || this.getTemplate(template.baseSpecies).learnset || {}; - let prevoset = template.prevo && this.getTemplate(template.prevo).learnset || {}; - for (const moveSlot of pokemon.baseMoveSlots) { - if (!learnset[moveSlot.id] && !prevoset[moveSlot.id]) { - moveSlot.used = true; - moveSlot.pp = 0; - } + let ally = pokemon.side.active.find(ally => ally && ally !== pokemon && !ally.fainted); + // @ts-ignore + if (ally && ally.innate) { + // @ts-ignore + ally.removeVolatile(ally.innate); + // @ts-ignore + delete ally.innate; } - pokemon.canMegaEvo = null; - return false; }, }, { @@ -583,12 +654,12 @@ let Formats = [ validate: [1, 3], battle: 1, }, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], banlist: [ - 'Illegal', 'Unreleased', 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', 'Ho-Oh', - 'Jirachi', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', 'Necrozma-Dawn-Wings', - 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', 'Solgaleo', 'Tapu Koko', 'Xerneas', 'Yveltal', 'Zekrom', - 'Focus Sash', 'Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Perish Song', 'Sand Attack', 'Smokescreen', + 'Illegal', 'Unreleased', 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', + 'Ho-Oh', 'Jirachi', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', + 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', 'Snorlax', + 'Solgaleo', 'Tapu Koko', 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Perish Song', ], }, { @@ -660,7 +731,7 @@ let Formats = [ mod: 'gen7', ruleset: ['[Gen 7] OU', 'Ability Clause', 'Ignore Illegal Abilities'], - banlist: ['Archeops', 'Dragonite', 'Hoopa-Unbound', 'Kartana', 'Keldeo', 'Kyurem-Black', 'Regigigas', 'Shedinja', 'Slaking', 'Terrakion'], + banlist: ['Archeops', 'Dragonite', 'Hoopa-Unbound', 'Kartana', 'Keldeo', 'Kyurem-Black', 'Regigigas', 'Shedinja', 'Slaking', 'Terrakion', 'Zygarde-Base'], unbanlist: ['Aegislash', 'Genesect', 'Landorus', 'Metagross-Mega', 'Naganadel'], restrictedAbilities: [ 'Comatose', 'Contrary', 'Fluffy', 'Fur Coat', 'Huge Power', 'Illusion', 'Imposter', 'Innards Out', @@ -678,9 +749,6 @@ let Formats = [ if (!legalAbility) return ['The ability ' + set.ability + ' is banned on Pok\u00e9mon that do not naturally have it.']; } }, - onBegin: function () { - if (this.rated) this.add('html', `
Almost Any Ability is currently suspecting Zygarde! For information on how to participate check out the suspect thread.
`); - }, }, { name: "[Gen 7] STABmons", @@ -690,7 +758,7 @@ let Formats = [ ], mod: 'gen7', - // searchShow: false, + searchShow: false, ruleset: ['[Gen 7] OU', 'STABmons Move Legality'], banlist: ['Aerodactyl-Mega', 'Blacephalon', 'Kartana', 'Komala', 'Kyurem-Black', 'Porygon-Z', 'Silvally', 'Tapu Koko', 'Tapu Lele', 'King\'s Rock', 'Razor Fang'], restrictedMoves: ['Acupressure', 'Belly Drum', 'Chatter', 'Extreme Speed', 'Geomancy', 'Lovely Kiss', 'Shell Smash', 'Shift Gear', 'Spore', 'Thousand Arrows'], @@ -703,17 +771,16 @@ let Formats = [ ], mod: 'gen7', - searchShow: false, + // searchShow: false, ruleset: ['[Gen 7] PU'], banlist: [ // PU - 'Absol', 'Aggron', 'Altaria', 'Archeops', 'Aromatisse', 'Articuno', 'Audino', 'Aurorus', 'Claydol', - 'Clefairy', 'Drampa', 'Eelektross', 'Exeggutor-Alola', 'Floatzel', 'Froslass', 'Golurk', 'Gourgeist-Super', - 'Granbull', 'Gurdurr', 'Haunter', 'Hitmonchan', 'Kangaskhan', 'Kingler', 'Komala', 'Lanturn', 'Liepard', - 'Lilligant', 'Lycanroc-Base', 'Manectric', 'Mesprit', 'Mudsdale', 'Omastar', 'Oricorio-Sensu', 'Passimian', - 'Persian-Alola', 'Poliwrath', 'Primeape', 'Probopass', 'Pyroar', 'Pyukumuku', 'Quagsire', 'Qwilfish', - 'Raichu-Alola', 'Regirock', 'Sableye', 'Sandslash-Alola', 'Scyther', 'Silvally-Fairy', 'Silvally-Ghost', - 'Skuntank', 'Spiritomb', 'Stoutland', 'Swanna', 'Togedemaru', 'Weezing', 'Zangoose', + 'Absol', 'Aggron', 'Altaria', 'Archeops', 'Aromatisse', 'Articuno', 'Audino', 'Aurorus', 'Claydol', 'Clefairy', + 'Drampa', 'Eelektross', 'Exeggutor-Alola', 'Floatzel', 'Froslass', 'Golurk', 'Gourgeist-Super', 'Gurdurr', + 'Haunter', 'Hitmonchan', 'Kangaskhan', 'Kingler', 'Komala', 'Lanturn', 'Liepard', 'Lilligant', 'Lycanroc-Base', + 'Manectric', 'Mesprit', 'Mudsdale', 'Omastar', 'Oricorio-Sensu', 'Passimian', 'Persian-Alola', 'Poliwrath', + 'Primeape', 'Pyukumuku', 'Quagsire', 'Qwilfish', 'Raichu-Alola', 'Regirock', 'Sableye', 'Sandslash-Alola', + 'Scyther', 'Silvally-Fairy', 'Silvally-Ghost', 'Skuntank', 'Spiritomb', 'Swanna', 'Togedemaru', 'Weezing', 'Zangoose', // ZUBL 'Carracosta', 'Crabominable', 'Exeggutor-Base', 'Gorebyss', 'Jynx', 'Kabutops', 'Ludicolo', 'Musharna', 'Raticate-Alola', 'Raticate-Alola-Totem', 'Throh', 'Turtonator', 'Type: Null', 'Ursaring', 'Victreebel', @@ -755,11 +822,12 @@ let Formats = [ validate: [2, 4], battle: 2, }, - ruleset: ['[Gen 7] Doubles OU'], - banlist: [ - 'Salamence-Mega', 'Tapu Lele', 'Focus Sash', 'Final Gambit', 'Perish Song', - 'Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Sand Attack', 'Smokescreen', - ], + ruleset: ['[Gen 7] Doubles OU', 'Accuracy Moves Clause', 'Sleep Clause Mod'], + banlist: ['Salamence-Mega', 'Tapu Lele', 'Focus Sash', 'Final Gambit', 'Perish Song'], + onValidateSet: function (set) { + const item = this.getItem(set.item); + if (item.zMove) return [(set.name || set.species) + "'s item " + item.name + " is banned."]; + }, }, { name: "[Gen 6] Gen-NEXT OU", @@ -916,91 +984,61 @@ let Formats = [ column: 3, }, { - name: "[Gen 3] Ubers", + name: "[Gen 4] Ubers", threads: [ - `• ADV Ubers Information & Resources`, - `• ADV Ubers Viability Ranking`, + `• DPP Ubers Information & Resources`, + `• DPP Ubers Viability Ranking`, ], - mod: 'gen3', + mod: 'gen4', // searchShow: false, ruleset: ['Pokemon', 'Standard'], - banlist: ['Smeargle + Ingrain', 'Wobbuffet + Leftovers'], + banlist: ['Arceus'], }, { - name: "[Gen 6] Mix and Mega", - desc: `Mega Stones and Primal Orbs can be used on almost any fully evolved Pokémon with no Mega Evolution limit.`, - threads: [`• ORAS Mix and Mega`], + name: "[Gen 6] 1v1", + desc: `Bring three Pokémon to Team Preview and choose one to battle.`, - mod: 'mixandmega6', - ruleset: ['Pokemon', 'Standard', 'Mega Rayquaza Clause', 'Team Preview'], - banlist: ['Baton Pass', 'Dynamic Punch', 'Electrify', 'Zap Cannon'], - restrictedStones: ['Beedrillite', 'Gengarite', 'Kangaskhanite', 'Mawilite', 'Medichamite'], - cannotMega: [ - 'Arceus', 'Cresselia', 'Darkrai', 'Deoxys', 'Deoxys-Attack', 'Dialga', 'Dragonite', 'Genesect', 'Giratina', - 'Groudon', 'Ho-Oh', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lucario', 'Lugia', 'Manaphy', 'Mewtwo', - 'Palkia', 'Rayquaza', 'Regigigas', 'Reshiram', 'Shaymin-Sky', 'Slaking', 'Xerneas', 'Yveltal', 'Zekrom', - ], - onValidateTeam: function (team) { - let itemTable = {}; - for (const set of team) { - let item = this.getItem(set.item); - if (!item) continue; - if (itemTable[item.id] && item.megaStone) return ["You are limited to one of each Mega Stone.", "(You have more than one " + this.getItem(item).name + ")"]; - if (itemTable[item.id] && (item.id === 'blueorb' || item.id === 'redorb')) return ["You are limited to one of each Primal Orb.", "(You have more than one " + this.getItem(item).name + ")"]; - itemTable[item.id] = true; - } - }, - onValidateSet: function (set, format) { - let template = this.getTemplate(set.species || set.name); - let item = this.getItem(set.item); - if (!item.megaEvolves && item.id !== 'blueorb' && item.id !== 'redorb') return; - if (template.baseSpecies === item.megaEvolves || (template.baseSpecies === 'Groudon' && item.id === 'redorb') || (template.baseSpecies === 'Kyogre' && item.id === 'blueorb')) return; - if (template.evos.length) return ["" + template.species + " is not allowed to hold " + item.name + " because it's not fully evolved."]; - let uberStones = format.restrictedStones || []; - let uberPokemon = format.cannotMega || []; - if (uberPokemon.includes(template.name) || uberStones.includes(item.name)) return ["" + template.species + " is not allowed to hold " + item.name + "."]; - }, - onBegin: function () { - for (const pokemon of this.p1.pokemon.concat(this.p2.pokemon)) { - pokemon.originalSpecies = pokemon.baseTemplate.species; - } - }, - onSwitchIn: function (pokemon) { - let oMegaTemplate = this.getTemplate(pokemon.template.originalMega); - if (oMegaTemplate.exists && pokemon.originalSpecies !== oMegaTemplate.baseSpecies) { - // Place volatiles on the Pokémon to show its mega-evolved condition and details - this.add('-start', pokemon, oMegaTemplate.requiredItem || oMegaTemplate.requiredMove, '[silent]'); - let oTemplate = this.getTemplate(pokemon.originalSpecies); - if (oTemplate.types.length !== pokemon.template.types.length || oTemplate.types[1] !== pokemon.template.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.template.types.join('/'), '[silent]'); - } - } - }, - onSwitchOut: function (pokemon) { - let oMegaTemplate = this.getTemplate(pokemon.template.originalMega); - if (oMegaTemplate.exists && pokemon.originalSpecies !== oMegaTemplate.baseSpecies) { - this.add('-end', pokemon, oMegaTemplate.requiredItem || oMegaTemplate.requiredMove, '[silent]'); - } + mod: 'gen6', + teamLength: { + validate: [1, 3], + battle: 1, }, + ruleset: ['Pokemon', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + banlist: [ + 'Illegal', 'Unreleased', 'Arceus', 'Blaziken', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Dialga', 'Giratina', + 'Groudon', 'Ho-Oh', 'Kyogre', 'Kyurem-White', 'Lugia', 'Mewtwo', 'Palkia', 'Rayquaza', 'Reshiram', 'Shaymin-Sky', + 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Kangaskhanite', 'Salamencite', 'Soul Dew', 'Perish Song', + ], }, { - name: "[Gen 2] Mediocremons", + name: "[Gen 3] Tier Shift", + desc: `Pokémon get +5 to each stat per tier below OU, including UUBL, they are in. UUBL gets +5, UU +10, NU +15, and PU +20.`, - mod: 'gen2', - ruleset: ['[Gen 2] OU'], - banlist: [], - onValidateSet: function (set) { - let problems = []; - let template = this.getTemplate(set.species); - let item = this.getItem(set.item); - if (item.megaEvolves === template.species) template = this.getTemplate(item.megaStone); - let statTable = {hp: 'an HP', atk: 'an Attack', def: 'a Defense', spa: 'a Special Attack', spd: 'a Special Defense', spe: 'a Speed'}; - for (let stat in statTable) { + mod: 'gen3', + ruleset: ['[Gen 3] OU'], + onModifyTemplate: function (template, pokemon) { + let tsTemplate = Object.assign({}, template); + let puPokemon = [ + 'Aipom', 'Anorith', 'Ariados', 'Beautifly', 'Beedrill', 'Butterfree', 'Castform', 'Charmeleon', 'Clamperl', 'Combusken', + 'Corsola', 'Croconaw', 'Delcatty', 'Delibird', 'Ditto', 'Doduo', 'Dragonair', 'Drowzee', 'Duskull', 'Dustox', 'Farfetch\'d', + 'Furret', 'Gastly', 'Grovyle', 'Houndour', 'Illumise', 'Ivysaur', 'Lairon', 'Ledian', 'Lombre', 'Luvdisc', 'Machoke', + 'Marshtomp', 'Masquerain', 'Mightyena', 'Minun', 'Noctowl', 'Nosepass', 'Omanyte', 'Parasect', 'Poliwhirl', 'Ponyta', + 'Porygon', 'Quilava', 'Seaking', 'Sealeo', 'Seviper', 'Shedinja', 'Shuckle', 'Smoochum', 'Spinda', 'Sunflora', 'Tentacool', + 'Trapinch', 'Unown', 'Vibrava', 'Wartortle', 'Weepinbell', 'Wigglytuff', 'Yanma', + ]; + if (puPokemon.includes(tsTemplate.species)) tsTemplate.tier = 'PU'; + const boosts = {'UUBL': 5, 'UU': 10, 'NU': 15, 'PU': 20, 'NFE': 20, 'LC': 20}; + let tier = tsTemplate.tier; + let boost = (tier in boosts) ? boosts[tier] : 0; + tsTemplate.baseStats = Object.assign({}, tsTemplate.baseStats); + // `Dex` needs to be used in /data, `this` needs to be used in battles + const clampRange = this && this.clampIntRange ? this.clampIntRange : Dex.clampIntRange; + for (let statName in tsTemplate.baseStats) { // @ts-ignore - if (template.baseStats[stat] >= 100) problems.push(template.species + " has " + statTable[stat] + " of " + template.baseStats[stat] + ", which is banned."); + tsTemplate.baseStats[statName] = clampRange(tsTemplate.baseStats[statName] + boost, 1, 255); } - return problems; + return tsTemplate; }, }, @@ -1031,7 +1069,7 @@ let Formats = [ mod: 'gen5', ruleset: ['Pokemon', 'Standard', 'Evasion Abilities Clause', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], - banlist: ['Uber', 'Drizzle ++ Swift Swim', 'Drought ++ Chlorophyll', 'Sand Stream ++ Sand Rush', 'Soul Dew'], + banlist: ['Uber', 'Arena Trap', 'Drizzle ++ Swift Swim', 'Drought ++ Chlorophyll', 'Sand Stream ++ Sand Rush', 'Soul Dew'], }, { name: "[Gen 4] OU", @@ -1488,18 +1526,6 @@ let Formats = [ section: "DPP Singles", column: 4, }, - { - name: "[Gen 4] Ubers", - threads: [ - `• DPP Ubers Information & Resources`, - `• DPP Ubers Viability Ranking`, - ], - - mod: 'gen4', - searchShow: false, - ruleset: ['Pokemon', 'Standard'], - banlist: ['Arceus'], - }, { name: "[Gen 4] UU", threads: [ @@ -1589,6 +1615,18 @@ let Formats = [ section: "Past Generations", column: 4, }, + { + name: "[Gen 3] Ubers", + threads: [ + `• ADV Ubers Information & Resources`, + `• ADV Ubers Viability Ranking`, + ], + + mod: 'gen3', + searchShow: false, + ruleset: ['Pokemon', 'Standard'], + banlist: ['Smeargle + Ingrain', 'Wobbuffet + Leftovers'], + }, { name: "[Gen 3] UU", threads: [ diff --git a/data/abilities.js b/data/abilities.js index 59964033bdaf..315b7817af10 100644 --- a/data/abilities.js +++ b/data/abilities.js @@ -872,7 +872,7 @@ let BattleAbilities = { }, id: "emergencyexit", name: "Emergency Exit", - rating: 2, + rating: 1.5, num: 194, }, "fairyaura": { @@ -3173,6 +3173,7 @@ let BattleAbilities = { "soulheart": { desc: "This Pokemon's Special Attack is raised by 1 stage when another Pokemon faints.", shortDesc: "This Pokemon's Sp. Atk is raised by 1 stage when another Pokemon faints.", + onAnyFaintPriority: 1, onAnyFaint: function () { this.boost({spa: 1}, this.effectData.target); }, diff --git a/data/factory-sets.json b/data/factory-sets.json index 260644edef5b..6b27e9af48b9 100644 --- a/data/factory-sets.json +++ b/data/factory-sets.json @@ -8,7 +8,7 @@ "ability": ["Drought"], "evs": {"hp": 252, "def": 56, "spd": 200}, "nature": "Relaxed", - "moves": [["Precipice Blades"], ["Toxic", "Roar"], ["Overheat"], ["Stealth Rock"]] + "moves": [["Precipice Blades"], ["Toxic"], ["Overheat"], ["Stealth Rock"]] }, { "species": "Groudon", "item": ["Red Orb"], @@ -34,16 +34,16 @@ "species": "Groudon", "item": ["Red Orb"], "ability": ["Drought"], - "evs": {"atk": 168, "spd": 144, "spe": 196}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Precipice Blades"], ["Stone Edge"], ["Stealth Rock"]] + "evs": {"spa": 212, "spd": 144, "spe": 152}, + "nature": "Mild", + "moves": [["Swords Dance"], ["Precipice Blades"], ["Rock Tomb"], ["Overheat"]] }, { "species": "Groudon", - "item": ["Choice Band"], + "item": ["Red Orb"], "ability": ["Drought"], - "evs": {"hp": 152, "atk": 252, "spd": 44, "spe": 60}, + "evs": {"hp": 144, "atk": 156, "spd": 56, "spe": 152}, "nature": "Adamant", - "moves": [["Precipice Blades"], ["Stone Edge"], ["Dragon Claw"], ["Fire Punch"]] + "moves": [["Precipice Blades"], ["Swords Dance"], ["Stealth Rock"], ["Rock Tomb"]] }] }, "xerneas": { @@ -59,16 +59,9 @@ "species": "Xerneas", "item": ["Power Herb"], "ability": ["Fairy Aura"], - "evs": {"hp": 72, "spa": 252, "spd": 100, "spe": 84}, + "evs": {"def": 168, "spa": 252, "spe": 88}, "nature": "Modest", "moves": [["Geomancy"], ["Moonblast"], ["Psyshock"], ["Hidden Power Fire"]] - }, { - "species": "Xerneas", - "item": ["Leftovers"], - "ability": ["Fairy Aura"], - "evs": {"hp": 252, "def": 252, "spa": 4}, - "nature": "Bold", - "moves": [["Moonblast"], ["Aromatherapy"], ["Rest"], ["Sleep Talk"]] }, { "species": "Xerneas", "item": ["Fairium Z"], @@ -83,27 +76,6 @@ "evs": {"spa": 252, "spd": 4, "spe": 252}, "nature": "Modest", "moves": [["Moonblast"], ["Focus Blast"], ["Aromatherapy"], ["Thunder"]] - }, { - "species": "Xerneas", - "item": ["Choice Scarf"], - "ability": ["Fairy Aura"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Modest", - "moves": [["Moonblast"], ["Focus Blast"], ["Aromatherapy"], ["Defog"]] - }, { - "species": "Xerneas", - "item": ["Choice Scarf"], - "ability": ["Fairy Aura"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Modest", - "moves": [["Moonblast"], ["Hidden Power Fire"], ["Aromatherapy"], ["Thunder"]] - }, { - "species": "Xerneas", - "item": ["Choice Scarf"], - "ability": ["Fairy Aura"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Modest", - "moves": [["Moonblast"], ["Hidden Power Fire"], ["Aromatherapy"], ["Defog"]] }, { "species": "Xerneas", "item": ["Choice Specs"], @@ -112,13 +84,6 @@ "ivs": {"atk": 0}, "nature": "Timid", "moves": [["Moonblast"], ["Grass Knot"], ["Thunder"], ["Hidden Power Fire"]] - }, { - "species": "Xerneas", - "item": ["Expert Belt"], - "ability": ["Fairy Aura"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Hasty", - "moves": [["Moonblast"], ["Close Combat"], ["Rock Slide"], ["Hidden Power Fire"]] }] }, "arceusground": { @@ -129,14 +94,7 @@ "ability": ["Multitype"], "evs": {"hp": 252, "def": 64, "spe": 192}, "nature": "Timid", - "moves": [["Judgment"], ["Ice Beam"], ["Defog"], ["Recover"]] - }, { - "species": "Arceus-Ground", - "item": ["Earth Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, - "nature": "Timid", - "moves": [["Judgment"], ["Ice Beam"], ["Stealth Rock"], ["Recover"]] + "moves": [["Judgment"], ["Ice Beam"], ["Defog", "Stealth Rock"], ["Recover"]] }, { "species": "Arceus-Ground", "item": ["Groundium Z"], @@ -151,13 +109,6 @@ "evs": {"hp": 252, "spa": 4, "spe": 252}, "nature": "Timid", "moves": [["Judgment"], ["Ice Beam"], ["Calm Mind"], ["Recover"]] - }, { - "species": "Arceus-Ground", - "item": ["Earth Plate"], - "ability": ["Multitype"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Earthquake"], ["Stone Edge"], ["Swords Dance"], ["Recover", "Substitute"]] }] }, "gengar": { @@ -168,37 +119,16 @@ "species": "Gengar", "item": ["Gengarite"], "ability": ["Cursed Body"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Will-O-Wisp"], ["Taunt"], ["Hex"], ["Sludge Wave", "Sludge Bomb"]] - }, { - "species": "Gengar", - "item": ["Gengarite"], - "ability": ["Cursed Body"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Taunt"], ["Destiny Bond"], ["Focus Blast", "Sludge Wave"], ["Shadow Ball"]] - }, { - "species": "Gengar", - "item": ["Gengarite"], - "ability": ["Cursed Body"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Perish Song"], ["Protect"], ["Disable"], ["Sludge Wave"]] - }, { - "species": "Gengar", - "item": ["Gengarite"], - "ability": ["Cursed Body"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, + "evs": {"spa": 252, "spd": 4, "spe": 252}, "nature": "Timid", - "moves": [["Perish Song"], ["Protect"], ["Substitute"], ["Sludge Wave"]] + "moves": [["Will-O-Wisp"], ["Taunt", "Substitute"], ["Hex"], ["Sludge Wave"]] }, { "species": "Gengar", "item": ["Gengarite"], "ability": ["Cursed Body"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, + "evs": {"spa": 252, "spd": 4, "spe": 252}, "nature": "Timid", - "moves": [["Perish Song"], ["Substitute"], ["Disable"], ["Sludge Wave"]] + "moves": [["Shadow Ball"], ["Sludge Wave"], ["Icy Wind", "Thunder"], ["Taunt", "Substitute"]] }] }, "salamence": { @@ -216,23 +146,9 @@ "species": "Salamence", "item": ["Salamencite"], "ability": ["Intimidate"], - "evs": {"hp": 184, "atk": 136, "spe": 188}, + "evs": {"hp": 44, "atk": 252, "spe": 212}, "nature": "Adamant", "moves": [["Dragon Dance"], ["Double-Edge"], ["Roost"], ["Facade", "Earthquake"]] - }, { - "species": "Salamence", - "item": ["Salamencite"], - "ability": ["Intimidate"], - "evs": {"hp": 248, "def": 244, "spe": 16}, - "nature": "Impish", - "moves": [["Body Slam"], ["Wish"], ["Roost"], ["Dragon Tail"]] - }, { - "species": "Salamence", - "item": ["Salamencite"], - "ability": ["Intimidate"], - "evs": {"hp": 248, "atk": 68, "def": 124, "spe": 68}, - "nature": "Impish", - "moves": [["Return"], ["Substitute"], ["Dragon Dance"], ["Roost"]] }] }, "marshadow": { @@ -250,14 +166,7 @@ "ability": ["Technician"], "evs": {"atk": 252, "spa": 4, "spe": 252}, "nature": "Naive", - "moves": [["Spectral Thief"], ["Close Combat"], ["Rock Tomb"], ["Shadow Sneak"]] - }, { - "species": "Marshadow", - "item": ["Life Orb"], - "ability": ["Technician"], - "evs": {"atk": 252, "spa": 4, "spe": 252}, - "nature": "Naive", - "moves": [["Spectral Thief"], ["Close Combat"], ["Hidden Power Ice"], ["Shadow Sneak"]] + "moves": [["Spectral Thief"], ["Close Combat"], ["Rock Tomb", "Hidden Power Ice"], ["Shadow Sneak"]] }] }, "hooh": { @@ -268,21 +177,14 @@ "ability": ["Regenerator"], "evs": {"hp": 252, "def": 204, "spd": 52}, "nature": "Impish", - "moves": [["Sacred Fire"], ["Brave Bird"], ["Toxic"], ["Recover"]] - }, { - "species": "Ho-Oh", - "item": ["Leftovers"], - "ability": ["Regenerator"], - "evs": {"hp": 252, "def": 204, "spd": 52}, - "nature": "Impish", - "moves": [["Sacred Fire"], ["Defog"], ["Toxic"], ["Recover"]] + "moves": [["Sacred Fire"], ["Brave Bird", "Defog"], ["Toxic"], ["Recover"]] }, { "species": "Ho-Oh", "item": ["Life Orb"], "ability": ["Regenerator"], - "evs": {"hp": 224, "atk": 136, "spd": 72, "spe": 76}, + "evs": {"hp": 104, "atk": 252, "spe": 152}, "nature": "Adamant", - "moves": [["Brave Bird"], ["Sacred Fire"], ["Recover"], ["Toxic", "Earthquake"]] + "moves": [["Brave Bird"], ["Sacred Fire"], ["Recover"], ["Toxic"]] }, { "species": "Ho-Oh", "item": ["Choice Band"], @@ -300,7 +202,7 @@ "ability": ["Dark Aura"], "evs": {"atk": 4, "spa": 252, "spe": 252}, "nature": "Rash", - "moves": [["Oblivion Wing"], ["Dark Pulse"], ["Sucker Punch", "Knock Off"], ["Taunt", "Heat Wave"]] + "moves": [["Oblivion Wing"], ["Dark Pulse"], ["Sucker Punch", "Toxic"], ["Taunt"]] }, { "species": "Yveltal", "item": ["Choice Scarf"], @@ -341,21 +243,7 @@ "ability": ["Justified"], "evs": {"atk": 252, "spd": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Close Combat"], ["Meteor Mash"], ["Bullet Punch"], ["Swords Dance", "Stone Edge"]] - }, { - "species": "Lucario", - "item": ["Lucarionite"], - "ability": ["Justified"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Nasty Plot"], ["Focus Blast", "Aura Sphere"], ["Flash Cannon"], ["Vacuum Wave"]] - }, { - "species": "Lucario", - "item": ["Lucarionite"], - "ability": ["Justified"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Close Combat"], ["Meteor Mash"], ["Bullet Punch"], ["Earthquake", "Ice Punch"]] + "moves": [["Close Combat"], ["Meteor Mash"], ["Bullet Punch"], ["Swords Dance"]] }] }, "kyogre": { @@ -383,14 +271,7 @@ "moves": [["Scald"], ["Rest"], ["Sleep Talk"], ["Ice Beam"]] }, { "species": "Kyogre", - "item": ["Choice Specs"], - "ability": ["Drizzle"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Water Spout"], ["Origin Pulse"], ["Ice Beam"], ["Thunder"]] - }, { - "species": "Kyogre", - "item": ["Choice Scarf"], + "item": ["Choice Specs", "Choice Scarf"], "ability": ["Drizzle"], "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Modest", @@ -405,28 +286,14 @@ "ability": ["Multitype"], "evs": {"hp": 252, "def": 64, "spe": 192}, "nature": "Timid", - "moves": [["Ice Beam"], ["Judgment"], ["Toxic", "Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Water", - "item": ["Splash Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, - "nature": "Timid", - "moves": [["Ice Beam"], ["Defog"], ["Toxic", "Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Water", - "item": ["Splash Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, - "nature": "Timid", - "moves": [["Fire Blast"], ["Ice Beam"], ["Toxic"], ["Recover"]] + "moves": [["Ice Beam"], ["Judgment"], ["Toxic"], ["Recover"]] }, { "species": "Arceus-Water", "item": ["Splash Plate"], "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, + "evs": {"hp": 252, "def": 16, "spa": 144, "spe": 96}, "nature": "Timid", - "moves": [["Fire Blast"], ["Defog"], ["Toxic"], ["Recover"]] + "moves": [["Ice Beam"], ["Judgment"], ["Stealth Rock"], ["Recover"]] }] }, "arceusfairy": { @@ -442,7 +309,7 @@ "species": "Arceus-Fairy", "item": ["Pixie Plate"], "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, + "evs": {"hp": 252, "spa": 4, "spe": 252}, "nature": "Timid", "moves": [["Calm Mind"], ["Recover"], ["Judgment"], ["Earth Power"]] }] @@ -466,28 +333,7 @@ "ability": ["Pressure"], "evs": {"atk": 4, "spa": 252, "spe": 252}, "nature": "Rash", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Extreme Speed", "Dark Pulse"]] - }, { - "species": "Deoxys-Attack", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Spikes"], ["Psycho Boost"], ["Knock Off"], ["Extreme Speed"]] - }, { - "species": "Deoxys-Attack", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Stealth Rock"], ["Psycho Boost"], ["Knock Off"], ["Extreme Speed"]] - }, { - "species": "Deoxys-Attack", - "item": ["Life Orb"], - "ability": ["Pressure"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Rash", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Dark Pulse", "Extreme Speed"]] + "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Spikes", "Dark Pulse", "Extreme Speed"]] }] }, "giratinaorigin": { @@ -538,13 +384,6 @@ "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Adamant", "moves": [["Dragon Dance"], ["Thousand Arrows"], ["Substitute"], ["Dragon Tail", "Glare"]] - }, { - "species": "Zygarde", - "item": ["Dragonium Z"], - "ability": ["Power Construct"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Dragon Dance"], ["Thousand Arrows"], ["Substitute"], ["Outrage", "Dragon Tail"]] }] }, "rayquaza": { @@ -601,21 +440,14 @@ "evs": {"hp": 252, "atk": 4, "spd": 252}, "ivs": {"spe": 0}, "nature": "Sassy", - "moves": [["Spikes"], ["Leech Seed"], ["Gyro Ball", "Power Whip"], ["Protect"]] + "moves": [["Spikes"], ["Leech Seed"], ["Gyro Ball"], ["Power Whip", "Protect"]] }] }, "toxapex": { "flags": {}, "sets": [{ "species": "Toxapex", - "item": ["Rocky Helmet"], - "ability": ["Regenerator"], - "evs": {"hp": 252, "def": 252, "spd": 4}, - "nature": "Bold", - "moves": [["Toxic Spikes"], ["Scald"], ["Recover"], ["Haze"]] - }, { - "species": "Toxapex", - "item": ["Shed Shell"], + "item": ["Rocky Helmet", "Shed Shell"], "ability": ["Regenerator"], "evs": {"hp": 252, "def": 252, "spd": 4}, "nature": "Bold", @@ -630,7 +462,7 @@ "ability": ["Multiscale"], "evs": {"hp": 252, "def": 160, "spe": 96}, "nature": "Bold", - "moves": [["Toxic"], ["Roost"], ["Whirlwind", "Dragon Tail"], ["Substitute", "Ice Beam"]] + "moves": [["Toxic"], ["Roost"], ["Whirlwind", "Dragon Tail"], ["Psychic", "Ice Beam"]] }] }, "arceus": { @@ -641,34 +473,16 @@ "ability": ["Multitype"], "evs": {"hp": 240, "atk": 252, "spe": 16}, "nature": "Adamant", - "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Earthquake"]] + "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Recover"]] }, { "species": "Arceus", "item": ["Normalium Z"], "ability": ["Multitype"], - "evs": {"hp": 132, "atk": 252, "spe": 124}, - "nature": "Adamant", - "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Refresh"]] - }, { - "species": "Arceus", - "item": ["Leftovers"], - "ability": ["Multitype"], "evs": {"hp": 240, "atk": 252, "spe": 16}, "nature": "Adamant", "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Substitute"]] }] }, - "arceusbug": { - "flags": {}, - "sets": [{ - "species": "Arceus-Bug", - "item": ["Buginium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["X-Scissor"], ["Earthquake"], ["Stone Edge"]] - }] - }, "deoxysspeed": { "flags": {}, "sets": [{ @@ -701,69 +515,26 @@ "item": ["Ghostium Z"], "ability": ["Shadow Shield"], "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Calm Mind"], ["Moongeist Beam"], ["Psyshock"], ["Focus Blast"]] - }, { - "species": "Lunala", - "item": ["Psychium Z"], - "ability": ["Shadow Shield"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Hypnosis"], ["Calm Mind"], ["Moongeist Beam"], ["Focus Blast"]] - }, { - "species": "Lunala", - "item": ["Choice Specs"], - "ability": ["Shadow Shield"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Moongeist Beam"], ["Psyshock"], ["Focus Blast"], ["Ice Beam"]] + "moves": [["Calm Mind"], ["Moongeist Beam"], ["Psyshock"], ["Ice Beam", "Focus Blast"]] }, { "species": "Lunala", "item": ["Choice Scarf"], "ability": ["Shadow Shield"], "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Modest", - "moves": [["Moongeist Beam"], ["Psyshock"], ["Focus Blast"], ["Ice Beam"]] + "moves": [["Moongeist Beam"], ["Psyshock"], ["Ice Beam"], ["Focus Blast"]] }] }, "dialga": { "flags": {}, "sets": [{ - "species": "Dialga", - "item": ["Dragonium Z"], - "ability": ["Pressure"], - "evs": {"hp": 180, "spa": 252, "spe": 76}, - "nature": "Modest", - "moves": [["Draco Meteor"], ["Fire Blast"], ["Stealth Rock"], ["Toxic", "Thunder"]] - }, { - "species": "Dialga", - "item": ["Dragonium Z"], - "ability": ["Pressure"], - "evs": {"hp": 252, "def": 4, "spa": 252}, - "nature": "Quiet", - "moves": [["Trick Room"], ["Draco Meteor"], ["Flash Cannon"], ["Fire Blast"]] - }, { - "species": "Dialga", - "item": ["Shuca Berry"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Stealth Rock"], ["Draco Meteor"], ["Flash Cannon"], ["Fire Blast"]] - }, { - "species": "Dialga", - "item": ["Chople Berry"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Stealth Rock"], ["Draco Meteor"], ["Flash Cannon"], ["Fire Blast"]] - }, { "species": "Dialga", "item": ["Life Orb"], "ability": ["Pressure"], "evs": {"hp": 104, "spa": 252, "spe": 152}, - "ivs": {"atk": 0}, "nature": "Modest", - "moves": [["Draco Meteor"], ["Fire Blast"], ["Thunder", "Toxic"], ["Ice Beam", "Flash Cannon"]] + "moves": [["Draco Meteor"], ["Fire Blast"], ["Stealth Rock"], ["Thunder"]] }] }, "excadrill": { @@ -774,7 +545,7 @@ "ability": ["Mold Breaker"], "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Jolly", - "moves": [["Stealth Rock"], ["Toxic", "Rock Tomb"], ["Earthquake"], ["Rapid Spin"]] + "moves": [["Stealth Rock"], ["Earthquake", "Rock Tomb"], ["Toxic"], ["Rapid Spin"]] }] }, "diancie": { @@ -802,21 +573,13 @@ "megaOnly": 1 }, "sets": [{ - "species": "Scizor", - "item": ["Scizorite"], - "ability": ["Light Metal"], - "evs": {"hp": 252, "atk": 108, "spd": 148}, - "ivs": {"spe": 0}, - "nature": "Brave", - "moves": [["Bullet Punch"], ["Roost"], ["U-turn"], ["Swords Dance", "Toxic"]] - }, { "species": "Scizor", "item": ["Scizorite"], "ability": ["Light Metal"], "evs": {"hp": 252, "atk": 4, "spd": 252}, "ivs": {"spe": 0}, "nature": "Sassy", - "moves": [["Bullet Punch"], ["Roost"], ["U-turn"], ["Toxic", "Swords Dance"]] + "moves": [["Bullet Punch"], ["Roost"], ["U-turn"], ["Swords Dance", "Toxic"]] }] }, "tapulele": { @@ -828,13 +591,6 @@ "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Timid", "moves": [["Psychic"], ["Moonblast"], ["Grass Knot", "Aromatherapy"], ["Nature's Madness"]] - }, { - "species": "Tapu Lele", - "item": ["Terrain Extender"], - "ability": ["Psychic Surge"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Psychic"], ["Moonblast"], ["Taunt"], ["Nature's Madness"]] }] }, "mewtwo": { @@ -852,21 +608,7 @@ "ability": ["Pressure"], "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Low Kick"], ["Taunt"], ["Ice Beam"], ["Stone Edge"]] - }, { - "species": "Mewtwo", - "item": ["Mewtwonite X"], - "ability": ["Pressure"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Low Kick"], ["Taunt"], ["Ice Beam"], ["Zen Headbutt"]] - }, { - "species": "Mewtwo", - "item": ["Mewtwonite X"], - "ability": ["Pressure"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Low Kick"], ["Taunt"], ["Zen Headbutt"], ["Stone Edge"]] + "moves": [["Low Kick"], ["Taunt"], ["Ice Beam", "Zen Headbutt"], ["Stone Edge"]] }, { "species": "Mewtwo", "item": ["Psychium Z"], @@ -882,23 +624,9 @@ "species": "Blissey", "item": ["Shed Shell"], "ability": ["Natural Cure"], - "evs": {"def": 252, "spd": 252, "spe": 4}, - "nature": "Calm", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Confide"]] - }, { - "species": "Blissey", - "item": ["Shed Shell"], - "ability": ["Natural Cure"], - "evs": {"def": 252, "spd": 252, "spe": 4}, - "nature": "Calm", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Stealth Rock"]] - }, { - "species": "Blissey", - "item": ["Shed Shell"], - "ability": ["Natural Cure"], - "evs": {"def": 252, "spd": 252, "spe": 4}, + "evs": {"hp": 4, "def": 252, "spd": 252}, "nature": "Calm", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Wish", "Seismic Toss"]] + "moves": [["Soft-Boiled"], ["Toxic", "Wish"], ["Heal Bell"], ["Confide"]] }] }, "chansey": { @@ -909,14 +637,7 @@ "ability": ["Natural Cure"], "evs": {"hp": 4, "def": 252, "spd": 252}, "nature": "Bold", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Wish"]] - }, { - "species": "Chansey", - "item": ["Eviolite"], - "ability": ["Natural Cure"], - "evs": {"hp": 4, "def": 252, "spd": 252}, - "nature": "Bold", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Stealth Rock"]] + "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Wish", "Stealth Rock"]] }] }, "arceusdark": { @@ -934,7 +655,7 @@ "ability": ["Multitype"], "evs": {"hp": 252, "def": 160, "spe": 96}, "nature": "Timid", - "moves": [["Judgment"], ["Defog"], ["Recover"], ["Toxic", "Will-O-Wisp"]] + "moves": [["Judgment"], ["Ice Beam"], ["Recover"], ["Toxic", "Will-O-Wisp"]] }] }, "tyranitar": { @@ -953,20 +674,6 @@ "evs": {"hp": 248, "def": 8, "spd": 252}, "nature": "Careful", "moves": [["Stealth Rock"], ["Rock Tomb"], ["Pursuit"], ["Toxic", "Rest"]] - }, { - "species": "Tyranitar", - "item": ["Shuca Berry"], - "ability": ["Sand Stream"], - "evs": {"hp": 248, "def": 8, "spd": 252}, - "nature": "Careful", - "moves": [["Stealth Rock"], ["Rock Tomb"], ["Pursuit"], ["Toxic", "Rest"]] - }, { - "species": "Tyranitar", - "item": ["Tyranitarite"], - "ability": ["Sand Stream"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Dragon Dance"], ["Stone Edge"], ["Crunch", "Ice Punch"], ["Earthquake", "Taunt"]] }] }, "magearna": { @@ -978,13 +685,6 @@ "evs": {"hp": 252, "spa": 4, "spd": 252}, "nature": "Calm", "moves": [["Fleur Cannon"], ["Heart Swap"], ["Volt Switch"], ["Pain Split"]] - }, { - "species": "Magearna", - "item": ["Choice Specs"], - "ability": ["Soul-Heart"], - "evs": {"hp": 252, "spa": 252, "spd": 4}, - "nature": "Modest", - "moves": [["Fleur Cannon"], ["Volt Switch"], ["Grass Knot"], ["Hidden Power Fire"]] }, { "species": "Magearna", "item": ["Normalium Z"], @@ -1022,13 +722,6 @@ "megaOnly": 1 }, "sets": [{ - "species": "Gyarados", - "item": ["Gyaradosite"], - "ability": ["Intimidate"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Dragon Dance"], ["Crunch"], ["Taunt"], ["Waterfall", "Earthquake"]] - }, { "species": "Gyarados", "item": ["Gyaradosite"], "ability": ["Intimidate"], @@ -1040,13 +733,6 @@ "palkia": { "flags": {}, "sets": [{ - "species": "Palkia", - "item": ["Psychium Z"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Heal Block"], ["Hydro Pump"], ["Spacial Rend"], ["Fire Blast", "Thunder"]] - }, { "species": "Palkia", "item": ["Life Orb"], "ability": ["Pressure"], @@ -1055,17 +741,6 @@ "moves": [["Hydro Pump"], ["Spacial Rend", "Draco Meteor"], ["Thunder"], ["Focus Punch", "Fire Blast"]] }] }, - "bronzong": { - "flags": {}, - "sets": [{ - "species": "Bronzong", - "item": ["Leftovers"], - "ability": ["Levitate"], - "evs": {"hp": 252, "atk": 112, "spd": 144}, - "nature": "Sassy", - "moves": [["Stealth Rock"], ["Gyro Ball"], ["Toxic"], ["Protect"]] - }] - }, "blaziken": { "flags": {}, "sets": [{ @@ -1075,25 +750,11 @@ "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Adamant", "moves": [["Swords Dance", "Protect"], ["Flare Blitz"], ["Low Kick"], ["Stone Edge"]] - }, { - "species": "Blaziken", - "item": ["Life Orb"], - "ability": ["Speed Boost"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Protect"], ["Flare Blitz"], ["Low Kick"], ["Hidden Power Ice"]] }] }, "skarmory": { "flags": {}, "sets": [{ - "species": "Skarmory", - "item": ["Leftovers"], - "ability": ["Sturdy"], - "evs": {"hp": 252, "def": 252, "spd": 4}, - "nature": "Bold", - "moves": [["Spikes"], ["Toxic"], ["Whirlwind"], ["Roost"]] - }, { "species": "Skarmory", "item": ["Shed Shell"], "ability": ["Sturdy"], @@ -1108,16 +769,9 @@ "species": "Gothitelle", "item": ["Leftovers"], "ability": ["Shadow Tag"], - "evs": {"hp": 252, "spa": 4, "spd": 252}, - "nature": "Calm", - "moves": [["Charm", "Heal Bell"], ["Calm Mind"], ["Rest"], ["Psyshock"]] - }, { - "species": "Gothitelle", - "item": ["Leftovers"], - "ability": ["Shadow Tag"], - "evs": {"hp": 252, "spa": 4, "spd": 252}, + "evs": {"hp": 244, "def": 108, "spd": 156}, "nature": "Calm", - "moves": [["Charm", "Heal Bell"], ["Confide"], ["Rest"], ["Taunt"]] + "moves": [["Charm"], ["Confide"], ["Rest"], ["Taunt"]] }] }, "buzzwole": { @@ -1128,25 +782,7 @@ "ability": ["Beast Boost"], "evs": {"hp": 248, "def": 184, "spd": 76}, "nature": "Impish", - "moves": [["Hammer Arm"], ["Toxic"], ["Roost"], ["Earthquake"]] - }] - }, - "landorustherian": { - "flags": {}, - "sets": [{ - "species": "Landorus-Therian", - "item": ["Flyinium Z"], - "ability": ["Intimidate"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Earthquake"], ["Fly"], ["Stealth Rock"], ["Swords Dance"]] - }, { - "species": "Landorus-Therian", - "item": ["Rocky Helmet"], - "ability": ["Intimidate"], - "evs": {"hp": 252, "def": 252, "spe": 4}, - "nature": "Impish", - "moves": [["Earthquake"], ["U-turn"], ["Stealth Rock"], ["Stone Edge"]] + "moves": [["Hammer Arm"], ["Toxic"], ["Roost"], ["Earthquake", "Taunt"]] }] }, "arceusghost": { @@ -1157,14 +793,7 @@ "ability": ["Multitype"], "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Jolly", - "moves": [["Swords Dance"], ["Shadow Force"], ["Brick Break"], ["Shadow Claw", "Extreme Speed"]] - }, { - "species": "Arceus-Ghost", - "item": ["Spooky Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Will-O-Wisp", "Toxic"], ["Recover"]] + "moves": [["Swords Dance"], ["Shadow Force"], ["Substitute"], ["Shadow Claw", "Brick Break"]] }] }, "cloyster": { @@ -1175,26 +804,12 @@ "ability": ["Skill Link"], "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Spikes"], ["Rapid Spin"], ["Icicle Spear"], ["Shell Smash"]] - }, { - "species": "Cloyster", - "item": ["Focus Sash"], - "ability": ["Skill Link"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Toxic Spikes"], ["Rapid Spin"], ["Icicle Spear"], ["Shell Smash"]] + "moves": [["Spikes", "Toxic Spikes"], ["Rapid Spin"], ["Icicle Spear"], ["Shell Smash"]] }] }, "arceuspoison": { "flags": {}, "sets": [{ - "species": "Arceus-Poison", - "item": ["Toxic Plate"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Poison Jab"], ["Earthquake"], ["Recover", "Extreme Speed"]] - }, { "species": "Arceus-Poison", "item": ["Toxic Plate"], "ability": ["Multitype"], @@ -1211,46 +826,18 @@ "ability": ["Multitype"], "evs": {"hp": 252, "spa": 4, "spe": 252}, "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Recover"], ["Refresh", "Fire Blast"]] + "moves": [["Calm Mind"], ["Judgment"], ["Recover"], ["Refresh"]] }] }, "shayminsky": { "flags": {}, "sets": [{ "species": "Shaymin-Sky", - "item": ["Leftovers"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Seed Flare"], ["Air Slash"], ["Substitute"], ["Leech Seed"]] - }, { - "species": "Shaymin-Sky", - "item": ["Choice Scarf"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Seed Flare"], ["Air Slash"], ["Hidden Power Rock"], ["Healing Wish"]] - }, { - "species": "Shaymin-Sky", - "item": ["Choice Scarf"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Seed Flare"], ["Air Slash"], ["Hidden Power Ice"], ["Healing Wish"]] - }, { - "species": "Shaymin-Sky", - "item": ["Normalium Z"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Seed Flare"], ["Air Slash"], ["Earth Power"], ["Growth"]] - }, { - "species": "Shaymin-Sky", - "item": ["Normalium Z"], + "item": ["Life Orb"], "ability": ["Serene Grace"], "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Seed Flare"], ["Air Slash"], ["Hidden Power Ice"], ["Growth"]] + "moves": [["Seed Flare"], ["Air Slash"], ["Healing Wish"], ["Hidden Power Ice", "Hidden Power Rock"]] }] }, "smeargle": { @@ -1261,14 +848,7 @@ "ability": ["Own Tempo"], "evs": {"hp": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Sticky Web"], ["Nuzzle"], ["Spore"], ["Skill Swap"]] - }, { - "species": "Smeargle", - "item": ["Focus Sash"], - "ability": ["Own Tempo"], - "evs": {"hp": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Sticky Web"], ["Nuzzle"], ["Spore"], ["Rapid Spin"]] + "moves": [["Sticky Web"], ["Nuzzle"], ["Taunt"], ["Whirlwind", "Toxic Thread"]] }] }, "klefki": { @@ -1303,21 +883,7 @@ "ability": ["Multitype"], "evs": {"hp": 248, "def": 8, "spe": 252}, "nature": "Timid", - "moves": [["Judgment"], ["Calm Mind"], ["Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Rock", - "item": ["Stone Plate"], - "ability": ["Multitype"], - "evs": {"hp": 248, "def": 8, "spe": 252}, - "nature": "Timid", - "moves": [["Judgment"], ["Defog"], ["Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Rock", - "item": ["Rockium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Stone Edge"], ["Earthquake"], ["Substitute", "Recover"]] + "moves": [["Judgment"], ["Calm Mind", "Defog"], ["Will-O-Wisp"], ["Recover"]] }] }, "zekrom": { @@ -1329,20 +895,6 @@ "evs": {"hp": 64, "atk": 252, "spe": 192}, "nature": "Adamant", "moves": [["Hone Claws"], ["Outrage"], ["Bolt Strike"], ["Substitute", "Draco Meteor"]] - }, { - "species": "Zekrom", - "item": ["Shuca Berry"], - "ability": ["Teravolt"], - "evs": {"hp": 64, "atk": 252, "spe": 192}, - "nature": "Adamant", - "moves": [["Hone Claws"], ["Bolt Strike"], ["Outrage"], ["Dragon Claw"]] - }, { - "species": "Zekrom", - "item": ["Choice Scarf"], - "ability": ["Teravolt"], - "evs": {"atk": 252, "spa": 4, "spe": 252}, - "nature": "Naughty", - "moves": [["Bolt Strike"], ["Outrage"], ["Draco Meteor"], ["Volt Switch"]] }] }, "dugtrio": { @@ -1367,17 +919,6 @@ "moves": [["Calm Mind"], ["Judgment"], ["Ice Beam"], ["Recover"]] }] }, - "shuckle": { - "flags": {}, - "sets": [{ - "species": "Shuckle", - "item": ["Mental Herb"], - "ability": ["Sturdy"], - "evs": {"hp": 252, "def": 4, "spd": 252}, - "nature": "Calm", - "moves": [["Sticky Web"], ["Rock Tomb"], ["Encore"], ["Toxic"]] - }] - }, "ditto": { "flags": {}, "sets": [{ @@ -1394,18 +935,11 @@ "flags": {}, "sets": [{ "species": "Darkrai", - "item": ["Psychium Z"], + "item": ["Life Orb"], "ability": ["Bad Dreams"], "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Nasty Plot"], ["Dark Pulse"], ["Hypnosis"], ["Sludge Bomb"]] - }, { - "species": "Darkrai", - "item": ["Choice Scarf"], - "ability": ["Bad Dreams"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Dark Pulse"], ["Ice Beam"], ["Trick"], ["Haze"]] + "moves": [["Nasty Plot"], ["Dark Pulse"], ["Hypnosis"], ["Thunder", "Sludge Bomb"]] }] }, "tapukoko": { @@ -1419,52 +953,9 @@ "moves": [["Thunder"], ["Nature's Madness"], ["Taunt"], ["U-turn"]] }] }, - "solgaleo": { - "flags": {}, - "sets": [{ - "species": "Solgaleo", - "item": ["Choice Scarf"], - "ability": ["Full Metal Body"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Sunsteel Strike"], ["Flare Blitz"], ["Earthquake"], ["Stone Edge"]] - }, { - "species": "Solgaleo", - "item": ["Normalium Z"], - "ability": ["Full Metal Body"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Adamant", - "moves": [["Splash"], ["Sunsteel Strike"], ["Earthquake"], ["Flame Charge"]] - }] - }, - "arceusice": { - "flags": {}, - "sets": [{ - "species": "Arceus-Ice", - "item": ["Icicle Plate"], - "ability": ["Multitype"], - "evs": {"hp": 248, "spa": 8, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Fire Blast"], ["Recover"]] - }, { - "species": "Arceus-Ice", - "item": ["Icium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Ice Beam"], ["Fire Blast"], ["Thunder"]] - }] - }, "arceussteel": { "flags": {}, "sets": [{ - "species": "Arceus-Steel", - "item": ["Steelium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Swords Dance"], ["Iron Head"], ["Earthquake"], ["Stone Edge", "Recover"]] - }, { "species": "Arceus-Steel", "item": ["Iron Plate"], "ability": ["Multitype"], @@ -1477,7 +968,7 @@ "flags": {}, "sets": [{ "species": "Aegislash", - "item": ["Leftovers"], + "item": ["Iron Ball"], "ability": ["Stance Change"], "evs": {"hp": 252, "atk": 4, "spd": 252}, "nature": "Sassy", @@ -1493,24 +984,6 @@ "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Naive", "moves": [["Earth Power"], ["Rock Slide"], ["Hidden Power Ice"], ["Knock Off"]] - }, { - "species": "Landorus", - "item": ["Life Orb"], - "ability": ["Sheer Force"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Earth Power"], ["Rock Slide"], ["Hidden Power Ice"], ["Stealth Rock"]] - }] - }, - "pheromosa": { - "flags": {}, - "sets": [{ - "species": "Pheromosa", - "item": ["Life Orb"], - "ability": ["Beast Boost"], - "evs": {"atk": 252, "spa": 4, "spe": 252}, - "nature": "Naive", - "moves": [["U-turn"], ["Low Kick"], ["Ice Beam"], ["Rapid Spin"]] }] }, "arceusdragon": { @@ -1522,44 +995,17 @@ "evs": {"hp": 252, "def": 64, "spe": 192}, "nature": "Timid", "moves": [["Judgment"], ["Fire Blast"], ["Defog"], ["Recover"]] - }, { - "species": "Arceus-Dragon", - "item": ["Dragonium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Outrage"], ["Earthquake"], ["Iron Tail"]] }] }, "kyuremwhite": { "flags": {}, "sets": [{ - "species": "Kyurem-White", - "item": ["Choice Specs"], - "ability": ["Turboblaze"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Ice Beam"], ["Draco Meteor"], ["Fusion Flare"], ["Earth Power"]] - }, { "species": "Kyurem-White", "item": ["Life Orb"], "ability": ["Turboblaze"], "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Ice Beam"], ["Draco Meteor"], ["Fusion Flare"], ["Stone Edge", "Focus Blast"]] - }] - }, - "kangaskhan": { - "flags": { - "megaOnly": 1 - }, - "sets": [{ - "species": "Kangaskhan", - "item": ["Kangaskhanite"], - "ability": ["Early Bird"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Seismic Toss"], ["Body Slam"], ["Crunch", "Ice Punch"], ["Fake Out"]] + "moves": [["Ice Beam"], ["Draco Meteor"], ["Fusion Flare"], ["Roost"]] }] }, "arceusgrass": { @@ -1573,125 +1019,6 @@ "moves": [["Grass Knot"], ["Ice Beam"], ["Fire Blast"], ["Recover"]] }] }, - "genesect": { - "flags": {}, - "sets": [{ - "species": "Genesect", - "item": ["Choice Specs"], - "ability": ["Download"], - "evs": {"hp": 68, "spa": 188, "spe": 252}, - "nature": "Timid", - "moves": [["Techno Blast"], ["Flamethrower", "Dark Pulse"], ["Flash Cannon"], ["U-turn"]] - }, { - "species": "Genesect", - "item": ["Choice Scarf"], - "ability": ["Download"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Hasty", - "moves": [["U-turn"], ["Iron Head"], ["Flamethrower"], ["Ice Beam"]] - }] - }, - "arceusfighting": { - "flags": {}, - "sets": [{ - "species": "Arceus-Fighting", - "item": ["Fist Plate"], - "ability": ["Multitype"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Judgment"], ["Ice Beam"], ["Shadow Ball"], ["Recover"]] - }] - }, - "arceusfire": { - "flags": {}, - "sets": [{ - "species": "Arceus-Fire", - "item": ["Firium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Blast Burn"], ["Ice Beam"], ["Earth Power", "Thunder"]] - }] - }, - "arceuspsychic": { - "flags": {}, - "sets": [{ - "species": "Arceus-Psychic", - "item": ["Mind Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "spd": 64, "spe": 192}, - "nature": "Calm", - "moves": [["Fire Blast"], ["Dark Pulse"], ["Recover"], ["Toxic", "Judgment"]] - }, { - "species": "Arceus-Psychic", - "item": ["Mind Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Recover"], ["Ice Beam", "Dark Pulse"]] - }] - }, - "deoxys": { - "flags": {}, - "sets": [{ - "species": "Deoxys", - "item": ["Life Orb"], - "ability": ["Pressure"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Rash", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Extreme Speed"]] - }, { - "species": "Deoxys", - "item": ["Choice Scarf"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Focus Blast"], ["Trick"]] - }, { - "species": "Deoxys", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Spikes"], ["Stealth Rock"], ["Taunt"], ["Psycho Boost"]] - }, { - "species": "Deoxys", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Nasty Plot"], ["Psyshock", "Psycho Boost"], ["Ice Beam"], ["Focus Blast"]] - }] - }, - "deoxysdefense": { - "flags": {}, - "sets": [{ - "species": "Deoxys-Defense", - "item": ["Leftovers"], - "ability": ["Pressure"], - "evs": {"hp": 252, "def": 252, "spd": 4}, - "nature": "Bold", - "moves": [["Spikes"], ["Recover"], ["Toxic"], ["Knock Off"]] - }] - }, - "reshiram": { - "flags": {}, - "sets": [{ - "species": "Reshiram", - "item": ["Life Orb"], - "ability": ["Turboblaze"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Blue Flare"], ["Draco Meteor"], ["Stone Edge"], ["Flame Charge"]] - }, { - "species": "Reshiram", - "item": ["Grassium Z"], - "ability": ["Turboblaze"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Blue Flare"], ["Draco Meteor"], ["Solar Beam"], ["Roost"]] - }] - }, "wobbuffet": { "flags": {}, "sets": [{ @@ -1704,17 +1031,6 @@ "moves": [["Encore"], ["Counter"], ["Mirror Coat"], ["Safeguard"]] }] }, - "xurkitree": { - "flags": {}, - "sets": [{ - "species": "Xurkitree", - "item": ["Psychium Z"], - "ability": ["Beast Boost"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Hypnosis"], ["Tail Glow"], ["Thunderbolt"], ["Grass Knot"]] - }] - }, "latias": { "flags": { "megaOnly": 1 @@ -1728,49 +1044,13 @@ "moves": [["Ice Beam"], ["Defog"], ["Toxic"], ["Roost"]] }] }, - "greninja": { - "flags": {}, - "sets": [{ - "species": "Greninja", - "item": ["Focus Sash"], - "ability": ["Protean"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Spikes"], ["Hydro Pump"], ["Taunt"], ["Shadow Sneak"]] - }, { - "species": "Greninja", - "item": ["Focus Sash"], - "ability": ["Protean"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Toxic Spikes"], ["Hydro Pump"], ["Taunt"], ["Shadow Sneak"]] - }] - }, - "scolipede": { - "flags": {}, - "sets": [{ - "species": "Scolipede", - "item": ["Focus Sash"], - "ability": ["Speed Boost"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Pin Missile"], ["Toxic Spikes"], ["Endeavor"], ["Protect"]] - }, { - "species": "Scolipede", - "item": ["Focus Sash"], - "ability": ["Speed Boost"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Pin Missile"], ["Spikes"], ["Endeavor"], ["Protect"]] - }] - }, "necrozmaduskmane": { "flags": {}, "sets": [{ "species": "Necrozma-Dusk-Mane", "item": ["Solganium Z"], "ability": ["Prism Armor"], - "evs": {"atk": 252, "def": 4, "spe": 252}, + "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Adamant", "moves": [["Rock Polish"], ["Swords Dance"], ["Earthquake"], ["Sunsteel Strike"]] }, { @@ -1784,28 +1064,28 @@ "species": "Necrozma-Dusk-Mane", "item": ["Solganium Z"], "ability": ["Prism Armor"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Swords Dance"], ["Moonlight"], ["Sunsteel Strike"], ["Earthquake"]] - }, { - "species": "Necrozma-Dusk-Mane", - "item": ["Solganium Z"], - "ability": ["Prism Armor"], - "evs": {"hp": 252, "atk": 252, "def": 4}, + "evs": {"hp": 252, "atk": 252, "spd": 4}, "ivs": {"spe": 0}, "nature": "Brave", "moves": [["Swords Dance"], ["Trick Room"], ["Sunsteel Strike"], ["Earthquake"]] + }, { + "species": "Necrozma-Dusk-Mane", + "item": ["Life Orb"], + "ability": ["Prism Armor"], + "evs": {"hp": 108, "atk": 252, "spe": 148}, + "nature": "Adamant", + "moves": [["Swords Dance"], ["Morning Sun"], ["Sunsteel Strike"], ["Earthquake", "Photon Geyser"]] }, { "species": "Necrozma-Dusk-Mane", "item": ["Ultranecrozium Z"], - "ability": ["Neuroforce"], + "ability": ["Prism Armor"], "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Swords Dance"], ["Photon Geyser"], ["Earthquake", "Knock Off"], ["Stone Edge", "Sunsteel Strike", "Brick Break"]] + "moves": [["Swords Dance"], ["Photon Geyser"], ["Earthquake"], ["Stone Edge", "Sunsteel Strike"]] }, { "species": "Necrozma-Dusk-Mane", "item": ["Ultranecrozium Z"], - "ability": ["Neuroforce"], + "ability": ["Prism Armor"], "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Timid", "moves": [["Calm Mind"], ["Photon Geyser"], ["Heat Wave"], ["Power Gem", "Dragon Pulse"]] @@ -1826,20 +1106,12 @@ "necrozmadawnwings": { "flags": {}, "sets": [{ - "species": "Necrozma-Dawn-Wings", - "item": ["Lunalium Z"], - "ability": ["Prism Armor"], - "evs": {"hp": 252, "def": 4, "spa": 252}, - "ivs": {"spe": 0}, - "nature": "Quiet", - "moves": [["Trick Room"], ["Moongeist Beam"], ["Power Gem"], ["Calm Mind", "Psyshock"]] - }, { "species": "Necrozma-Dawn-Wings", "item": ["Ultranecrozium Z"], "ability": ["Prism Armor"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Moongeist Beam"], ["Power Gem"], ["Brick Break", "Calm Mind"], ["Photon Geyser", "Heat Wave"]] + "evs": {"atk": 252, "def": 32, "spe": 224}, + "nature": "Jolly", + "moves": [["Swords Dance"], ["Photon Geyser"], ["Earthquake"], ["Stone Edge"]] }] } }, diff --git a/data/formats-data.js b/data/formats-data.js index 305947a7b2fb..0c5deab0275d 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -471,7 +471,7 @@ let BattleFormatsData = { sandslash: { randomBattleMoves: ["earthquake", "swordsdance", "rapidspin", "toxic", "stealthrock", "knockoff"], randomDoubleBattleMoves: ["earthquake", "rockslide", "stoneedge", "swordsdance", "xscissor", "knockoff", "protect"], - tier: "NU", + tier: "PU", doublesTier: "DUU", }, sandslashalola: { @@ -3733,6 +3733,7 @@ let BattleFormatsData = { {"generation": 5, "level": 45, "isHidden": true, "moves": ["irondefense", "agility", "hammerarm", "doubleedge"]}, {"generation": 5, "level": 45, "isHidden": true, "moves": ["psychic", "meteormash", "hammerarm", "doubleedge"]}, {"generation": 5, "level": 58, "nature": "Serious", "ivs": {"hp": 30, "atk": 30, "def": 30, "spa": 30, "spd": 30, "spe": 30}, "isHidden": false, "moves": ["earthquake", "hyperbeam", "psychic", "meteormash"], "pokeball": "cherishball"}, + {"generation": 7, "level": 50, "nature": "Jolly", "ivs": {"hp": 31, "atk": 31, "def": 31, "spa": 31, "spd": 31, "spe": 31}, "isHidden": false, "moves": ["ironhead", "icepunch", "bulletpunch", "stompingtantrum"], "pokeball": "cherishball"}, ], tier: "UU", doublesTier: "DUU", @@ -3804,6 +3805,8 @@ let BattleFormatsData = { {"generation": 5, "level": 68, "shiny": 1, "moves": ["psychoshift", "charm", "psychic", "healpulse"]}, {"generation": 6, "level": 30, "shiny": 1, "moves": ["healpulse", "dragonbreath", "mistball", "psychoshift"]}, {"generation": 7, "level": 60, "shiny": 1, "moves": ["mistball", "dragonpulse", "psychoshift", "wish"]}, + {"generation": 7, "level": 60, "moves": ["mistball", "dragonpulse", "psychoshift", "wish"], "pokeball": "cherishball"}, + {"generation": 7, "level": 100, "moves": ["mistball", "psychic", "dracometeor", "tailwind"], "pokeball": "cherishball"}, ], eventOnly: true, tier: "UU", @@ -3829,6 +3832,8 @@ let BattleFormatsData = { {"generation": 6, "level": 30, "shiny": 1, "moves": ["healpulse", "dragonbreath", "lusterpurge", "psychoshift"]}, {"generation": 6, "level": 50, "nature": "Modest", "moves": ["dragonpulse", "lusterpurge", "psychic", "healpulse"], "pokeball": "cherishball"}, {"generation": 7, "level": 60, "shiny": 1, "moves": ["lusterpurge", "dragonpulse", "psychoshift", "dragonbreath"]}, + {"generation": 7, "level": 60, "moves": ["lusterpurge", "dragonpulse", "psychoshift", "dragonbreath"], "pokeball": "cherishball"}, + {"generation": 7, "level": 100, "moves": ["lusterpurge", "psychic", "dracometeor", "tailwind"], "pokeball": "cherishball"}, ], eventOnly: true, tier: "OU", @@ -4051,7 +4056,7 @@ let BattleFormatsData = { tier: "NFE", }, empoleon: { - randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "hiddenpowerfire", "icebeam", "scald", "toxic", "roar", "stealthrock"], + randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "defog", "icebeam", "scald", "toxic", "roar", "stealthrock"], randomDoubleBattleMoves: ["icywind", "scald", "surf", "icebeam", "hiddenpowerelectric", "protect", "grassknot", "flashcannon"], eventPokemon: [ {"generation": 5, "level": 100, "gender": "M", "isHidden": false, "moves": ["hydropump", "icebeam", "aquajet", "grassknot"], "pokeball": "cherishball"}, @@ -5118,7 +5123,7 @@ let BattleFormatsData = { tier: "LC", }, whimsicott: { - randomBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast", "defog"], + randomBattleMoves: ["encore", "taunt", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast", "defog"], randomDoubleBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "helpinghand", "stunspore", "moonblast", "tailwind", "dazzlinggleam", "gigadrain", "protect", "defog"], eventPokemon: [ {"generation": 5, "level": 50, "gender": "F", "nature": "Timid", "ivs": {"spe": 31}, "isHidden": false, "abilities": ["prankster"], "moves": ["swagger", "gigadrain", "beatup", "helpinghand"], "pokeball": "cherishball"}, @@ -6050,7 +6055,7 @@ let BattleFormatsData = { eventPokemon: [ {"generation": 6, "level": 49, "gender": "M", "perfectIVs": 2, "isHidden": false, "abilities": ["unnerve"], "moves": ["hypervoice", "fireblast", "darkpulse"], "pokeball": "cherishball"}, ], - tier: "PU", + tier: "PUBL", doublesTier: "DUU", }, flabebe: { @@ -6172,7 +6177,7 @@ let BattleFormatsData = { tier: "LC", }, malamar: { - randomBattleMoves: ["superpower", "knockoff", "psychocut", "rockslide", "substitute", "trickroom"], + randomBattleMoves: ["superpower", "knockoff", "psychocut", "rest", "sleeptalk", "happyhour"], randomDoubleBattleMoves: ["superpower", "psychocut", "rockslide", "trickroom", "knockoff", "protect"], eventPokemon: [ {"generation": 6, "level": 50, "nature": "Adamant", "ivs": {"hp": 31, "atk": 31}, "isHidden": false, "abilities": ["contrary"], "moves": ["superpower", "knockoff", "facade", "rockslide"], "pokeball": "cherishball"}, @@ -6956,7 +6961,7 @@ let BattleFormatsData = { doublesTier: "DUU", }, silvallywater: { - randomBattleMoves: ["multiattack", "icebeam", "thunderbolt", "partingshot"], + randomBattleMoves: ["multiattack", "icebeam", "thunderbolt", "partingshot", "defog"], randomDoubleBattleMoves: ["protect", "multiattack", "icebeam", "thunderbolt", "flamethrower", "partingshot", "uturn", "thunderwave"], requiredItem: "Water Memory", tier: "PU", @@ -7529,6 +7534,10 @@ let BattleFormatsData = { isNonstandard: true, tier: "CAP LC", }, + mumbao: { + isNonstandard: true, + tier: "CAP LC", + }, pokestarufo: { isNonstandard: true, eventPokemon: [ diff --git a/data/items.js b/data/items.js index bb4abf131171..9dbc231414f8 100644 --- a/data/items.js +++ b/data/items.js @@ -563,7 +563,7 @@ let BattleItems = { }, "brightpowder": { id: "brightpowder", - name: "BrightPowder", + name: "Bright Powder", spritenum: 51, fling: { basePower: 10, diff --git a/data/learnsets.js b/data/learnsets.js index 4e697634fa95..01fca4f9be9d 100644 --- a/data/learnsets.js +++ b/data/learnsets.js @@ -25508,7 +25508,6 @@ let BattleLearnsets = { captivate: ["4M"], confide: ["7M", "6M"], counter: ["7E", "6E", "5E", "4E"], - dive: ["6M"], doubleedge: ["3T"], doubleteam: ["7M", "6M", "5M", "4M", "3M"], echoedvoice: ["7M", "6M", "5M"], @@ -33967,7 +33966,7 @@ let BattleLearnsets = { bodyslam: ["3T"], brickbreak: ["7M", "6M", "5M", "4M", "3M"], bulldoze: ["7M", "6M", "5M"], - bulletpunch: ["7L26", "6L26", "5L32", "5S2", "5S1", "4L32", "4S0"], + bulletpunch: ["7L26", "7S7", "6L26", "5L32", "5S2", "5S1", "4L32", "4S0"], confide: ["7M", "6M"], confusion: ["7L1", "6L1", "5L1", "4L1", "3L1"], cut: ["6M", "5M", "4M", "3M"], @@ -33992,10 +33991,10 @@ let BattleLearnsets = { hiddenpower: ["7M", "6M", "5M", "4M", "3M"], honeclaws: ["6M", "5M"], hyperbeam: ["7M", "7L60", "6M", "6L60", "5M", "5L71", "5S6", "4M", "4L71", "3M", "3L77"], - icepunch: ["7T", "6T", "5T", "5S2", "4T", "3T"], + icepunch: ["7T", "7S7", "6T", "5T", "5S2", "4T", "3T"], icywind: ["7T", "6T", "5T", "4T", "3T"], irondefense: ["7T", "7L52", "6T", "6L52", "5T", "5L40", "5S4", "4T", "4L40", "3L44"], - ironhead: ["7T", "6T", "5T", "4T"], + ironhead: ["7T", "7S7", "6T", "5T", "4T"], laserfocus: ["7T"], lightscreen: ["7M", "6M", "5M", "4M", "3M"], magnetrise: ["7T", "7L1", "6T", "6L1", "5T", "5L1", "4T", "4L1"], @@ -34031,7 +34030,7 @@ let BattleLearnsets = { sludgebomb: ["7M", "6M", "5M", "4M", "3M"], snore: ["7T", "6T", "5T", "4T", "3T"], stealthrock: ["7T", "6T", "5T", "4M"], - stompingtantrum: ["7T"], + stompingtantrum: ["7T", "7S7"], strength: ["6M", "5M", "4M", "3M"], substitute: ["7M", "6M", "5M", "4M", "3T"], sunnyday: ["7M", "6M", "5M", "4M", "3M"], @@ -34299,10 +34298,10 @@ let BattleLearnsets = { dive: ["6M", "5M", "4T", "3M"], doubleedge: ["3T"], doubleteam: ["7M", "6M", "5M", "4M", "3M"], - dracometeor: ["7T", "6T", "5T", "4T"], + dracometeor: ["7T", "7S9", "6T", "5T", "4T"], dragonbreath: ["7L20", "6L20", "6S6", "5L20", "4L20", "4S3", "3L20"], dragonclaw: ["7M", "6M", "5M", "4M", "3M"], - dragonpulse: ["7T", "7L56", "7S7", "6T", "6L1", "5T", "5L80", "4M", "4L70"], + dragonpulse: ["7T", "7L56", "7S8", "7S7", "6T", "6L1", "5T", "5L80", "4M", "4L70"], dreameater: ["7M", "6M", "5M", "4M", "3T"], earthquake: ["7M", "6M", "5M", "4M", "3M"], endure: ["4M", "3T"], @@ -34329,13 +34328,13 @@ let BattleLearnsets = { magiccoat: ["7T", "6T", "5T", "4T"], magicroom: ["7T", "6T", "5T"], mimic: ["3T"], - mistball: ["7L24", "7S7", "6L24", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], + mistball: ["7L24", "7S9", "7S8", "7S7", "6L24", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], mudslap: ["4T", "3T"], naturalgift: ["4M"], outrage: ["7T", "6T", "5T", "4T"], protect: ["7M", "6M", "5M", "4M", "3M"], - psychic: ["7M", "7L51", "6M", "6L51", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], - psychoshift: ["7L28", "7S7", "6L28", "6S6", "5L50", "5S5", "4L50"], + psychic: ["7M", "7L51", "7S9", "6M", "6L51", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], + psychoshift: ["7L28", "7S8", "7S7", "6L28", "6S6", "5L50", "5S5", "4L50"], psychup: ["7M", "6M", "5M", "4M", "3T"], psyshock: ["7M", "6M", "5M"], psywave: ["7L1", "6L1", "5L1", "4L1", "3L1"], @@ -34368,7 +34367,7 @@ let BattleLearnsets = { surf: ["7M", "6M", "5M", "4M", "3M"], swagger: ["7M", "6M", "5M", "4M", "3T"], swift: ["4T", "3T"], - tailwind: ["7T", "6T", "5T", "4T"], + tailwind: ["7T", "7S9", "6T", "5T", "4T"], telekinesis: ["7T", "5M"], thunder: ["7M", "6M", "5M", "4M", "3M"], thunderbolt: ["7M", "6M", "5M", "4M", "3M"], @@ -34380,7 +34379,7 @@ let BattleLearnsets = { waterpulse: ["7T", "6T", "4M", "3M"], watersport: ["7L4", "6L4", "5L25", "4L25", "4S4", "4S3", "3L25", "3S0"], whirlpool: ["4M"], - wish: ["7L1", "7S7", "6L1", "5L5", "4L5", "3L5"], + wish: ["7L1", "7S8", "7S7", "6L1", "5L5", "4L5", "3L5"], zenheadbutt: ["7T", "7L41", "6T", "6L40", "5T", "5L40", "4T", "4L40", "4S4"], }}, latios: {learnset: { @@ -34398,11 +34397,11 @@ let BattleLearnsets = { dive: ["6M", "5M", "4T", "3M"], doubleedge: ["3T"], doubleteam: ["7M", "6M", "5M", "4M", "3M"], - dracometeor: ["7T", "6T", "5T", "4T"], - dragonbreath: ["7L20", "7S8", "6L20", "6S6", "5L20", "4L20", "4S3", "3L20"], + dracometeor: ["7T", "7S10", "6T", "5T", "4T"], + dragonbreath: ["7L20", "7S9", "7S8", "6L20", "6S6", "5L20", "4L20", "4S3", "3L20"], dragonclaw: ["7M", "6M", "5M", "4M", "3M"], dragondance: ["7L7", "6L1", "5L55", "5S5", "4L55", "3L50", "3S2", "3S1"], - dragonpulse: ["7T", "7L56", "7S8", "6T", "6L1", "6S7", "5T", "5L80", "4M", "4L70"], + dragonpulse: ["7T", "7L56", "7S9", "7S8", "6T", "6L1", "6S7", "5T", "5L80", "4M", "4L70"], dreameater: ["7M", "6M", "5M", "4M", "3T"], earthquake: ["7M", "6M", "5M", "4M", "3M"], endure: ["4M", "3T"], @@ -34425,7 +34424,7 @@ let BattleLearnsets = { laserfocus: ["7T"], lastresort: ["7T", "6T", "5T", "4T"], lightscreen: ["7M", "6M", "5M", "4M", "3M"], - lusterpurge: ["7L24", "7S8", "6L24", "6S7", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], + lusterpurge: ["7L24", "7S10", "7S9", "7S8", "6L24", "6S7", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], magiccoat: ["7T", "6T", "5T", "4T"], memento: ["7L61", "7L1", "6L1", "5L85", "4L60", "3L5"], mimic: ["3T"], @@ -34434,8 +34433,8 @@ let BattleLearnsets = { outrage: ["7T", "6T", "5T", "4T"], powersplit: ["7L46", "6L1", "5L75"], protect: ["7M", "7L4", "6M", "6L4", "5M", "5L25", "4M", "4L25", "4S4", "4S3", "3M", "3L25", "3S0"], - psychic: ["7M", "7L51", "6M", "6L51", "6S7", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], - psychoshift: ["7L28", "7S8", "6L28", "6S6", "5L50", "5S5", "4L50"], + psychic: ["7M", "7L51", "7S10", "6M", "6L51", "6S7", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], + psychoshift: ["7L28", "7S9", "7S8", "6L28", "6S6", "5L50", "5S5", "4L50"], psychup: ["7M", "6M", "5M", "4M", "3T"], psyshock: ["7M", "6M", "5M"], psywave: ["7L1", "6L1", "5L1", "4L1", "3L1"], @@ -34465,7 +34464,7 @@ let BattleLearnsets = { surf: ["7M", "6M", "5M", "4M", "3M"], swagger: ["7M", "6M", "5M", "4M", "3T"], swift: ["4T", "3T"], - tailwind: ["7T", "6T", "5T", "4T"], + tailwind: ["7T", "7S10", "6T", "5T", "4T"], telekinesis: ["7T", "7L36", "6L1", "5M", "5L70"], thunder: ["7M", "6M", "5M", "4M", "3M"], thunderbolt: ["7M", "6M", "5M", "4M", "3M"], @@ -63258,537 +63257,645 @@ let BattleLearnsets = { wrap: ["7L1", "4L1"], }}, syclar: {learnset: { - attract: ["4M"], - avalanche: ["4M"], - blizzard: ["4M"], + absorb: ["7L1"], + attract: ["7M", "4M"], + avalanche: ["7L48", "4M"], + blizzard: ["7M", "4M"], bugbite: ["4T"], - bugbuzz: ["4L42"], + bugbuzz: ["7L43", "4L42"], captivate: ["4M"], - counter: ["4T"], + confide: ["7M"], + counter: ["7E", "4T"], cut: ["4M"], - doubleteam: ["4M"], - earthpower: ["4T", "4E"], + doubleedge: ["4T"], + doubleteam: ["7M", "4M"], + earthpower: ["7E", "4T", "4E"], endure: ["4M"], - facade: ["4M"], - falseswipe: ["4M"], - fling: ["4M"], - focusenergy: ["4L13"], - frustration: ["4M"], - furyattack: ["4L1"], - furycutter: ["4T"], - hail: ["4M", "4L28"], - hiddenpower: ["4M"], - icebeam: ["4M"], - iceshard: ["4L8"], - icywind: ["4T", "4L18"], - leechlife: ["4L5"], - leer: ["4L1"], - mimic: ["4T"], + facade: ["7M", "4M"], + falseswipe: ["7M", "4M"], + fellstinger: ["7E"], + fling: ["7M", "4M"], + focusenergy: ["7L10", "4L13"], + frostbreath: ["7M"], + frustration: ["7M", "4M"], + furyattack: ["7L14", "4L1"], + furycutter: ["7L23", "4T"], + hail: ["7M", "7L34", "4M", "4L28"], + hiddenpower: ["7M", "4M"], + honeclaws: ["7M"], + icebeam: ["7M", "4M"], + icepunch: ["7L1"], + iceshard: ["7L5", "4L8"], + iciclecrash: ["7L39"], + icywind: ["7T", "7L19", "4T", "4L18"], + leechlife: ["7M", "4L5"], + leer: ["7L1", "4L1"], + mimic: ["7E", "4T"], naturalgift: ["4M"], - pinmissile: ["4E"], - protect: ["4M"], - rest: ["4M"], - return: ["4M"], - secretpower: ["4M"], - sheercold: ["4L49"], + pinmissile: ["7E", "4E"], + protect: ["7M", "4M"], + raindance: ["7M", "4M"], + rest: ["7M", "4M"], + return: ["7M", "4M"], + rocksmash: ["6M", "5M", "4M"], + round: ["7M"], + secretpower: ["7M", "4M"], + sheercold: ["5L55", "4L49"], + signalbeam: ["7T", "7E"], silverwind: ["4M"], - sleeptalk: ["4M"], - snore: ["4T"], - spikes: ["4E"], + slash: ["7L28"], + sleeptalk: ["7M", "4M"], + snore: ["7T", "4T"], + spikes: ["7E", "4E"], + strength: ["6M", "5M"], stringshot: ["4T"], - substitute: ["4M"], - superpower: ["4T", "4E"], - swagger: ["4M"], - swordsdance: ["4M"], - tailglow: ["4E"], - taunt: ["4M"], - toxic: ["4M"], - uturn: ["4M"], - waterpulse: ["4M"], - xscissor: ["4M", "4L23"], + strugglebug: ["7M"], + substitute: ["7M", "4M"], + superpower: ["7T", "7E", "4T", "4E"], + swagger: ["7M", "4M"], + swordsdance: ["7M", "4M"], + tailglow: ["7E", "4E"], + taunt: ["7M", "4M"], + toxic: ["7M", "4M"], + uturn: ["7M", "4M"], + waterpulse: ["6T", "4M"], + xscissor: ["7M", "7L31", "4M", "4L23"], }}, embirch: {learnset: { - aromatherapy: ["4E"], - attract: ["4M"], - block: ["4T"], - bulletseed: ["4M", "4L1"], - counter: ["4T", "4E"], - doubleedge: ["4T", "4L33"], - doubleteam: ["4M"], - dragonbreath: ["4E"], - ember: ["4L9"], + amnesia: ["7L37"], + aromatherapy: ["7E", "4E"], + attract: ["7M", "4M"], + block: ["7T", "4T"], + bulletseed: ["7L1", "4M", "4L1"], + counter: ["7E", "4T", "4E"], + doubleedge: ["7L1", "7E", "4T", "4L33"], + doubleteam: ["7M", "4M"], + dragonbreath: ["7E", "4E"], + earthpower: ["7T"], + ember: ["7L5", "4L9"], endure: ["4M"], energyball: ["4M"], - facade: ["4M"], - firespin: ["4L25"], - flamethrower: ["4M"], - flamewheel: ["4L17"], + facade: ["7M", "4M"], + firespin: ["7E", "4L25"], + flamethrower: ["7M", "4M"], + flamewheel: ["7L19", "4L17"], flash: ["4M"], - frustration: ["4M"], - gigadrain: ["4M", "4L21"], - grassknot: ["4M"], - grasswhistle: ["4E"], - growth: ["4L5"], + frustration: ["7M", "4M"], + gigadrain: ["7T", "7L32", "4M", "4L21"], + grassknot: ["7M", "4M"], + grasswhistle: ["7E", "4E"], + grassyterrain: ["7E"], + growth: ["7L28", "4L5"], headbutt: ["4T"], - heatwave: ["4T"], - hiddenpower: ["4M"], - irondefense: ["4T"], - irontail: ["4M"], - lavaplume: ["4L40"], - leechseed: ["4L13"], - lightscreen: ["4M"], + heatwave: ["7T", "4T"], + hiddenpower: ["7M", "4M"], + irondefense: ["7T", "7L37", "4T"], + ironhead: ["7T"], + irontail: ["7T", "4M"], + lavaplume: ["7L41", "4L40"], + leechseed: ["7L10", "4L13"], + lightscreen: ["7M", "4M"], lowkick: ["4T"], mimic: ["4T"], mudslap: ["4T"], naturalgift: ["4M"], - overheat: ["4M"], - petaldance: ["4L29"], - protect: ["4M"], - psybeam: ["4E"], - rest: ["4M"], - return: ["4M"], - revenge: ["4E"], - roar: ["4M"], + naturepower: ["7M"], + overheat: ["7M", "4M"], + petalblizzard: ["7E"], + petaldance: ["7L 46", "4L29"], + protect: ["7M", "4M"], + psybeam: ["7E", "4E"], + rest: ["7M", "4M"], + return: ["7M", "4M"], + revenge: ["7E", "4E"], + roar: ["7M", "4M"], rockclimb: ["4M"], - rockslide: ["4M"], + rockslide: ["7M", "4M"], rocksmash: ["4M"], - rocktomb: ["4M"], - safeguard: ["4M"], - sandtomb: ["4E"], + rocktomb: ["7M", "4M"], + round: ["7M"], + safeguard: ["7M", "4M"], + sandtomb: ["7L23", "7E", "4E"], secretpower: ["4M"], - seedbomb: ["4T"], - sleeptalk: ["4M"], - snore: ["4T"], - solarbeam: ["4M"], - stealthrock: ["4M"], + seedbomb: ["7T", "4T"], + sleeptalk: ["7M", "4M"], + snore: ["7T", "4T"], + solarbeam: ["7M", "4M"], + stealthrock: ["7T", "4M"], strength: ["4M"], - substitute: ["4M"], - sunnyday: ["4M"], - swagger: ["4M"], - sweetscent: ["4L1"], - swordsdance: ["4M"], - synthesis: ["4T", "4L37"], - toxic: ["4M"], - watersport: ["4E"], - willowisp: ["4M"], - worryseed: ["4T"], + substitute: ["7M", "4M"], + sunnyday: ["7M", "4M"], + swagger: ["7M", "4M"], + sweetscent: ["7L1", "4L1"], + swordsdance: ["7M", "4M"], + synthesis: ["7T", "7L14", "4T", "4L37"], + toxic: ["7M", "4M"], + watersport: ["7E", "4E"], + willowisp: ["7M", "4M"], + worryseed: ["7T", "4T"], zapcannon: ["4L44"], }}, flarelm: {learnset: { - aromatherapy: ["4E"], - attract: ["4M"], - block: ["4T"], - bulletseed: ["4M", "4L1"], - counter: ["4T", "4E"], - doubleedge: ["4T"], - doubleteam: ["4M"], - dragonbreath: ["4E"], - ember: ["4L9"], + amnesia: ["7L37"], + ancientpower: ["4T"], + aromatherapy: ["7E", "4E"], + attract: ["7M", "4M"], + block: ["7T", "4T"], + bulldoze: ["7M"], + bulletseed: ["7L1", "4M", "4L1"], + confide: ["7M"], + counter: ["7E", "4T", "4E"], + doubleedge: ["7L1", "7E", "4T"], + doubleteam: ["7M", "4M"], + dragonbreath: ["7E", "4E"], + dragonpulse: ["7T"], + dragontail: ["7M"], + earthpower: ["7T", "4T"], + earthquake: ["7M", "4M"], + ember: ["7L5", "4L9"], endure: ["4M"], energyball: ["4M"], - facade: ["4M"], - firespin: ["4L28"], - flamethrower: ["4M"], - flamewheel: ["4L17"], + facade: ["7M", "4M"], + fireblast: ["7M", "4M"], + firespin: ["7E", "4L28"], + flameburst: ["7L24"], + flamecharge: ["7M"], + flamethrower: ["7M", "4M"], + flamewheel: ["7L19", "4L17"], flash: ["4M"], - frustration: ["4M"], - gigadrain: ["4M", "4L21"], - grassknot: ["4M"], - grasswhistle: ["4E"], - growth: ["4L5"], + flashcannon: ["7M"], + frustration: ["7M", "4M"], + gigadrain: ["7L32", "4M", "4L21"], + grassknot: ["7M", "4M"], + grasswhistle: ["7E", "4E"], + grassyterrain: ["7E"], + growth: ["7L28", "4L5"], headbutt: ["4T"], - heatwave: ["4T"], - hiddenpower: ["4M"], - irondefense: ["4T", "4L40"], - irontail: ["4M"], - lavaplume: ["4L48"], - leechseed: ["4L13"], - lightscreen: ["4M"], - lowkick: ["4T"], + heatwave: ["7T", "4T"], + hiddenpower: ["7M", "4M"], + incinerate: ["7M"], + irondefense: ["7L37", "4T", "4L40"], + ironhead: ["7T"], + irontail: ["7T", "4M"], + lavaplume: ["7L46", "4L48"], + leechseed: ["7L10", "4L13"], + lightscreen: ["7M", "4M"], + lowkick: ["7T", "4T"], mimic: ["4T"], mudslap: ["4T"], naturalgift: ["4M"], - overheat: ["4M"], - petaldance: ["4L36"], - protect: ["4M"], - psybeam: ["4E"], - rest: ["4M"], - return: ["4M"], - revenge: ["4E"], - roar: ["4M"], + naturepower: ["7M"], + overheat: ["7M", "4M"], + petalblizzard: ["7E"], + petaldance: ["7L50", "4L36"], + protect: ["7M", "4M"], + psybeam: ["7E", "4E"], + rest: ["7M", "4M"], + return: ["7M", "4M"], + revenge: ["7E", "4E"], + roar: ["7M", "4M"], rockclimb: ["4M"], - rockslide: ["4M"], - rocksmash: ["4M"], - rocktomb: ["4M"], - safeguard: ["4M"], - sandtomb: ["4E"], - secretpower: ["4M"], - seedbomb: ["4T"], - sleeptalk: ["4M"], - snore: ["4T"], - solarbeam: ["4M"], - stealthrock: ["4M"], - strength: ["4M"], - substitute: ["4M"], - sunnyday: ["4M"], - swagger: ["4M"], - sweetscent: ["4L1"], - swordsdance: ["4M"], - synthesis: ["4T", "4L44"], - toxic: ["4M"], - watersport: ["4E"], - willowisp: ["4M"], - worryseed: ["4T"], + rockslide: ["7M", "4M"], + rocksmash: ["6M", "5M", "4M"], + rocktomb: ["7M", "4M"], + round: ["7M"], + safeguard: ["7M", "4M"], + sandtomb: ["7E", "4E"], + secretpower: ["7M", "4M"], + seedbomb: ["7L23", "4T"], + sleeptalk: ["7M", "4M"], + snore: ["7T", "4T"], + solarbeam: ["7M", "4M"], + stealthrock: ["7T", "4M"], + strength: ["6M", "5M", "4M"], + substitute: ["7M", "4M"], + sunnyday: ["7M", "4M"], + swagger: ["7M", "4M"], + sweetscent: ["7L1", "4L1"], + swordsdance: ["7M", "4M"], + synthesis: ["7L14", "4T", "4L44"], + toxic: ["7M", "4M"], + watersport: ["7E", "4E"], + wildcharge: ["7M"], + willowisp: ["7M", "4M"], + worryseed: ["7T", "4T"], + zenheadbutt: ["7T"], zapcannon: ["4L52"], }}, breezi: {learnset: { - aerialace: ["4M", "4L55"], - attract: ["4M"], - block: ["4T"], + acrobatics: ["7M", "7L59"], + aerialace: ["7M", "7L30", "4M", "4L55"], + afteryou: ["7T"], + attract: ["7M", "4M"], bodyslam: ["4T", "4L30"], + block: ["7T", "4T"], captivate: ["4M"], - copycat: ["4L19"], - disable: ["4E"], - doubleedge: ["4T"], - doubleteam: ["4M"], - encore: ["4L5"], + confide: ["7M"], + copycat: ["7L19", "4L19"], + disable: ["7E", "4E"], + doubleedge: ["7E", "4T"], + doubleteam: ["7M", "4M"], + encore: ["7L5", "4L5"], endure: ["4M"], - facade: ["4M"], - fling: ["4M"], - followme: ["4E"], - frustration: ["4M"], - gastroacid: ["4T"], - gust: ["4L1"], - healblock: ["4L50"], - helpinghand: ["4T", "4L1"], - hiddenpower: ["4M"], - icywind: ["4T"], - knockoff: ["4T", "4L14"], - lightscreen: ["4M"], - luckychant: ["4L59"], - mefirst: ["4E"], - metronome: ["4E"], - mimic: ["4T", "4E"], + entrainment: ["7E"], + facade: ["7M", "4M"], + fling: ["7M", "4M"], + followme: ["7E", "4E"], + frustration: ["7M", "4M"], + gastroacid: ["7T", "4T"], + gust: ["7L1", "4L1"], + healblock: ["7L54", "4L50"], + helpinghand: ["7T", "7L1", "4T", "4L1"], + hiddenpower: ["7M", "4M"], + icywind: ["7T", "4T"], + knockoff: ["7L14", "4T", "4L14"], + lightscreen: ["7M", "4M"], + luckychant: ["7L55", "4L59"], + mefirst: ["7E", "4E"], + metronome: ["7E", "4T", "4E"], + mimic: ["7E", "4T", "4E"], mudslap: ["4T"], naturalgift: ["4M"], ominouswind: ["4T"], - poisonjab: ["4M"], - protect: ["4M"], - psychup: ["4M"], - raindance: ["4M"], - reflect: ["4M"], - rest: ["4M", "4L44"], - return: ["4M"], - safeguard: ["4M", "4L9"], - sandstorm: ["4M"], - sandtomb: ["4E"], - secretpower: ["4M"], - shadowball: ["4M"], - skillswap: ["4M"], - sleeptalk: ["4M"], - sludgebomb: ["4M", "4L34"], - snatch: ["4M"], - snore: ["4T"], - spikes: ["4E"], - substitute: ["4M"], - sunnyday: ["4M"], - swagger: ["4M"], + poisonjab: ["7M", "4M"], + protect: ["7M", "4M"], + psychup: ["7M", "4M"], + raindance: ["7M", "4M"], + reflect: ["7M", "4M"], + rest: ["7M", "7L44", "4M", "4L44"], + return: ["7M", "4M"], + roleplay: ["7T"], + round: ["7M"], + safeguard: ["7M", "7L9", "4M", "4L9"], + sandstorm: ["7M", "4M"], + sandtomb: ["7E", "4E"], + secretpower: ["7M", "4M"], + shadowball: ["7M", "4M"], + skillswap: ["7M", "4M"], + sleeptalk: ["7M", "4M"], + sludgebomb: ["7M", "7L34", "4M", "4L34"], + snatch: ["7M", "4M"], + snore: ["7T", "4T"], + spikes: ["7E", "4E"], + stealthrock: ["7T"], + substitute: ["7M", "4M"], + sunnyday: ["7M", "4M"], + swagger: ["7M", "4M"], swift: ["4T"], - tailwind: ["4T", "4L1"], - toxic: ["4M"], - toxicspikes: ["4L39"], - trickroom: ["4M"], + tailwind: ["7L1", "4T", "4L1"], + taunt: ["7M"], + thief: ["7M"], + toxic: ["7M", "4M"], + toxicspikes: ["7L39", "4L39"], + trickroom: ["7M", "4M"], twister: ["4T"], - uturn: ["4M"], - whirlwind: ["4L25"], - wish: ["4E"], + uturn: ["7M", "4M"], + venoshock: ["7M"], + whirlwind: ["7L25", "4L25"], + wish: ["7E", "4E"], }}, scratchet: {learnset: { - aerialace: ["5M"], - attract: ["5M"], - batonpass: ["5E"], - brickbreak: ["5M"], - bulkup: ["5M", "5L40"], - bulldoze: ["5M"], - confuseray: ["5E"], - doubleteam: ["5M"], - echoedvoice: ["5M"], - facade: ["5M"], - falseswipe: ["5M"], - flash: ["5M"], - fling: ["5M"], - focusblast: ["5M"], - focusenergy: ["5L13", "5E"], - frustration: ["5M"], - furyswipes: ["5L1"], - grassknot: ["5M"], - harden: ["5L9"], - haze: ["5E"], - hiddenpower: ["5M"], - hypervoice: ["5L36"], - memento: ["5E"], - naturepower: ["5E"], - protect: ["5M"], - quash: ["5M"], - raindance: ["5M"], - rapidspin: ["5E"], - rest: ["5M", "5L53"], - retaliate: ["5M", "5L57"], - return: ["5M"], - roar: ["5M", "5L23"], - rockslide: ["5M"], - rocksmash: ["5M", "5L18"], - rocktomb: ["5M"], - roost: ["5E"], - round: ["5M"], - safeguard: ["5M"], - stealthrock: ["5E"], - strength: ["5M"], - submission: ["5L32"], - substitute: ["5M"], - sunnyday: ["5M"], - superpower: ["5L45"], - swagger: ["5M"], - taunt: ["5M", "5L49"], - thief: ["5M"], - toxic: ["5M"], - workup: ["5M", "5L27"], - yawn: ["5E"], + aerialace: ["7M", "5M"], + attract: ["7M", "5M"], + batonpass: ["7E", "5E"], + brickbreak: ["7M", "7E", "5M"], + bulkup: ["7M", "7L40", "5M", "5L40"], + bulldoze: ["7M", "5M"], + confide: ["7M"], + confuseray: ["7M", "7E", "5E"], + doubleteam: ["7M", "5M"], + echoedvoice: ["7M", "5M"], + facade: ["7M", "5M"], + falseswipe: ["7M", "5M"], + flash: ["6M", "5M"], + fling: ["7M", "5M"], + focusblast: ["7M", "5M"], + focusenergy: ["7L13", "7E", "5L13", "5E"], + frustration: ["7M", "5M"], + furyswipes: ["7L18", "5L1"], + grassknot: ["7M", "5M"], + harden: ["7L4", "5L9"], + haze: ["7E", "5E"], + helpinghand: ["7T"], + hiddenpower: ["7M", "5M"], + hypervoice: ["7L36", "5L36"], + irontail: ["7T"], + memento: ["7E", "5E"], + naturepower: ["7M", "7E", "5E"], + poweruppunch: ["6L99"], + protect: ["7M", "5M"], + quash: ["7M", "5M"], + raindance: ["7M", "5M"], + rapidspin: ["7E", "5E"], + rest: ["7M", "7L53", "5M", "5L53"], + retaliate: ["7L57", "6M", "5M", "5L57"], + return: ["7M", "5M"], + roar: ["7M", "7L23", "5M", "5L23"], + rockslide: ["7M", "5M"], + rocksmash: ["7T", "7L9", "6M", "5M", "5L18"], + rocktomb: ["7M", "5M"], + roost: ["7M", "7E", "5E"], + round: ["7M", "5M"], + safeguard: ["7M", "5M"], + scratch: ["7L1"], + secretpower: ["7M"], + snore: ["7T"], + stealthrock: ["7T", "7E", "5E"], + strength: ["6M", "5M"], + submission: ["7L32", "5L32"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "5M"], + superpower: ["7L45", "5L45"], + swagger: ["7M", "5M"], + taunt: ["7M", "7L49", "5M", "5L49"], + thief: ["7M", "5M"], + toxic: ["7M", "5M"], + workup: ["7M", "7L27", "5M", "5L27"], + yawn: ["7E", "5E"], }}, necturine: {learnset: { - attract: ["5M"], - calmmind: ["5M"], - curse: ["5E"], - cut: ["5M"], - doubleteam: ["5M"], - dreameater: ["5M"], - energyball: ["5M"], - facade: ["5M"], - flash: ["5M"], - frustration: ["5M"], - futuresight: ["5E"], - gigadrain: ["5E"], - grassknot: ["5M"], - gravity: ["5E"], - hex: ["5L25"], - hiddenpower: ["5M"], - ingrain: ["5E"], - leafblade: ["5E"], - leafstorm: ["5E"], - leer: ["5L1"], - naturalgift: ["5L31", "5E"], - nightmare: ["5E"], - ominouswind: ["5L7"], - painsplit: ["5L37"], - payback: ["5M"], - powerwhip: ["5L50"], - protect: ["5M"], - psychic: ["5M"], - psychup: ["5M"], - rest: ["5M"], - return: ["5M"], - round: ["5M"], - shadowball: ["5M", "5L44"], - shadowsneak: ["5L13"], - sketch: ["5E"], - solarbeam: ["5M"], - substitute: ["5M"], - sunnyday: ["5M"], - swagger: ["5M"], - telekinesis: ["5M"], - thief: ["5M"], - torment: ["5M"], - toxic: ["5M"], - toxicspikes: ["5L19"], - vinewhip: ["5L1"], - willowisp: ["5M", "5L19"], + attract: ["7M", "5M"], + calmmind: ["7M", "5M"], + confide: ["7M"], + curse: ["7E", "5E"], + cut: ["7M", "5M"], + darkpulse: ["7M"], + doubleteam: ["7M", "5M"], + dreameater: ["7M", "5M"], + energyball: ["7M", "5M"], + facade: ["7M", "5M"], + flash: ["6M", "5M"], + frustration: ["7M", "5M"], + futuresight: ["7E", "5E"], + gigadrain: ["7E", "6T", "5E"], + grassknot: ["7M", "5M"], + grassyterrain: ["7L22"], + gravity: ["7E", "6T", "5E"], + hex: ["7L18", "5L25"], + hiddenpower: ["7M", "5M"], + ingrain: ["7E", "5E"], + leafblade: ["7E", "5E"], + leafstorm: ["7E", "5E"], + leechlife: ["7M"], + leechseed: ["7L8"], + leer: ["7L1", "5L1"], + magicalleaf: ["7L11"], + naturalgift: ["7E", "5L31", "5E"], + naturepower: ["7M"], + nightmare: ["7E", "5E"], + nightshade: ["7L26"], + ominouswind: ["7E", "5L7"], + painsplit: ["7L34", "6T", "5L37"], + payback: ["7M", "5M"], + phantomforce: ["7L56"], + powerwhip: ["7L50", "5L50"], + protect: ["7M", "5M"], + psychic: ["7M", "5M"], + psychup: ["7M", "5M"], + rest: ["7M", "5M"], + return: ["7M", "5M"], + round: ["7M", "5M"], + secretpower: ["7M"], + seedbomb: ["7T", "7L39"], + shadowball: ["7M", "7L43", "5M", "5L44"], + shadowsneak: ["7L4", "5L13"], + sketch: ["7E", "5E"], + sleeptalk: ["7M"], + snore: ["7T"], + solarbeam: ["7M", "5M"], + spite: ["7T"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "5M"], + swagger: ["7M", "5M"], + telekinesis: ["6M", "5M"], + thief: ["7M", "5M"], + torment: ["7M", "5M"], + toxic: ["7M", "5M"], + toxicspikes: ["7L23", "5L19"], + vinewhip: ["7L1", "5L1"], + willowisp: ["7M", "7L15", "5M", "5L19"], + worryseed: ["7T"], }}, cupra: {learnset: { allyswitch: ["5M"], - ancientpower: ["5L44"], - attract: ["5M"], - bugbite: ["5T", "5L7"], - bugbuzz: ["5E"], - closecombat: ["5E"], - counter: ["5E"], - cut: ["5M"], - disable: ["5E"], - doubleteam: ["5M"], - dreameater: ["5M"], - echoedvoice: ["5M"], - electroweb: ["5T"], - facade: ["5M"], - feint: ["5E"], - finalgambit: ["5L38"], - flash: ["5M"], - fling: ["5M"], - frustration: ["5M"], - hail: ["5M"], - healpulse: ["5L21"], - helpinghand: ["5T"], - hiddenpower: ["5M"], - hydropump: ["5E"], - lightscreen: ["5M"], - magiccoat: ["5T"], - magicroom: ["5T"], - megahorn: ["5E"], - protect: ["5M"], - psychic: ["5M"], - psychup: ["5M"], - psyshock: ["5M"], - raindance: ["5M"], - recycle: ["5T"], - reflect: ["5M"], - rest: ["5M"], + ancientpower: ["7L44", "5L44"], + attract: ["7M", "5M"], + bugbite: ["7T", "7L7", "5T", "5L7"], + bugbuzz: ["7E", "5E"], + closecombat: ["7E", "5E"], + confide: ["7M"], + counter: ["7E", "5E"], + cut: ["6M", "5M"], + disable: ["7E", "5E"], + doubleteam: ["7M", "5M"], + dreameater: ["7M", "5M"], + echoedvoice: ["7M", "5M"], + electroweb: ["7T", "5T"], + facade: ["7M", "5M"], + feint: ["7E", "5E"], + finalgambit: ["7L38", "5L38"], + flash: ["6M", "5M"], + fling: ["7M", "5M"], + frustration: ["7M", "5M"], + hail: ["7M", "5M"], + healpulse: ["7L21", "5L21"], + helpinghand: ["7T", "5T"], + hiddenpower: ["7M", "5M"], + hydropump: ["7E", "5E"], + icywind: ["7T", "5T"], + infestation: ["7M"], + lightscreen: ["7M", "5M"], + magiccoat: ["7T", "5T"], + magicroom: ["7T", "5T"], + megahorn: ["7E", "5E"], + protect: ["7M", "5M"], + psychic: ["7M", "5M"], + psychup: ["7M", "5M"], + psyshock: ["7M", "5M"], + raindance: ["7M", "5M"], + recycle: ["7T", "5T"], + reflect: ["7M", "5M"], + rest: ["7M", "5M"], retaliate: ["5M"], - return: ["5M"], - roleplay: ["5T"], - round: ["5M"], - safeguard: ["5M", "5E"], - shadowball: ["5M"], - skillswap: ["5T"], - stringshot: ["5L1"], - strugglebug: ["5M", "5L27"], - substitute: ["5M"], - sunnyday: ["5M", "5L14"], - swagger: ["5M"], - tackle: ["5L1"], + return: ["7M", "5M"], + roleplay: ["7T", "5T"], + round: ["7M", "5M"], + safeguard: ["7M", "7E", "5M", "5E"], + secretpower: ["7M"], + shadowball: ["7M", "5M"], + shockwave: ["7M"], + signalbeam: ["7T"], + skillswap: ["7T", "5T"], + sleeptalk: ["7M"], + snore: ["7T"], + steelwing: ["7M"], + stringshot: ["7L1", "5L1"], + strugglebug: ["7L27", "5M"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "7L14", "5M", "5L14"], + swagger: ["7M", "5M"], + tackle: ["7L1", "5L1"], + tailglow: ["7L60"], telekinesis: ["5M"], - toxic: ["5M"], - trick: ["5T"], - willowisp: ["5M", "5L32"], - wingattack: ["5E"], - wish: ["5L48"], - wonderroom: ["5T"], - xscissor: ["5M"], - zenheadbutt: ["5T", "5L54"], + toxic: ["7M", "5M"], + trick: ["7T", "5T"], + waterpulse: ["7T"], + willowisp: ["7M", "7L32", "5M", "5L32"], + wingattack: ["7E", "5E"], + wish: ["7L48", "5L48"], + wonderroom: ["7T", "5T"], + xscissor: ["7M", "5M"], + zenheadbutt: ["7T", "5L54", "5T", "5L54"], }}, argalis: {learnset: { allyswitch: ["5M"], - ancientpower: ["5L47"], - attract: ["5M"], - bugbite: ["5L7"], - bugbuzz: ["5E"], - closecombat: ["5E"], - counter: ["5E"], - cut: ["5M"], - disable: ["5E"], - doubleteam: ["5M"], - dreameater: ["5M"], - echoedvoice: ["5M"], - electroweb: ["5T"], - facade: ["5M"], - feint: ["5E"], - finalgambit: ["5L41"], - flash: ["5M"], - fling: ["5M"], - focusblast: ["5M"], - frustration: ["5M"], - hail: ["5M"], - healpulse: ["5L21"], - helpinghand: ["5T"], - hiddenpower: ["5M"], - hydropump: ["5E"], - icebeam: ["5M"], - icywind: ["5T"], - lightscreen: ["5L57"], - magiccoat: ["5T"], - magicroom: ["5T"], - megahorn: ["5E"], - ominouswind: ["5L27"], - protect: ["5M"], - psychic: ["5M", "5L62"], - psychup: ["5M"], - psyshock: ["5M"], - raindance: ["5M"], - recycle: ["5T"], - reflect: ["5L57"], - rest: ["5M"], + ancientpower: ["7L47", "5L47"], + attract: ["7M", "5M"], + bugbite: ["7T", "7L7", "5T", "5L7"], + bugbuzz: ["7E", "5E"], + closecombat: ["7E", "5E"], + confide: ["7M"], + counter: ["7E", "5E"], + cut: ["6M", "5M"], + disable: ["7E", "5E"], + doubleteam: ["7M", "5M"], + dreameater: ["7M", "5M"], + echoedvoice: ["7M", "5M"], + electroweb: ["7T", "5T"], + facade: ["7M", "5M"], + feint: ["7E", "5E"], + finalgambit: ["7L41", "5L41"], + flash: ["6M", "5M"], + fling: ["7M", "5M"], + focusblast: ["7M", "5M"], + frustration: ["7M", "5M"], + hail: ["7M", "5M"], + healpulse: ["7L21", "5L21"], + helpinghand: ["7T", "5T"], + hiddenpower: ["7M", "5M"], + hydropump: ["7E", "5E"], + icebeam: ["7M", "5M"], + icywind: ["7T", "5T"], + infestation: ["7M"], + lightscreen: ["7M", "7L57", "5M"], + magiccoat: ["7T", "5T"], + magicroom: ["7T", "5T"], + megahorn: ["7E", "5E"], + ominouswind: ["7L27", "5L27"], + protect: ["7M", "5M"], + psychic: ["7M", "7L62", "5M"], + psychup: ["7M", "5M"], + psyshock: ["7M", "5M"], + raindance: ["7M", "5M"], + recycle: ["7T", "5T"], + reflect: ["7M", "7L57", "5M"], + rest: ["7M", "5M"], retaliate: ["5M"], - return: ["5M"], - roleplay: ["5T"], - round: ["5M"], - safeguard: ["5M", "5E"], - shadowball: ["5M"], - skillswap: ["5T"], - stringshot: ["5L1"], + return: ["7M", "5M"], + roleplay: ["7T", "5T"], + round: ["7M", "5M"], + safeguard: ["7M", "7E", "5M", "5E"], + secretpower: ["7M"], + shadowball: ["7M", "5M"], + shockwave: ["7M"], + signalbeam: ["7T"], + skillswap: ["7T", "5T"], + sleeptalk: ["7M"], + snore: ["7T"], + solarbeam: ["7M", "5M"], + spotlight: ["7L1"], + steelwing: ["7M"], + stringshot: ["7L1", "5L1"], strugglebug: ["5M"], - substitute: ["5M"], - sunnyday: ["5L14"], - swagger: ["5M"], - tackle: ["5L1"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "7L14", "5M", "5L14"], + surf: ["7M", "5M"], + swagger: ["7M", "5M"], + tackle: ["7L1", "5L1"], + tailglow: ["7L65"], telekinesis: ["5M"], - thunderbolt: ["5M"], - toxic: ["5M"], - trick: ["5T"], - willowisp: ["5L34"], - wingattack: ["5E"], - wish: ["5L52"], - wonderroom: ["5T"], - xscissor: ["5M"], - zenheadbutt: ["5T"], + thunderbolt: ["7M", "5M"], + toxic: ["7M", "5M"], + trick: ["7T", "5T"], + waterpulse: ["7T"], + willowisp: ["7M", "7L34", "5M", "5L34"], + wingattack: ["7E", "5E"], + wish: ["7L54", "5L54"], + wonderroom: ["7T", "5T"], + xscissor: ["7M", "5M"], + zenheadbutt: ["7T", "5T"], }}, brattler: {learnset: { - aromatherapy: ["5E"], - attract: ["5M"], - beatup: ["5E"], - bind: ["5T"], - crunch: ["5L39"], - cut: ["5M"], - darkpulse: ["5T"], - doubleteam: ["5M"], - dragontail: ["5M"], - energyball: ["5M"], - facade: ["5M"], - foulplay: ["5T"], - frustration: ["5M"], - gigadrain: ["5T"], - glare: ["5E"], - grassknot: ["5L18"], - haze: ["5E"], - healbell: ["5T"], - hiddenpower: ["5M"], - irontail: ["5T"], - knockoff: ["5T"], - leafblade: ["5L34"], - nightslash: ["5E"], - payback: ["5M"], - poisonpowder: ["5E"], - powerwhip: ["5L50"], - protect: ["5M"], - punishment: ["5L55"], - pursuit: ["5L6", "5L1"], - rest: ["5M"], - retaliate: ["5M"], - return: ["5M"], - roar: ["5M"], - round: ["5M"], - scaryface: ["5E"], - screech: ["5E"], - seedbomb: ["5T"], - slam: ["5L30"], - sleeptalk: ["5T"], - snarl: ["5M"], - snore: ["5T"], - solarbeam: ["5M"], - spite: ["5T"], - strength: ["5M"], - stunspore: ["5E"], - substitute: ["5M"], - suckerpunch: ["5L25"], - sunnyday: ["5M"], - swagger: ["5M"], - sweetscent: ["5E"], - synthesis: ["5T"], - taunt: ["5M"], - thief: ["5M"], - toxic: ["5M"], - uturn: ["5M"], - vinewhip: ["5L1"], - wildcharge: ["5M"], - worryseed: ["5T"], - wrap: ["5L12", "5L1"], - wringout: ["5L44"], + aromatherapy: ["7E", "5E"], + attract: ["7M", "5M"], + beatup: ["7E", "5E"], + bind: ["7T", "5T"], + brutalswing: ["7M"], + confide: ["7M"], + crunch: ["7L43", "5L39"], + cut: ["6M", "5M"], + darkpulse: ["7M", "5T"], + doubleteam: ["7M", "5M"], + dragontail: ["7M", "5M"], + energyball: ["7M", "5M"], + facade: ["7M", "5M"], + feint: ["7E"], + foulplay: ["7T", "5T"], + frustration: ["7M", "5M"], + gigadrain: ["7T", "5T"], + glare: ["7L1", "7L31", "7E", "5E"], + grassknot: ["7M", "7L15", "5M", "5L18"], + haze: ["7E", "5E"], + healbell: ["7T", "5T"], + hiddenpower: ["7M", "5M"], + icefang: ["7E"], + irontail: ["7T", "5T"], + knockoff: ["7T", "5T"], + leafblade: ["7L26", "5L34"], + leer: ["7L1"], + naturepower: ["7M"], + nightslash: ["7E", "5E"], + payback: ["7M", "5M"], + poisonpowder: ["7E", "5E"], + powerwhip: ["7L44", "5L50"], + protect: ["7M", "5M"], + punishment: ["7L55", "5L55"], + pursuit: ["7L8", "5L6", "5L1"], + recycle: ["7T"], + rest: ["7M", "5M"], + retaliate: ["7M", "5M"], + return: ["7M", "5M"], + roar: ["7M", "5M"], + round: ["7M", "5M"], + scaryface: ["7L11", "7E", "5E"], + screech: ["7E", "5E"], + secretpower: ["7M"], + seedbomb: ["7T", "5T"], + slam: ["7L22", "5L30"], + sleeptalk: ["7M", "5T"], + snarl: ["7M", "5M"], + snore: ["7T", "5T"], + solarbeam: ["7M", "5M"], + spikyshield: ["7L40"], + spite: ["7T", "5T"], + strength: ["6M", "5M"], + stunspore: ["7E", "5E"], + substitute: ["7M", "5M"], + suckerpunch: ["7L18", "5L25"], + sunnyday: ["7M", "5M"], + swagger: ["7M", "5M"], + sweetscent: ["7E", "5E"], + synthesis: ["7T", "5T"], + taunt: ["7M", "5M"], + thief: ["7M", "5M"], + thunderfang: ["7E"], + toxic: ["7M", "5M"], + uturn: ["7M", "5M"], + vinewhip: ["7L5", "5L1"], + wildcharge: ["7M", "5M"], + worryseed: ["7T", "5T"], + wrap: ["7L1", "5L1"], + wringout: ["7L49", "5L44"], }}, cawdet: {learnset: { acrobatics: ["5M"], @@ -64432,6 +64539,58 @@ let BattleLearnsets = { workup: ["7M"], wrap: ["7L1", "4L1"], }}, + mumbao: {learnset: { + attract: ["7M"], + bodyslam: ["7L28"], + confide: ["7M"], + dazzlinggleam: ["7M"], + doubleteam: ["7M"], + energyball: ["7M", "7L37"], + facade: ["7M"], + flowershield: ["7L1"], + focusblast: ["7M"], + frustration: ["7M"], + gigadrain: ["7T"], + grassknot: ["7M"], + grassyterrain: ["7E"], + gravity: ["7T"], + gyroball: ["7M"], + healingwish: ["7E"], + helpinghand: ["7T", "7L35"], + hiddenpower: ["7M"], + hyperbeam: ["7M"], + ingrain: ["7L10"], + leafstorm: ["7L46"], + leafage: ["7L13"], + lightscreen: ["7M"], + luckychant: ["7L17"], + magiccoat: ["7T"], + magicalleaf: ["7L19"], + mistyterrain: ["7E"], + moonblast: ["7L44"], + naturalgift: ["7L31"], + protect: ["7M"], + psychup: ["7M", "7L26"], + rest: ["7M"], + return: ["7M"], + rototiller: ["7L8"], + round: ["7M"], + safeguard: ["7M"], + sandstorm: ["7M"], + seedbomb: ["7T"], + shadowball: ["7M"], + sleeptalk: ["7M"], + snore: ["7T"], + solarbeam: ["7M"], + substitute: ["7M"], + sunnyday: ["7M", "7L40"], + swagger: ["7M"], + synthesis: ["7T"], + tackle: ["7L4"], + toxic: ["7M"], + wish: ["7L22"], + worryseed: ["7T"], + }}, }; exports.BattleLearnsets = BattleLearnsets; diff --git a/data/moves.js b/data/moves.js index a24956e00afa..99f0f5e10f81 100644 --- a/data/moves.js +++ b/data/moves.js @@ -948,8 +948,11 @@ let BattleMovedex = { accuracy: 100, basePower: 60, basePowerCallback: function (pokemon, target, move) { - if (target.lastDamage > 0 && pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.pokemon === target) { - this.debug('Boosted for getting hit by ' + pokemon.lastAttackedBy.move); + let hurtByTarget = pokemon.hurtBy.find(function (x) { + return x.source === target && x.damage > 0 && x.thisTurn; + }); + if (hurtByTarget) { + this.debug('Boosted for getting hit by ' + hurtByTarget.move); return move.basePower * 2; } return move.basePower; @@ -995,7 +998,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user become poisoned. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user become poisoned. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Protects from moves. Contact: poison.", id: "banefulbunker", isViable: true, @@ -2577,7 +2580,7 @@ let BattleMovedex = { let possibleTypes = []; let attackType = target.lastMove.type; for (let type in this.data.TypeChart) { - if (source.hasType(type) || target.hasType(type)) continue; + if (source.hasType(type)) continue; let typeCheck = this.data.TypeChart[type].damageTaken[attackType]; if (typeCheck === 2 || typeCheck === 3) { possibleTypes.push(type); @@ -3274,7 +3277,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Prevents moves from affecting the user this turn.", id: "detect", isViable: true, @@ -4483,7 +4486,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "User survives attacks this turn with at least 1 HP.", id: "endure", name: "Endure", @@ -8831,7 +8834,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Protects from attacks. Contact: lowers Atk by 2.", id: "kingsshield", isViable: true, @@ -8955,6 +8958,10 @@ let BattleMovedex = { onStart: function (pokemon) { this.add('-start', pokemon, 'move: Laser Focus'); }, + onRestart: function (pokemon) { + this.effectData.duration = 2; + this.add('-start', pokemon, 'move: Laser Focus'); + }, onModifyCritRatio: function (critRatio) { return 5; }, @@ -10344,7 +10351,7 @@ let BattleMovedex = { accuracy: 100, basePower: 150, category: "Special", - desc: "Whether or not this move is successful, the user loses 1/2 of its maximum HP, rounded up, unless the user has the Magic Guard Ability. This move is prevented from executing and the user does not lose HP if any active Pokemon has the Damp Ability, or if this move is Fire type and the user is affected by Powder or the weather is Primordial Sea.", + desc: "Whether or not this move is successful and even if it would cause fainting, the user loses 1/2 of its maximum HP, rounded up, unless the user has the Magic Guard Ability. This move is prevented from executing and the user does not lose HP if any active Pokemon has the Damp Ability, or if this move is Fire type and the user is affected by Powder or the weather is Primordial Sea.", shortDesc: "User loses 50% max HP. Hits adjacent Pokemon.", id: "mindblown", isViable: true, @@ -10394,7 +10401,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "Raises the user's evasiveness by 2 stages. Whether or not the user's evasiveness was changed, Body Slam, Dragon Rush, Flying Press, Heat Crash, Heavy Slam, Malicious Moonsault, Phantom Force, Shadow Force, Steamroller, and Stomp will not check accuracy and have their damage doubled if used against the user while it is active.", + desc: "Raises the user's evasiveness by 2 stages. Whether or not the user's evasiveness was changed, Body Slam, Dragon Rush, Flying Press, Heat Crash, Heavy Slam, Malicious Moonsault, Steamroller, and Stomp will not check accuracy and have their damage doubled if used against the user while it is active.", shortDesc: "Raises the user's evasiveness by 2.", id: "minimize", name: "Minimize", @@ -10405,12 +10412,12 @@ let BattleMovedex = { effect: { noCopy: true, onSourceModifyDamage: function (damage, source, target, move) { - if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'phantomforce', 'heatcrash', 'shadowforce', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { + if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'heatcrash', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { return this.chainModify(2); } }, onAccuracy: function (accuracy, target, source, move) { - if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'phantomforce', 'heatcrash', 'shadowforce', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { + if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'heatcrash', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { return true; } return accuracy; @@ -10515,6 +10522,7 @@ let BattleMovedex = { basePower: 0, category: "Status", desc: "The user uses the last move used by the target. The copied move is used against that target, if possible. Fails if the target has not made a move, or if the last move used cannot be copied by this move.", + shortDesc: "User uses the target's last used move against it.", id: "mirrormove", name: "Mirror Move", pp: 20, @@ -11655,7 +11663,7 @@ let BattleMovedex = { accuracy: 100, basePower: 90, category: "Physical", - desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn. Damage doubles and no accuracy check is done if the target has used Minimize while active.", + desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn.", shortDesc: "Disappears turn 1. Hits turn 2. Breaks protection.", id: "phantomforce", name: "Phantom Force", @@ -12310,7 +12318,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Prevents moves from affecting the user this turn.", id: "protect", isViable: true, @@ -12829,7 +12837,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", shortDesc: "Protects allies from priority attacks this turn.", id: "quickguard", name: "Quick Guard", @@ -13388,8 +13396,11 @@ let BattleMovedex = { accuracy: 100, basePower: 60, basePowerCallback: function (pokemon, target, move) { - if (target.lastDamage > 0 && pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.pokemon === target) { - this.debug('Boosted for getting hit by ' + pokemon.lastAttackedBy.move); + let hurtByTarget = pokemon.hurtBy.find(function (x) { + return x.source === target && x.damage > 0 && x.thisTurn; + }); + if (hurtByTarget) { + this.debug('Boosted for getting hit by ' + hurtByTarget.move); return move.basePower * 2; } return move.basePower; @@ -14384,7 +14395,7 @@ let BattleMovedex = { accuracy: 100, basePower: 120, category: "Physical", - desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn. Damage doubles and no accuracy check is done if the target has used Minimize while active.", + desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn.", shortDesc: "Disappears turn 1. Hits turn 2. Breaks protection.", id: "shadowforce", isViable: true, @@ -14946,7 +14957,6 @@ let BattleMovedex = { if (target.hasType('Flying')) { this.add('-immune', target, '[msg]'); - this.add('-end', target, 'Sky Drop'); return null; } } else { @@ -15498,7 +15508,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Protects from moves. Contact: loses 1/8 max HP.", id: "spikyshield", isViable: true, @@ -18677,7 +18687,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user and its party members are protected from moves made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from moves made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", shortDesc: "Protects allies from multi-target moves this turn.", id: "wideguard", name: "Wide Guard", diff --git a/data/pokedex.js b/data/pokedex.js index 8ecc8b408209..a4189c4bb980 100644 --- a/data/pokedex.js +++ b/data/pokedex.js @@ -13408,7 +13408,7 @@ let BattlePokedex = { color: "Brown", prevo: "voodoll", evoLevel: 32, - eggGroups: ["Human-Like", "Ground"], + eggGroups: ["Human-Like", "Field"], }, tomohawk: { num: -12, @@ -13587,6 +13587,8 @@ let BattlePokedex = { heightm: 2.4, weightkg: 600, color: "Brown", + prevo: "mumbao", + evoLevel: 2, eggGroups: ["Grass"], }, syclar: { @@ -13594,7 +13596,7 @@ let BattlePokedex = { species: "Syclar", types: ["Ice", "Bug"], baseStats: {hp: 40, atk: 76, def: 45, spa: 74, spd: 39, spe: 91}, - abilities: {0: "Compound Eyes", 1: "Snow Cloak"}, + abilities: {0: "Compound Eyes", 1: "Snow Cloak", H: "Ice Body"}, heightm: 0.2, weightkg: 4.0, color: "Blue", @@ -13606,7 +13608,7 @@ let BattlePokedex = { species: "Embirch", types: ["Fire", "Grass"], baseStats: {hp: 60, atk: 40, def: 55, spa: 65, spd: 40, spe: 60}, - abilities: {0: "Reckless", 1: "Leaf Guard"}, + abilities: {0: "Reckless", 1: "Leaf Guard", H: "Chlorophyll"}, heightm: 0.6, weightkg: 15, color: "Brown", @@ -13618,7 +13620,7 @@ let BattlePokedex = { species: "Flarelm", types: ["Fire", "Grass"], baseStats: {hp: 90, atk: 50, def: 95, spa: 75, spd: 70, spe: 40}, - abilities: {0: "Rock Head", 1: "Battle Armor"}, + abilities: {0: "Rock Head", 1: "Battle Armor", H: "White Smoke"}, heightm: 1.4, weightkg: 73, color: "Brown", @@ -13632,7 +13634,7 @@ let BattlePokedex = { species: "Breezi", types: ["Poison", "Flying"], baseStats: {hp: 50, atk: 46, def: 69, spa: 60, spd: 50, spe: 75}, - abilities: {0: "Unburden", 1: "Own Tempo"}, + abilities: {0: "Unburden", 1: "Own Tempo", H: "Frisk"}, heightm: 0.4, weightkg: 0.6, color: "Purple", @@ -13669,7 +13671,7 @@ let BattlePokedex = { species: "Cupra", types: ["Bug", "Psychic"], baseStats: {hp: 50, atk: 60, def: 49, spa: 67, spd: 30, spe: 44}, - abilities: {0: "Shield Dust", 1: "Keen Eye"}, + abilities: {0: "Shield Dust", 1: "Keen Eye", H: "Magic Guard"}, heightm: 0.5, weightkg: 4.8, color: "Brown", @@ -13681,7 +13683,7 @@ let BattlePokedex = { species: "Argalis", types: ["Bug", "Psychic"], baseStats: {hp: 60, atk: 90, def: 89, spa: 87, spd: 40, spe: 54}, - abilities: {0: "Shed Skin", 1: "Compound Eyes"}, + abilities: {0: "Shed Skin", 1: "Compound Eyes", H: "Overcoat"}, heightm: 1.3, weightkg: 341.4, color: "Gray", @@ -13695,7 +13697,7 @@ let BattlePokedex = { species: "Brattler", types: ["Dark", "Grass"], baseStats: {hp: 80, atk: 70, def: 40, spa: 20, spd: 90, spe: 30}, - abilities: {0: "Harvest", 1: "Infiltrator"}, + abilities: {0: "Harvest", 1: "Infiltrator", H: "Rattled"}, heightm: 1.8, weightkg: 11.5, color: "Brown", @@ -13826,7 +13828,19 @@ let BattlePokedex = { weightkg: 25, color: "Brown", evos: ["voodoom"], - eggGroups: ["Human-Like", "Ground"], + eggGroups: ["Human-Like", "Field"], + }, + mumbao: { + num: -120, + species: "Mumbao", + types: ["Grass", "Fairy"], + baseStats: {hp: 55, atk: 30, def: 64, spa: 87, spd: 73, spe: 66}, + abilities: {0: "Solar Power", 1: "Trace", H: "Overcoat"}, + heightm: 1, + weightkg: 250, + color: "Brown", + evos: ["jumbao"], + eggGroups: ["Grass"], }, pokestarsmeargle: { num: -5000, diff --git a/data/random-teams.js b/data/random-teams.js index 8dbface89f10..fa24332a5e48 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -678,8 +678,8 @@ class RandomTeams extends Dex.ModdedDex { switch (moveid) { // Not very useful without their supporting moves - case 'clangingscales': - if (teamDetails.zMove) rejected = true; + case 'clangingscales': case 'happyhour': + if (teamDetails.zMove || hasMove['rest']) rejected = true; break; case 'cottonguard': case 'defendorder': if (!counter['recovery'] && !hasMove['rest']) rejected = true; @@ -751,7 +751,7 @@ class RandomTeams extends Dex.ModdedDex { if (!!counter['speedsetup'] || hasMove['encore'] || hasMove['raindance'] || hasMove['roar'] || hasMove['whirlwind']) rejected = true; break; case 'defog': - if (counter.setupType || hasMove['spikes'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; + if (counter.setupType || hasMove['spikes'] || hasMove['stealthrock'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; break; case 'fakeout': if (counter.setupType || hasMove['substitute'] || hasMove['switcheroo'] || hasMove['trick']) rejected = true; @@ -1307,6 +1307,8 @@ class RandomTeams extends Dex.ModdedDex { ability = 'Klutz'; } else if ((template.species === 'Rampardos' && !hasMove['headsmash']) || hasMove['rockclimb']) { ability = 'Sheer Force'; + } else if (template.species === 'Torterra' && !counter['Grass']) { + ability = 'Shell Armor'; } else if (template.species === 'Umbreon') { ability = 'Synchronize'; } else if (template.id === 'venusaurmega') { @@ -1318,7 +1320,7 @@ class RandomTeams extends Dex.ModdedDex { item = !isDoubles ? 'Leftovers' : 'Sitrus Berry'; if (template.requiredItems) { - if (template.baseSpecies === 'Arceus' && hasMove['judgment']) { + if (template.baseSpecies === 'Arceus' && (hasMove['judgment'] || !counter[template.types[0]])) { // Judgment doesn't change type with Z-Crystals item = template.requiredItems[0]; } else { @@ -1347,6 +1349,8 @@ class RandomTeams extends Dex.ModdedDex { item = 'Lycanium Z'; } else if (template.species === 'Marshadow' && hasMove['spectralthief'] && counter.setupType && !teamDetails.zMove) { item = 'Marshadium Z'; + } else if (template.species === 'Mimikyu' && hasMove['playrough'] && counter.setupType && !teamDetails.zMove) { + item = 'Mimikium Z'; } else if ((template.species === 'Necrozma-Dusk-Mane' || template.species === 'Necrozma-Dawn-Wings') && !teamDetails.zMove) { if (hasMove['autotomize'] && hasMove['sunsteelstrike']) { item = 'Solganium Z'; @@ -1394,13 +1398,13 @@ class RandomTeams extends Dex.ModdedDex { } else { item = (counter.Physical > counter.Special) ? 'Choice Band' : 'Choice Specs'; } - } else if (hasMove['conversion']) { + } else if (hasMove['conversion'] || hasMove['happyhour']) { item = 'Normalium Z'; } else if (hasMove['dig'] && !teamDetails.zMove) { item = 'Groundium Z'; } else if (hasMove['mindblown'] && !!counter['Status'] && !teamDetails.zMove) { item = 'Firium Z'; - } else if (!teamDetails.zMove && hasMove['fly'] || ((hasMove['bounce'] || (hasAbility['Gale Wings'] && hasMove['bravebird'])) && counter.setupType)) { + } else if (!teamDetails.zMove && (hasMove['fly'] || ((hasMove['bounce'] || (hasAbility['Gale Wings'] && hasMove['bravebird'])) && counter.setupType))) { item = 'Flyinium Z'; } else if (hasMove['solarbeam'] && !hasAbility['Drought'] && !hasMove['sunnyday'] && !teamDetails['sun']) { item = !teamDetails.zMove ? 'Grassium Z' : 'Power Herb'; @@ -1640,8 +1644,8 @@ class RandomTeams extends Dex.ModdedDex { randomTeam() { let pokemon = []; - let excludedTiers = ['NFE', 'LC Uber', 'LC']; - let allowedNFE = ['Chansey', 'Doublade', 'Gligar', 'Porygon2', 'Scyther', 'Togetic']; + const excludedTiers = ['NFE', 'LC Uber', 'LC']; + const allowedNFE = ['Chansey', 'Doublade', 'Gligar', 'Porygon2', 'Scyther', 'Togetic']; // For Monotype let isMonotype = this.format.id === 'gen7monotyperandombattle'; @@ -1770,6 +1774,13 @@ class RandomTeams extends Dex.ModdedDex { // Okay, the set passes, add it to our team pokemon.push(set); + if (pokemon.length === 6) { + // Set Zoroark's level to be the same as the last Pokemon + let illusion = teamDetails['illusion']; + if (illusion) pokemon[illusion - 1].level = pokemon[5].level; + break; + } + // Now that our Pokemon has passed all checks, we can increment our counters baseFormes[template.baseSpecies] = 1; @@ -1805,6 +1816,9 @@ class RandomTeams extends Dex.ModdedDex { if (set.moves.includes('stealthrock')) teamDetails['stealthRock'] = 1; if (set.moves.includes('toxicspikes')) teamDetails['toxicSpikes'] = 1; if (set.moves.includes('defog') || set.moves.includes('rapidspin')) teamDetails['hazardClear'] = 1; + + // For setting Zoroark's level + if (set.ability === 'Illusion') teamDetails['illusion'] = pokemon.length; } return pokemon; } diff --git a/data/rulesets.js b/data/rulesets.js index 56ba8484c255..ab4552523cc7 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -544,6 +544,15 @@ let BattleFormats = { this.add('rule', 'Evasion Moves Clause: Evasion moves are banned'); }, }, + accuracymovesclause: { + effectType: 'ValidatorRule', + name: 'Accuracy Moves Clause', + desc: "Bans moves that have a chance to lower the target's accuracy when used", + banlist: ['Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Sand Attack', 'Smokescreen'], + onStart: function () { + this.add('rule', 'Accuracy Moves Clause: Accuracy-lowering moves are banned'); + }, + }, endlessbattleclause: { effectType: 'Rule', name: 'Endless Battle Clause', @@ -841,12 +850,6 @@ let BattleFormats = { return this.checkLearnset(move, template, lsetData, set); }, }, - allowonesketch: { - effectType: 'ValidatorRule', - name: 'Allow One Sketch', - desc: "Allows each Pokémon to use one move they don't normally have access to via Sketch", - // Implemented in team-validator.js - }, allowcap: { effectType: 'ValidatorRule', name: 'Allow CAP', diff --git a/data/scripts.js b/data/scripts.js index 0dffcf7d01f9..0fd8b929798a 100644 --- a/data/scripts.js +++ b/data/scripts.js @@ -271,7 +271,7 @@ let BattleScripts = { lacksTarget = !this.isAdjacent(target, pokemon); } } - if (lacksTarget && (!move.flags['charge'] || pokemon.volatiles['twoturnmove'])) { + if (lacksTarget && (!move.flags['charge'] || pokemon.volatiles['twoturnmove']) && !move.isFutureMove) { this.attrLastMove('[notarget]'); this.add('-notarget'); if (move.target === 'normal') pokemon.isStaleCon = 0; @@ -432,6 +432,7 @@ let BattleScripts = { } else { this.add('-activate', target, 'move: ' + move.name, '[broken]'); } + if (this.gen >= 6) delete target.volatiles['stall']; } } diff --git a/data/statuses.js b/data/statuses.js index 7bcba89063e3..103169740496 100644 --- a/data/statuses.js +++ b/data/statuses.js @@ -391,8 +391,8 @@ let BattleStatuses = { // time's up; time to hit! :D const move = this.getMove(posData.move); - if (target.fainted) { - this.add('-hint', '' + move.name + ' did not hit because the target is fainted.'); + if (target.fainted || target === posData.source) { + this.add('-hint', '' + move.name + ' did not hit because the target is ' + (target.fainted ? 'fainted' : 'the user') + '.'); this.effectData.positions[i] = null; continue; } diff --git a/dev-tools/globals.ts b/dev-tools/globals.ts index c3d4ce7150b3..f305361e5331 100644 --- a/dev-tools/globals.ts +++ b/dev-tools/globals.ts @@ -307,6 +307,7 @@ interface EffectData extends EventMethods { onAfterMoveSecondaryPriority?: number onAfterMoveSecondarySelfPriority?: number onAfterMoveSelfPriority?: number + onAnyFaintPriority?: number onAttractPriority?: number onBasePowerPriority?: number onBeforeMovePriority?: number @@ -873,6 +874,7 @@ interface RandomTeamsTypes { toxicSpikes?: number hazardClear?: number rapidSpin?: number + illusion?: number } FactoryTeamDetails: { megaCount: number diff --git a/mods/gen1/moves.js b/mods/gen1/moves.js index 030222389c78..a0a88fbd51dd 100644 --- a/mods/gen1/moves.js +++ b/mods/gen1/moves.js @@ -967,11 +967,11 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (!target.lastAttackedBy) { - target.lastAttackedBy = {pokemon: source, move: move.id, thisTurn: true, damage: damage}; + if (!target.lastHurtBy) { + target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { - target.lastAttackedBy.move = move.id; - target.lastAttackedBy.damage = damage; + target.lastHurtBy.move = move.id; + target.lastHurtBy.damage = damage; } return 0; }, diff --git a/mods/gen2/learnsets.js b/mods/gen2/learnsets.js index 4721ea41438a..3a137e088d38 100644 --- a/mods/gen2/learnsets.js +++ b/mods/gen2/learnsets.js @@ -9949,6 +9949,7 @@ let BattleLearnsets = { icepunch: ["2M"], leer: ["2E"], lick: ["2L19", "2E"], + lovelykiss: ["2S0"], metronome: ["2E"], mudslap: ["2M"], present: ["2E"], @@ -9968,7 +9969,6 @@ let BattleLearnsets = { strength: ["2M"], sunnyday: ["2M"], swagger: ["2M"], - sweetkiss: ["2S0"], tackle: ["2L1", "2S0"], tailwhip: ["2L4", "2S0"], takedown: ["2L43"], diff --git a/mods/gen2/moves.js b/mods/gen2/moves.js index 287529c3b533..e8c402634685 100644 --- a/mods/gen2/moves.js +++ b/mods/gen2/moves.js @@ -144,9 +144,9 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a physical attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a physical attack this turn, or if the user did not lose HP from the attack. If the opposing Pokemon used Fissure or Horn Drill and missed, this move deals 65535 damage.", damageCallback: function (pokemon, target) { - if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.move && (this.getCategory(pokemon.lastAttackedBy.move) === 'Physical' || this.getMove(pokemon.lastAttackedBy.move).id === 'hiddenpower') && - (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { - return 2 * pokemon.lastAttackedBy.damage; + if (!pokemon.lastHurtBy) return false; + if (pokemon.lastHurtBy.move && pokemon.lastHurtBy.thisTurn && (this.getCategory(pokemon.lastHurtBy.move) === 'Physical' || this.getMove(pokemon.lastHurtBy.move).id === 'hiddenpower') && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { + return 2 * pokemon.lastHurtBy.damage; } return false; }, @@ -493,9 +493,9 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a special attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a special attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon, target) { - if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.move && this.getCategory(pokemon.lastAttackedBy.move) === 'Special' && - this.getMove(pokemon.lastAttackedBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { - return 2 * pokemon.lastAttackedBy.damage; + if (!pokemon.lastHurtBy) return false; + if (pokemon.lastHurtBy.move && pokemon.lastHurtBy.thisTurn && this.getCategory(pokemon.lastHurtBy.move) === 'Special' && this.getMove(pokemon.lastHurtBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { + return 2 * pokemon.lastHurtBy.damage; } return false; }, diff --git a/mods/gen3/formats-data.js b/mods/gen3/formats-data.js index ab9cf6142612..4fd6b2766363 100644 --- a/mods/gen3/formats-data.js +++ b/mods/gen3/formats-data.js @@ -18,7 +18,7 @@ let BattleFormatsData = { tier: "LC", }, charmeleon: { - tier: "NU", + tier: "NFE", }, charizard: { inherit: true, @@ -30,7 +30,7 @@ let BattleFormatsData = { tier: "LC", }, wartortle: { - tier: "NU", + tier: "NFE", }, blastoise: { inherit: true, @@ -207,7 +207,7 @@ let BattleFormatsData = { tier: "NU", }, diglett: { - tier: "LC", + tier: "NU", }, dugtrio: { inherit: true, @@ -252,7 +252,7 @@ let BattleFormatsData = { tier: "LC", }, poliwhirl: { - tier: "NU", + tier: "NFE", }, poliwrath: { inherit: true, @@ -264,7 +264,7 @@ let BattleFormatsData = { tier: "UU", }, abra: { - tier: "LC", + tier: "NU", }, kadabra: { tier: "UUBL", @@ -278,7 +278,7 @@ let BattleFormatsData = { tier: "LC", }, machoke: { - tier: "NU", + tier: "NFE", }, machamp: { inherit: true, @@ -291,7 +291,7 @@ let BattleFormatsData = { }, weepinbell: { inherit: true, - tier: "NU", + tier: "NFE", }, victreebel: { randomBattleMoves: ["sleeppowder", "sludgebomb", "synthesis", "sunnyday", "solarbeam", "gigadrain", "hiddenpowerfire", "hiddenpowerground", "swordsdance"], @@ -660,7 +660,7 @@ let BattleFormatsData = { tier: "LC", }, dragonair: { - tier: "NU", + tier: "NFE", }, dragonite: { inherit: true, @@ -693,7 +693,7 @@ let BattleFormatsData = { tier: "LC", }, quilava: { - tier: "NU", + tier: "NFE", }, typhlosion: { inherit: true, @@ -1021,7 +1021,7 @@ let BattleFormatsData = { tier: "LC", }, grovyle: { - tier: "NU", + tier: "NFE", }, sceptile: { randomBattleMoves: ["leafblade", "dragonclaw", "hiddenpowerfire", "hiddenpowerice", "crunch", "thunderpunch", "leechseed", "substitute"], @@ -1219,7 +1219,7 @@ let BattleFormatsData = { tier: "LC", }, lairon: { - tier: "NU", + tier: "NFE", }, aggron: { inherit: true, @@ -1315,7 +1315,7 @@ let BattleFormatsData = { tier: "NU", }, trapinch: { - tier: "NU", + tier: "LC", }, vibrava: { tier: "NFE", @@ -1474,14 +1474,14 @@ let BattleFormatsData = { tier: "LC", }, sealeo: { - tier: "NU", + tier: "NFE", }, walrein: { randomBattleMoves: ["icebeam", "surf", "earthquake", "hiddenpowergrass", "toxic", "rest", "sleeptalk", "encore", "roar", "yawn"], tier: "UU", }, clamperl: { - tier: "NU", + tier: "LC", }, huntail: { randomBattleMoves: ["doubleedge", "surf", "hydropump", "icebeam", "crunch", "hiddenpowergrass", "raindance"], diff --git a/mods/gen3/moves.js b/mods/gen3/moves.js index 87b1144a1f76..6f747feebee7 100644 --- a/mods/gen3/moves.js +++ b/mods/gen3/moves.js @@ -187,9 +187,10 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the last opposing Pokemon to hit the user with a physical attack this turn equal to twice the HP lost by the user from that attack. If that opposing Pokemon's position is no longer in use and there is another opposing Pokemon on the field, the damage is done to it instead. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user was not hit by an opposing Pokemon's physical attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon) { - if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.move && (this.getCategory(pokemon.lastAttackedBy.move) === 'Physical' || this.getMove(pokemon.lastAttackedBy.move).id === 'hiddenpower')) { + if (!pokemon.lastHurtBy) return false; + if (pokemon.lastHurtBy.move && pokemon.lastHurtBy.thisTurn && (this.getCategory(pokemon.lastHurtBy.move) === 'Physical' || this.getMove(pokemon.lastHurtBy.move).id === 'hiddenpower')) { // @ts-ignore - return 2 * pokemon.lastAttackedBy.damage; + return 2 * pokemon.lastHurtBy.damage; } return false; }, @@ -557,10 +558,11 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['assist', 'curse', 'doomdesire', 'focuspunch', 'futuresight', 'magiccoat', 'metronome', 'mimic', 'mirrormove', 'naturepower', 'psychup', 'roleplay', 'sketch', 'sleeptalk', 'spikes', 'spitup', 'taunt', 'teeterdance', 'transform']; - if (!pokemon.lastAttackedBy || !pokemon.lastAttackedBy.pokemon.lastMove || !pokemon.lastAttackedBy.move || noMirror.includes(pokemon.lastAttackedBy.move) || !pokemon.lastAttackedBy.pokemon.hasMove(pokemon.lastAttackedBy.move)) { + if (!pokemon.lastHurtBy) return false; + if (!pokemon.lastHurtBy.source.lastMove || !pokemon.lastHurtBy.move || noMirror.includes(pokemon.lastHurtBy.move) || !pokemon.lastHurtBy.source.hasMove(pokemon.lastHurtBy.move)) { return false; } - this.useMove(pokemon.lastAttackedBy.move, pokemon); + this.useMove(pokemon.lastHurtBy.move, pokemon); }, target: "self", }, diff --git a/mods/gen4/moves.js b/mods/gen4/moves.js index 7091d45e0e62..b100f27c6a4d 100644 --- a/mods/gen4/moves.js +++ b/mods/gen4/moves.js @@ -1104,10 +1104,11 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['acupressure', 'aromatherapy', 'assist', 'chatter', 'copycat', 'counter', 'curse', 'doomdesire', 'feint', 'focuspunch', 'futuresight', 'gravity', 'hail', 'haze', 'healbell', 'helpinghand', 'lightscreen', 'luckychant', 'magiccoat', 'mefirst', 'metronome', 'mimic', 'mirrorcoat', 'mirrormove', 'mist', 'mudsport', 'naturepower', 'perishsong', 'psychup', 'raindance', 'reflect', 'roleplay', 'safeguard', 'sandstorm', 'sketch', 'sleeptalk', 'snatch', 'spikes', 'spitup', 'stealthrock', 'struggle', 'sunnyday', 'tailwind', 'toxicspikes', 'transform', 'watersport']; - if (!pokemon.lastAttackedBy || !pokemon.lastAttackedBy.pokemon.lastMove || !pokemon.lastAttackedBy.move || noMirror.includes(pokemon.lastAttackedBy.move) || !pokemon.lastAttackedBy.pokemon.hasMove(pokemon.lastAttackedBy.move)) { - return false; + if (!pokemon.lastHurtBy) return false; + if (!pokemon.lastHurtBy.source.lastMove || !pokemon.lastHurtBy.move || noMirror.includes(pokemon.lastHurtBy.move) || !pokemon.lastHurtBy.source.hasMove(pokemon.lastHurtBy.move)) { + return false; } - this.useMove(pokemon.lastAttackedBy.move, pokemon); + this.useMove(pokemon.lastHurtBy.move, pokemon); }, target: "self", }, diff --git a/mods/gen5/formats-data.js b/mods/gen5/formats-data.js index 99b5f0f4fd6b..03a93e4bebfa 100644 --- a/mods/gen5/formats-data.js +++ b/mods/gen5/formats-data.js @@ -98,7 +98,7 @@ let BattleFormatsData = { }, raticate: { inherit: true, - randomBattleMoves: ["facade", "flamewheel", "suckerpunch", "uturn"], + randomBattleMoves: ["facade", "flamewheel", "suckerpunch", "uturn", "swordsdance"], tier: "NU", }, spearow: { @@ -258,7 +258,7 @@ let BattleFormatsData = { dugtrio: { inherit: true, randomBattleMoves: ["earthquake", "stoneedge", "stealthrock", "suckerpunch", "reversal", "substitute"], - tier: "Uber", + tier: "OU", }, meowth: { inherit: true, @@ -587,7 +587,7 @@ let BattleFormatsData = { }, blissey: { inherit: true, - randomBattleMoves: ["wish", "softboiled", "protect", "toxic", "aromatherapy", "seismictoss", "counter", "thunderwave", "stealthrock", "flamethrower", "icebeam"], + randomBattleMoves: ["wish", "softboiled", "protect", "toxic", "aromatherapy", "seismictoss", "counter", "thunderwave", "stealthrock", "flamethrower"], tier: "OU", }, tangela: { @@ -715,7 +715,7 @@ let BattleFormatsData = { }, lapras: { inherit: true, - randomBattleMoves: ["icebeam", "thunderbolt", "healbell", "toxic", "surf", "dragondance", "substitute", "waterfall", "return", "avalanche", "rest", "sleeptalk", "curse", "iceshard", "drillrun"], + randomBattleMoves: ["icebeam", "thunderbolt", "healbell", "toxic", "surf", "dragondance", "waterfall", "return", "avalanche", "rest", "sleeptalk", "curse", "iceshard", "drillrun"], tier: "NU", }, ditto: { @@ -833,7 +833,7 @@ let BattleFormatsData = { }, dragonite: { inherit: true, - randomBattleMoves: ["dragondance", "outrage", "firepunch", "extremespeed", "dragonclaw", "earthquake", "roost", "waterfall", "substitute", "thunderwave", "dragontail", "hurricane", "superpower", "dracometeor"], + randomBattleMoves: ["dragondance", "outrage", "firepunch", "extremespeed", "dragonclaw", "earthquake", "roost", "substitute", "thunderwave", "dragontail", "hurricane", "superpower", "dracometeor"], tier: "OU", }, mewtwo: { @@ -1087,7 +1087,7 @@ let BattleFormatsData = { tier: "UU", }, gliscor: { - randomBattleMoves: ["swordsdance", "acrobatics", "earthquake", "roost", "substitute", "taunt", "icefang", "protect", "toxic", "stealthrock"], + randomBattleMoves: ["swordsdance", "earthquake", "roost", "substitute", "taunt", "icefang", "protect", "toxic", "stealthrock"], tier: "OU", }, snubbull: { @@ -1502,7 +1502,7 @@ let BattleFormatsData = { }, sableye: { inherit: true, - randomBattleMoves: ["recover", "willowisp", "taunt", "trick", "toxic", "nightshade", "seismictoss"], + randomBattleMoves: ["recover", "willowisp", "taunt", "trick", "toxic", "nightshade", "foulplay"], tier: "UU", }, mawile: { @@ -1587,7 +1587,7 @@ let BattleFormatsData = { tier: "LC", }, sharpedo: { - randomBattleMoves: ["protect", "hydropump", "icebeam", "crunch", "earthquake", "waterfall", "hiddenpowergrass", "aquajet"], + randomBattleMoves: ["protect", "hydropump", "icebeam", "crunch", "earthquake", "waterfall", "hiddenpowergrass"], tier: "UU", }, wailmer: { @@ -1714,7 +1714,7 @@ let BattleFormatsData = { tier: "LC", }, cradily: { - randomBattleMoves: ["stealthrock", "recover", "stockpile", "seedbomb", "rockslide", "earthquake", "curse", "swordsdance"], + randomBattleMoves: ["stealthrock", "recover", "toxic", "seedbomb", "rockslide", "earthquake", "curse", "swordsdance"], tier: "NU", }, anorith: { @@ -1733,7 +1733,7 @@ let BattleFormatsData = { }, milotic: { inherit: true, - randomBattleMoves: ["recover", "scald", "hypnosis", "toxic", "icebeam", "dragontail", "rest", "sleeptalk", "hiddenpowergrass"], + randomBattleMoves: ["recover", "scald", "toxic", "icebeam", "dragontail", "rest", "sleeptalk", "hiddenpowergrass"], tier: "UU", }, castform: { @@ -1913,7 +1913,7 @@ let BattleFormatsData = { }, jirachi: { inherit: true, - randomBattleMoves: ["bodyslam", "ironhead", "firepunch", "thunderwave", "stealthrock", "wish", "uturn", "calmmind", "psychic", "thunder", "icepunch", "flashcannon", "meteormash"], + randomBattleMoves: ["bodyslam", "ironhead", "firepunch", "thunderwave", "stealthrock", "wish", "uturn", "calmmind", "psychic", "thunder", "icepunch", "flashcannon"], tier: "OU", }, deoxys: { @@ -2026,7 +2026,7 @@ let BattleFormatsData = { tier: "NFE", }, luxray: { - randomBattleMoves: ["wildcharge", "icefang", "firefang", "crunch", "superpower"], + randomBattleMoves: ["wildcharge", "icefang", "facade", "crunch", "superpower"], tier: "NU", }, cranidos: { @@ -2194,7 +2194,7 @@ let BattleFormatsData = { tier: "LC", }, drapion: { - randomBattleMoves: ["crunch", "whirlwind", "toxicspikes", "pursuit", "earthquake", "aquatail", "swordsdance", "poisonjab", "rest", "sleeptalk"], + randomBattleMoves: ["crunch", "whirlwind", "toxicspikes", "pursuit", "earthquake", "aquatail", "swordsdance", "poisonjab", "taunt"], tier: "RU", }, croagunk: { @@ -2363,7 +2363,7 @@ let BattleFormatsData = { requiredItem: "Flame Plate", }, arceusflying: { - randomBattleMoves: ["calmmind", "judgment", "refresh", "recover"], + randomBattleMoves: ["calmmind", "judgment", "refresh", "recover", "focusblast"], eventOnly: true, requiredItem: "Sky Plate", }, @@ -2428,7 +2428,7 @@ let BattleFormatsData = { }, serperior: { inherit: true, - randomBattleMoves: ["leafstorm", "hiddenpowerfire", "substitute", "leechseed", "dragonpulse", "gigadrain"], + randomBattleMoves: ["calmmind", "hiddenpowerfire", "substitute", "leechseed", "dragonpulse", "gigadrain"], tier: "NU", }, tepig: { diff --git a/mods/gen5/random-teams.js b/mods/gen5/random-teams.js index 95ee461ec0ee..1e5ac09cceff 100644 --- a/mods/gen5/random-teams.js +++ b/mods/gen5/random-teams.js @@ -232,8 +232,8 @@ class RandomGen5Teams extends RandomGen6Teams { case 'drainpunch': if (hasMove['closecombat'] || hasMove['crosschop'] || hasMove['highjumpkick']) rejected = true; break; - case 'fierydance': case 'flamethrower': - if (hasMove['blueflare'] || hasMove['fireblast'] || hasMove['lavaplume'] || hasMove['overheat']) rejected = true; + case 'flareblitz': case 'fierydance': case 'flamethrower': case 'lavaplume': + if (hasMove['blueflare'] || hasMove['fireblast'] || hasMove['overheat']) rejected = true; break; case 'firepunch': if (hasMove['flareblitz']) rejected = true; @@ -241,14 +241,11 @@ class RandomGen5Teams extends RandomGen6Teams { case 'overheat': if (counter.setupType === 'Special' || hasMove['fireblast']) rejected = true; break; - case 'airslash': - if (hasMove['hurricane']) rejected = true; - break; - case 'bravebird': case 'drillpeck': case 'pluck': - if (hasMove['acrobatics']) rejected = true; + case 'airslash': case 'bravebird': case 'drillpeck': case 'pluck': + if (hasMove['acrobatics'] || hasMove['hurricane']) rejected = true; break; case 'gigadrain': - if ((!counter.setupType && hasMove['leafstorm']) || hasMove['petaldance']) rejected = true; + if ((!counter.setupType && hasMove['leafstorm']) || hasMove['petaldance'] || hasMove['powerwhip']) rejected = true; break; case 'solarbeam': if ((!hasAbility['Drought'] && !hasMove['sunnyday']) || hasMove['gigadrain'] || hasMove['leafstorm']) rejected = true; @@ -294,7 +291,7 @@ class RandomGen5Teams extends RandomGen6Teams { break; // Status: - case 'encore': case 'suckerpunch': + case 'encore': case 'iceshard': case 'suckerpunch': if (hasMove['rest'] && hasMove['sleeptalk']) rejected = true; break; case 'moonlight': case 'painsplit': case 'recover': case 'roost': case 'softboiled': case 'synthesis': @@ -427,7 +424,7 @@ class RandomGen5Teams extends RandomGen6Teams { rejectAbility = !counter['inaccurate']; } else if (ability === 'Defiant' || ability === 'Moxie') { rejectAbility = !counter['Physical'] && !hasMove['batonpass']; - } else if (ability === 'Gluttony' || ability === 'Moody') { + } else if (ability === 'Gluttony' || ability === 'Inner Focus' || ability === 'Moody') { rejectAbility = true; } else if (ability === 'Hydration' || ability === 'Rain Dish' || ability === 'Swift Swim') { rejectAbility = !hasMove['raindance'] && !teamDetails['rain']; @@ -467,6 +464,8 @@ class RandomGen5Teams extends RandomGen6Teams { rejectAbility = counter['damage'] >= counter.damagingMoves.length || (counter.Status > 2 && !counter.setupType); } else if (ability === 'Torrent') { rejectAbility = !counter['Water']; + } else if (ability === 'Unburden') { + rejectAbility = template.baseStats.spe > 100; } else if (ability === 'Water Absorb') { rejectAbility = abilities.includes('Volt Absorb'); } @@ -559,9 +558,11 @@ class RandomGen5Teams extends RandomGen6Teams { // Medium priority } else if (counter.Physical >= 4 && !hasMove['fakeout'] && !hasMove['suckerpunch'] && !hasMove['flamecharge'] && !hasMove['rapidspin']) { - item = this.randomChance(2, 3) ? 'Choice Band' : 'Expert Belt'; + item = !counter['Normal'] && this.randomChance(1, 3) ? 'Expert Belt' : 'Choice Band'; } else if (counter.Special >= 4) { - item = this.randomChance(2, 3) ? 'Choice Specs' : 'Expert Belt'; + item = this.randomChance(1, 3) ? 'Expert Belt' : 'Choice Specs'; + } else if (ability === 'Speed Boost' && !hasMove['substitute'] && counter.Physical + counter.Special > 2) { + item = 'Life Orb'; } else if ((hasMove['eruption'] || hasMove['waterspout']) && !counter['Status']) { item = 'Choice Scarf'; } else if (this.getEffectiveness('Ground', template) >= 2 && ability !== 'Levitate' && !hasMove['magnetrise']) { @@ -587,8 +588,8 @@ class RandomGen5Teams extends RandomGen6Teams { } else if (counter.Physical + counter.Special >= 3 && counter.setupType) { item = hasMove['outrage'] ? 'Lum Berry' : 'Life Orb'; } else if (counter.Physical + counter.Special >= 4) { - item = 'Expert Belt'; - } else if (slot === 0 && ability !== 'Sturdy' && !counter['recoil']) { + item = counter['Normal'] ? 'Life Orb' : 'Expert Belt'; + } else if (slot === 0 && ability !== 'Regenerator' && ability !== 'Sturdy' && !counter['recoil'] && !counter['recovery'] && template.baseStats.hp + template.baseStats.def + template.baseStats.spd < 285) { item = 'Focus Sash'; // This is the "REALLY can't think of a good item" cutoff @@ -651,7 +652,7 @@ class RandomGen5Teams extends RandomGen6Teams { randomTeam() { let pokemon = []; - let allowedNFE = ['Porygon2', 'Scyther']; + const allowedNFE = ['Porygon2', 'Scyther']; let pokemonPool = []; for (let id in this.data.FormatsData) { @@ -705,6 +706,8 @@ class RandomGen5Teams extends RandomGen6Teams { break; } + let types = template.types; + // Limit 2 of any type let skip = false; for (const type of template.types) { @@ -717,6 +720,9 @@ class RandomGen5Teams extends RandomGen6Teams { let set = this.randomSet(template, pokemon.length, teamDetails); + // Illusion shouldn't be the last Pokemon of the team + if (set.ability === 'Illusion' && pokemon.length > 4) continue; + // Limit 1 of any type combination let typeCombo = template.types.slice().sort().join(); if (set.ability === 'Drought' || set.ability === 'Drizzle' || set.ability === 'Sand Stream') { @@ -730,11 +736,18 @@ class RandomGen5Teams extends RandomGen6Teams { // Okay, the set passes, add it to our team pokemon.push(set); + if (pokemon.length === 6) { + // Set Zoroark's level to be the same as the last Pokemon + let illusion = teamDetails['illusion']; + if (illusion) pokemon[illusion - 1].level = pokemon[5].level; + break; + } + // Now that our Pokemon has passed all checks, we can increment our counters baseFormes[template.baseSpecies] = 1; // Increment type counters - for (const type of template.types) { + for (const type of types) { if (type in typeCount) { typeCount[type]++; } else { @@ -761,6 +774,9 @@ class RandomGen5Teams extends RandomGen6Teams { if (set.moves.includes('stealthrock')) teamDetails['stealthRock'] = 1; if (set.moves.includes('toxicspikes')) teamDetails['toxicSpikes'] = 1; if (set.moves.includes('rapidspin')) teamDetails['rapidSpin'] = 1; + + // For setting Zoroark's level + if (set.ability === 'Illusion') teamDetails['illusion'] = pokemon.length; } return pokemon; } diff --git a/mods/gen6/formats-data.js b/mods/gen6/formats-data.js index 3ed53eaee5c4..41f47fafef8d 100644 --- a/mods/gen6/formats-data.js +++ b/mods/gen6/formats-data.js @@ -2816,7 +2816,7 @@ let BattleFormatsData = { }, empoleon: { inherit: true, - randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "hiddenpowerfire", "icebeam", "scald", "toxic", "roar", "stealthrock"], + randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "defog", "icebeam", "scald", "toxic", "roar", "stealthrock"], randomDoubleBattleMoves: ["icywind", "scald", "surf", "icebeam", "hiddenpowerelectric", "protect", "grassknot", "flashcannon"], tier: "UU", doublesTier: "DUU", @@ -3682,7 +3682,7 @@ let BattleFormatsData = { }, whimsicott: { inherit: true, - randomBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast"], + randomBattleMoves: ["encore", "taunt", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast"], randomDoubleBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "helpinghand", "stunspore", "moonblast", "tailwind", "dazzlinggleam", "gigadrain", "protect"], tier: "UU", doublesTier: "DOU", @@ -4518,7 +4518,7 @@ let BattleFormatsData = { }, malamar: { inherit: true, - randomBattleMoves: ["superpower", "knockoff", "psychocut", "rockslide", "substitute", "trickroom"], + randomBattleMoves: ["superpower", "knockoff", "psychocut", "substitute", "trickroom"], randomDoubleBattleMoves: ["superpower", "psychocut", "rockslide", "trickroom", "knockoff", "protect"], tier: "NU", doublesTier: "DUU", diff --git a/mods/gen6/moves.js b/mods/gen6/moves.js index 0d299ad700d2..3544ca9c0113 100644 --- a/mods/gen6/moves.js +++ b/mods/gen6/moves.js @@ -56,7 +56,7 @@ let BattleMovedex = { }, detect: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, diamondstorm: { inherit: true, @@ -117,7 +117,7 @@ let BattleMovedex = { }, endure: { inherit: true, - desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, entrainment: { inherit: true, @@ -170,7 +170,7 @@ let BattleMovedex = { }, kingsshield: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, knockoff: { inherit: true, @@ -314,7 +314,7 @@ let BattleMovedex = { }, protect: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, pursuit: { inherit: true, @@ -322,7 +322,7 @@ let BattleMovedex = { }, quickguard: { inherit: true, - desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", }, ragepowder: { inherit: true, @@ -380,7 +380,7 @@ let BattleMovedex = { }, spikyshield: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, struggle: { inherit: true, @@ -449,7 +449,7 @@ let BattleMovedex = { }, wideguard: { inherit: true, - desc: "The user and its party members are protected from damaging attacks made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from damaging attacks made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", shortDesc: "Protects allies from multi-target hits this turn.", effect: { duration: 1, diff --git a/mods/gen6/random-teams.js b/mods/gen6/random-teams.js index 33a58ae28bde..70116ae70b86 100644 --- a/mods/gen6/random-teams.js +++ b/mods/gen6/random-teams.js @@ -30,7 +30,7 @@ class RandomGen6Teams extends RandomTeams { template = this.getTemplate('unown'); let err = new Error('Template incompatible with random battles: ' + species); - require('../../lib/crashlogger')(err, 'The randbat set generator'); + require('../../lib/crashlogger')(err, 'The gen 6 randbat set generator'); } if (template.battleOnly) { @@ -192,7 +192,7 @@ class RandomGen6Teams extends RandomTeams { if (!!counter['speedsetup'] || hasMove['encore'] || hasMove['raindance'] || hasMove['roar'] || hasMove['whirlwind']) rejected = true; break; case 'defog': - if (counter.setupType || hasMove['spikes'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; + if (counter.setupType || hasMove['spikes'] || hasMove['stealthrock'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; break; case 'fakeout': if (counter.setupType || hasMove['substitute'] || hasMove['switcheroo'] || hasMove['trick']) rejected = true; @@ -700,6 +700,8 @@ class RandomGen6Teams extends RandomTeams { ability = 'Klutz'; } else if ((template.species === 'Rampardos' && !hasMove['headsmash']) || hasMove['rockclimb']) { ability = 'Sheer Force'; + } else if (template.species === 'Torterra' && !counter['Grass']) { + ability = 'Shell Armor'; } else if (template.species === 'Umbreon') { ability = 'Synchronize'; } else if (template.id === 'venusaurmega') { diff --git a/mods/gennext/moves.js b/mods/gennext/moves.js index 365eecc0aaa0..79701744f956 100644 --- a/mods/gennext/moves.js +++ b/mods/gennext/moves.js @@ -973,9 +973,11 @@ let BattleMovedex = { avalanche: { inherit: true, basePowerCallback: function (pokemon, source) { - if ((source.lastDamage > 0 && pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn)) { - this.debug('Boosted for getting hit by ' + pokemon.lastAttackedBy.move); - return this.isWeather('hail') ? 180 : 120; + if (pokemon.lastHurtBy) { + if (pokemon.lastHurtBy.damage > 0 && pokemon.lastHurtBy.thisTurn) { + this.debug('Boosted for getting hit by ' + pokemon.lastHurtBy.move); + return this.isWeather('hail') ? 180 : 120; + } } return this.isWeather('hail') ? 90 : 60; }, diff --git a/mods/mixandmega6/items.js b/mods/mixandmega6/items.js deleted file mode 100644 index ec220e3e36a0..000000000000 --- a/mods/mixandmega6/items.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -/**@type {{[k: string]: ModdedItemData}} */ -let BattleItems = { - blueorb: { - inherit: true, - onSwitchIn: function (pokemon) { - if (pokemon.isActive && !pokemon.template.isPrimal) { - this.insertQueue({pokemon: pokemon, choice: 'runPrimal'}); - } - }, - onPrimal: function (pokemon) { - /**@type {Template} */ - // @ts-ignore - let template = this.getMixedTemplate(pokemon.originalSpecies, 'Kyogre-Primal'); - if (pokemon.originalSpecies === 'Kyogre') { - pokemon.formeChange(template, this.effect, true); - } else { - pokemon.formeChange(template, this.effect, true); - pokemon.baseTemplate = template; - this.add('-start', pokemon, 'Blue Orb', '[silent]'); - } - }, - onTakeItem: function (item) { - return false; - }, - }, - redorb: { - inherit: true, - onSwitchIn: function (pokemon) { - if (pokemon.isActive && !pokemon.template.isPrimal) { - this.insertQueue({pokemon: pokemon, choice: 'runPrimal'}); - } - }, - onPrimal: function (pokemon) { - /**@type {Template} */ - // @ts-ignore - let template = this.getMixedTemplate(pokemon.originalSpecies, 'Groudon-Primal'); - if (pokemon.originalSpecies === 'Groudon') { - pokemon.formeChange(template, this.effect, true); - } else { - pokemon.formeChange(template, this.effect, true); - pokemon.baseTemplate = template; - this.add('-start', pokemon, 'Red Orb', '[silent]'); - // @ts-ignore - let oTemplate = this.getTemplate(pokemon.illusion || pokemon.originalSpecies); - this.add('-start', pokemon, 'Red Orb', '[silent]'); - if (pokemon.illusion) { - pokemon.ability = ''; - let types = oTemplate.types; - if (types.length > 1 || types[types.length - 1] !== 'Fire') { - this.add('-start', pokemon, 'typechange', (types[0] !== 'Fire' ? types[0] + '/' : '') + 'Fire', '[silent]'); - } - } else if (oTemplate.types.length !== pokemon.template.types.length || oTemplate.types[1] !== pokemon.template.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.template.types.join('/'), '[silent]'); - } - } - }, - onTakeItem: function (item) { - return false; - }, - }, -}; - -exports.BattleItems = BattleItems; diff --git a/mods/mixandmega6/scripts.js b/mods/mixandmega6/scripts.js deleted file mode 100644 index bf80f27909cf..000000000000 --- a/mods/mixandmega6/scripts.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; - -/**@type {ModdedBattleScriptsData} */ -let BattleScripts = { - inherit: 'gen6', - init: function () { - for (let id in this.data.Items) { - if (!this.data.Items[id].megaStone) continue; - this.modData('Items', id).onTakeItem = false; - } - }, - canMegaEvo: function (pokemon) { - if (pokemon.template.isMega || pokemon.template.isPrimal) return null; - - const item = pokemon.getItem(); - if (item.megaStone) { - if (item.megaStone === pokemon.species) return null; - return item.megaStone; - } else if (pokemon.baseMoves.includes('dragonascent')) { - return 'Rayquaza-Mega'; - } else { - return null; - } - }, - runMegaEvo: function (pokemon) { - if (pokemon.template.isMega || pokemon.template.isPrimal) return false; - - /**@type {Template} */ - // @ts-ignore - const template = this.getMixedTemplate(pokemon.originalSpecies, pokemon.canMegaEvo); - const side = pokemon.side; - - // Pokémon affected by Sky Drop cannot Mega Evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; - } - } - - // Do we have a proper sprite for it? - // @ts-ignore - if (this.getTemplate(pokemon.canMegaEvo).baseSpecies === pokemon.originalSpecies) { - pokemon.formeChange(template, pokemon.getItem(), true); - } else { - let oTemplate = this.getTemplate(pokemon.originalSpecies); - let oMegaTemplate = this.getTemplate(template.originalMega); - pokemon.formeChange(template, pokemon.getItem(), true); - this.add('-start', pokemon, oMegaTemplate.requiredItem || oMegaTemplate.requiredMove, '[silent]'); - if (oTemplate.types.length !== pokemon.template.types.length || oTemplate.types[1] !== pokemon.template.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.template.types.join('/'), '[silent]'); - } - } - - pokemon.canMegaEvo = null; - return true; - }, - getMixedTemplate: function (originalSpecies, megaSpecies) { - let originalTemplate = this.getTemplate(originalSpecies); - let megaTemplate = this.getTemplate(megaSpecies); - if (originalTemplate.baseSpecies === megaTemplate.baseSpecies) return megaTemplate; - // @ts-ignore - let deltas = this.getMegaDeltas(megaTemplate); - // @ts-ignore - let template = this.doGetMixedTemplate(originalTemplate, deltas); - return template; - }, - getMegaDeltas: function (megaTemplate) { - let baseTemplate = this.getTemplate(megaTemplate.baseSpecies); - let deltas = { - ability: megaTemplate.abilities['0'], - baseStats: {}, - weightkg: megaTemplate.weightkg - baseTemplate.weightkg, - originalMega: megaTemplate.species, - requiredItem: megaTemplate.requiredItem, - }; - for (let statId in megaTemplate.baseStats) { - // @ts-ignore - deltas.baseStats[statId] = megaTemplate.baseStats[statId] - baseTemplate.baseStats[statId]; - } - if (megaTemplate.types.length > baseTemplate.types.length) { - deltas.type = megaTemplate.types[1]; - } else if (megaTemplate.types.length < baseTemplate.types.length) { - deltas.type = baseTemplate.types[0]; - } else if (megaTemplate.types[1] !== baseTemplate.types[1]) { - deltas.type = megaTemplate.types[1]; - } - if (megaTemplate.isMega) deltas.isMega = true; - if (megaTemplate.isPrimal) deltas.isPrimal = true; - return deltas; - }, - doGetMixedTemplate: function (template, deltas) { - if (!deltas) throw new TypeError("Must specify deltas!"); - if (!template || typeof template === 'string') template = this.getTemplate(template); - template = Object.assign({}, template); - template.abilities = {'0': deltas.ability}; - if (template.types[0] === deltas.type) { - template.types = [deltas.type]; - } else if (deltas.type) { - template.types = [template.types[0], deltas.type]; - } - let baseStats = template.baseStats; - // @ts-ignore - template.baseStats = {}; - for (let statName in baseStats) { - // @ts-ignore - template.baseStats[statName] = this.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255); - } - template.weightkg = Math.max(0.1, template.weightkg + deltas.weightkg); - template.originalMega = deltas.originalMega; - template.requiredItem = deltas.requiredItem; - if (deltas.isMega) template.isMega = true; - if (deltas.isPrimal) template.isPrimal = true; - return template; - }, -}; - -exports.BattleScripts = BattleScripts; diff --git a/mods/pic/moves.js b/mods/pic/moves.js new file mode 100644 index 000000000000..d622677a1623 --- /dev/null +++ b/mods/pic/moves.js @@ -0,0 +1,66 @@ +'use strict'; + +exports.BattleMovedex = { + "skillswap": { + inherit: true, + onHit: function (target, source, move) { + let targetAbility = this.getAbility(target.ability); + let sourceAbility = this.getAbility(source.ability); + if (target.side === source.side) { + this.add('-activate', source, 'move: Skill Swap', '', '', '[of] ' + target); + } else { + this.add('-activate', source, 'move: Skill Swap', targetAbility, sourceAbility, '[of] ' + target); + } + this.singleEvent('End', sourceAbility, source.abilityData, source); + let sourceAlly = source.side.active.find(ally => ally && ally !== source && !ally.fainted); + if (sourceAlly && sourceAlly.innate) { + sourceAlly.removeVolatile(sourceAlly.innate); + delete sourceAlly.innate; + } + this.singleEvent('End', targetAbility, target.abilityData, target); + let targetAlly = target.side.active.find(ally => ally && ally !== target && !ally.fainted); + if (targetAlly && targetAlly.innate) { + targetAlly.removeVolatile(targetAlly.innate); + delete targetAlly.innate; + } + if (targetAbility.id !== sourceAbility.id) { + source.ability = targetAbility.id; + target.ability = sourceAbility.id; + source.abilityData = {id: source.ability.id, target: source}; + target.abilityData = {id: target.ability.id, target: target}; + } + if (sourceAlly && sourceAlly.ability !== source.ability) { + let volatile = sourceAlly.innate = 'ability' + source.ability; + sourceAlly.volatiles[volatile] = {id: volatile}; + sourceAlly.volatiles[volatile].target = sourceAlly; + sourceAlly.volatiles[volatile].source = source; + sourceAlly.volatiles[volatile].sourcePosition = source.position; + if (!source.innate) { + volatile = source.innate = 'ability' + sourceAlly.ability; + source.volatiles[volatile] = {id: volatile}; + source.volatiles[volatile].target = source; + source.volatiles[volatile].source = sourceAlly; + source.volatiles[volatile].sourcePosition = sourceAlly.position; + } + } + if (targetAlly && targetAlly.ability !== target.ability) { + let volatile = targetAlly.innate = 'ability' + target.ability; + targetAlly.volatiles[volatile] = {id: volatile}; + targetAlly.volatiles[volatile].target = targetAlly; + targetAlly.volatiles[volatile].source = target; + targetAlly.volatiles[volatile].sourcePosition = target.position; + if (!target.innate) { + volatile = target.innate = 'ability' + targetAlly.ability; + target.volatiles[volatile] = {id: volatile}; + target.volatiles[volatile].target = target; + target.volatiles[volatile].source = targetAlly; + target.volatiles[volatile].sourcePosition = targetAlly.position; + } + } + this.singleEvent('Start', targetAbility, source.abilityData, source); + if (sourceAlly) this.singleEvent('Start', sourceAlly.innate, sourceAlly.volatiles[sourceAlly.innate], sourceAlly); + this.singleEvent('Start', sourceAbility, target.abilityData, target); + if (targetAlly) this.singleEvent('Start', targetAlly.innate, targetAlly.volatiles[targetAlly.innate], targetAlly); + }, + }, +}; diff --git a/mods/pic/scripts.js b/mods/pic/scripts.js new file mode 100644 index 000000000000..2aed4151e824 --- /dev/null +++ b/mods/pic/scripts.js @@ -0,0 +1,62 @@ +'use strict'; + +exports.BattleScripts = { + getEffect: function (name) { + if (name && typeof name !== 'string') { + return name; + } + let id = toId(name); + if (id.startsWith('ability')) return Object.assign(Object.create(this.getAbility(id.slice(7))), {id}); + return Object.getPrototypeOf(this).getEffect.call(this, name); + }, + pokemon: { + setAbility: function (ability, source, isFromFormechange) { + if (!this.hp) return false; + ability = this.battle.getAbility(ability); + let oldAbility = this.ability; + if (!isFromFormechange) { + if (['illusion', 'battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange'].includes(ability.id)) return false; + if (['battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange'].includes(oldAbility)) return false; + } + this.battle.singleEvent('End', this.battle.getAbility(oldAbility), this.abilityData, this, source); + let ally = this.side.active.find(ally => ally && ally !== this && !ally.fainted); + if (ally && ally.innate) { + ally.removeVolatile(ally.innate); + delete ally.innate; + } + this.ability = ability.id; + this.abilityData = {id: ability.id, target: this}; + if (ability.id) { + this.battle.singleEvent('Start', ability, this.abilityData, this, source); + if (ally && ally.ability !== this.ability) { + ally.innate = 'ability' + ability.id; + ally.addVolatile(ally.innate); + } + } + this.abilityOrder = this.battle.abilityOrder++; + return oldAbility; + }, + hasAbility: function (ability) { + if (!this.ignoringAbility()) { + if (Array.isArray(ability) ? ability.map(toId).includes(this.ability) : toId(ability) === this.ability) { + return true; + } + } + let ally = this.side.active.find(ally => ally && ally !== this && !ally.fainted); + if (!ally || ally.ignoringAbility()) return false; + if (Array.isArray(ability)) return ability.map(toId).includes(ally.ability); + return toId(ability) === ally.ability; + }, + getRequestData: function () { + let ally = this.side.active.find(ally => ally && ally !== this && !ally.fainted); + this.moveSlots = this.baseMoveSlots.concat(ally ? ally.baseMoveSlots : []); + for (const moveSlot of this.moveSlots) { + moveSlot.disabled = false; + moveSlot.disabledSource = ''; + } + this.battle.runEvent('DisableMove', this); + if (!this.ateBerry) this.disableMove('belch'); + return Object.getPrototypeOf(this).getRequestData.call(this); + }, + }, +}; diff --git a/mods/stadium/moves.js b/mods/stadium/moves.js index cf3d148ef42e..96d880a31056 100644 --- a/mods/stadium/moves.js +++ b/mods/stadium/moves.js @@ -159,11 +159,11 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (!target.lastAttackedBy) { - target.lastAttackedBy = {pokemon: source, move: move.id, thisTurn: true, damage: damage}; + if (!target.lastHurtBy) { + target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { - target.lastAttackedBy.move = move.id; - target.lastAttackedBy.damage = damage; + target.lastHurtBy.move = move.id; + target.lastHurtBy.damage = damage; } return 0; }, diff --git a/punishments.js b/punishments.js index 27f82007c6dd..8604535a5f99 100644 --- a/punishments.js +++ b/punishments.js @@ -742,9 +742,10 @@ Punishments.lock = function (user, expireTime, id, ...reason) { * @param {string} source * @param {string} reason * @param {string?} message - * @param {boolean?} week + * @param {boolean} week + * @param {boolean} name */ -Punishments.autolock = function (user, room, source, reason, message, week) { +Punishments.autolock = function (user, room, source, reason, message, week = false, name = false) { if (!message) message = reason; let punishment = `LOCKED`; @@ -753,9 +754,14 @@ Punishments.autolock = function (user, room, source, reason, message, week) { expires = Date.now() + 7 * 24 * 60 * 60 * 1000; punishment = `WEEKLOCKED`; } - Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`); + if (name) { + punishment = `NAMELOCKED`; + Punishments.namelock(user, expires, toId(user), `Autonamelock: ${user.name || toId(user)}: ${reason}`); + } else { + Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`); + } Monitor.log(`[${source}] ${punishment}: ${message}`); - Rooms.global.modlog(`(${toId(room)}) AUTOLOCK: [${toId(user)}]: ${reason}`); + Rooms.global.modlog(`(${toId(room)}) AUTO${name ? `NAME` : ''}LOCK: [${toId(user)}]: ${reason}`); }; /** * @param {string} name diff --git a/roomlogs.js b/roomlogs.js index 42907bbcc8f7..9d0dc3cf47c7 100644 --- a/roomlogs.js +++ b/roomlogs.js @@ -183,15 +183,36 @@ class Roomlog { } return false; } + /** + * @param {string[]} userids + */ + clearText(userids) { + const messageStart = this.logTimes ? '|c:|' : '|c|'; + const section = this.logTimes ? 4 : 3; // ['', 'c' timestamp?, author, message] + /** @type {string[]} */ + let cleared = []; + this.log = this.log.filter(line => { + if (line.startsWith(messageStart)) { + const parts = Chat.splitFirst(line, '|', section); + const userid = toId(parts[section - 1]); + if (userids.includes(userid)) { + if (!cleared.includes(userid)) cleared.push(userid); + return false; + } + } + return true; + }); + return cleared; + } /** * @param {string} message */ uhtmlchange(message) { - let thirdPipe = message.indexOf('|', 13); - let originalStart = '|uhtml|' + message.slice(13, thirdPipe + 1); - for (let line of this.log) { + const thirdPipe = message.indexOf('|', 13); + const originalStart = '|uhtml|' + message.slice(13, thirdPipe + 1); + for (const [i, line] of this.log.entries()) { if (line.startsWith(originalStart)) { - line = originalStart + message.slice(thirdPipe + 1); + this.log[i] = originalStart + message.slice(thirdPipe + 1); break; } } diff --git a/rooms.js b/rooms.js index 5ec9504ef382..f7b4d0e176af 100644 --- a/rooms.js +++ b/rooms.js @@ -101,12 +101,15 @@ class BasicRoom { /** @type {string?} */ this.modchat = null; this.staffRoom = false; + /** @type {string | false} */ + this.language = false; /** @type {false | number} */ this.slowchat = false; this.filterStretching = false; this.filterEmojis = false; this.filterCaps = false; this.mafiaEnabled = false; + this.unoDisabled = false; /** @type {Set?} */ this.privacySetter = null; /** @type {Map?} */ @@ -130,15 +133,23 @@ class BasicRoom { * @param {string} data */ sendMods(data) { + this.sendRankedUsers(data, '%'); + } + /** + * @param {string} data + * @param {string} [minRank] + */ + sendRankedUsers(data, minRank = '+') { if (this.staffRoom) { if (!this.log) throw new Error(`Staff room ${this.id} has no log`); this.log.add(data); return; } + for (let i in this.users) { let user = this.users[i]; // hardcoded for performance reasons (this is an inner loop) - if (user.isStaff || (this.auth && (this.auth[user.userid] || '+') !== '+')) { + if (user.isStaff || (this.auth && this.auth[user.userid] && this.auth[user.userid] in Config.groups && Config.groups[this.auth[user.userid]].rank >= Config.groups[minRank].rank)) { user.sendTo(this, data); } } @@ -180,6 +191,14 @@ class BasicRoom { addByUser(user, text) { return this.add('|c|' + user.getIdentity(this.id) + '|/log ' + text).update(); } + /** + * Like addByUser, but without logging + * @param {User} user + * @param {string} text + */ + sendByUser(user, text) { + return this.send('|c|' + user.getIdentity(this.id) + '|/log ' + text); + } /** * Like addByUser, but sends to mods only. * @param {User} user @@ -1071,6 +1090,16 @@ class BasicChatRoom extends BasicRoom { this.log.modlog(message); return this; } + /** + * @param {string[]} userids + */ + hideText(userids) { + const cleared = this.log.clearText(userids); + for (const userid of cleared) { + this.send(`|unlink|hide|${userid}`); + } + this.update(); + } logUserStats() { let total = 0; let guests = 0; diff --git a/sim/README.md b/sim/README.md index d0629ea33bc5..6717e02b6258 100644 --- a/sim/README.md +++ b/sim/README.md @@ -1,9 +1,7 @@ Simulator ========= -Pokémon Showdown's new simulator API is designed to be relatively more straightforward to use than the old one. - -It is implemented as a `ReadWriteStream`. You write to it player choices, and you read protocol messages from it. +Pokémon Showdown's simulator API is implemented as a `ReadWriteStream`. You write player choices to it, and you read protocol messages from it. ```js const Sim = require('Pokemon-Showdown/sim'); @@ -16,7 +14,7 @@ stream = new Sim.BattleStream(); } })(); -stream.write(`>start {"format":"gen7randombattle"}`); +stream.write(`>start {"formatid":"gen7randombattle"}`); stream.write(`>player p1 {"name":"Alice"}`); stream.write(`>player p2 {"name":"Bob"}`); ``` @@ -37,7 +35,7 @@ Writing to the simulator In a standard battle, what you write to the simulator looks something like this: ``` ->start {"format":"gen7ou"} +>start {"formatid":"gen7ou"} >player p1 {"name":"Alice","team":"insert packed team here"} >player p2 {"name":"Bob","team":"insert packed team here"} >p1 team 123456 @@ -152,11 +150,11 @@ To be exact, `CHOICE` is one of: - `MOVESLOTSPEC` is a move name (capitalization/spacing-insensitive) or 1-based move slot number - `TARGETSPEC` is a 1-based target slot number. Add a `-` in front of it to refer to allies. Remember that slots oppose each other, so in a battle, the slots go as follows: - Triples Doubles Singles - 3 2 1 2 1 1 - -1 -2 -3 -1 -2 -1 + Triples Doubles Singles + 3 2 1 2 1 1 + -1 -2 -3 -1 -2 -1 -(But note that slot numbers are unnecessary in Singles: you can never choose a target in Singles.) + (But note that slot numbers are unnecessary in Singles: you can never choose a target in Singles.) `SWITCHSPEC` is: diff --git a/sim/battle.js b/sim/battle.js index 5ae171961d3b..54cd8c177243 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -214,9 +214,9 @@ class Battle extends Dex.ModdedDex { let result = this.runEvent('SetWeather', source, source, status); if (!result) { if (result === false) { - if (source && sourceEffect && sourceEffect.weather) { + if (sourceEffect && sourceEffect.weather) { this.add('-fail', source, sourceEffect, '[from]: ' + this.weather); - } else if (source && sourceEffect && sourceEffect.effectType === 'Ability') { + } else if (sourceEffect && sourceEffect.effectType === 'Ability') { this.add('-ability', source, sourceEffect, '[from] ' + this.weather, '[fail]'); } } @@ -1542,13 +1542,16 @@ class Battle extends Dex.ModdedDex { this.runEvent('DisableMove', pokemon); if (!pokemon.ateBerry) pokemon.disableMove('belch'); - if (pokemon.lastAttackedBy) { - if (pokemon.lastAttackedBy.pokemon.isActive) { - pokemon.lastAttackedBy.thisTurn = false; + // If it was an illusion, it's not any more + if (pokemon.lastHurtBy && this.gen >= 7) pokemon.knownType = true; + + for (let i = pokemon.hurtBy.length - 1; i >= 0; i--) { + let attack = pokemon.hurtBy[i]; + if (attack.source.isActive) { + attack.thisTurn = false; } else { - pokemon.lastAttackedBy = null; + pokemon.hurtBy.slice(pokemon.hurtBy.indexOf(attack), 1); } - if (this.gen >= 7) pokemon.knownType = true; // If it was an illusion, it's not any more } if (this.gen >= 7) { @@ -2396,8 +2399,13 @@ class Battle extends Dex.ModdedDex { getTarget(pokemon, move, targetLoc) { move = this.getMove(move); let target; - if ((move.target !== 'randomNormal') && - this.validTargetLoc(targetLoc, pokemon, move.target)) { + // Fails if the target is the user and the move can't target its own position + if (['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === -(pokemon.position + 1) && + !pokemon.volatiles['twoturnmove'] && !pokemon.volatiles['iceball'] && !pokemon.volatiles['rollout']) { + if (move.isFutureMove) return pokemon; + return false; + } + if (move.target !== 'randomNormal' && this.validTargetLoc(targetLoc, pokemon, move.target)) { if (targetLoc > 0) { target = pokemon.side.foe.active[targetLoc - 1]; } else { diff --git a/sim/dex.js b/sim/dex.js index 949247f97358..d4b85a4cfc03 100644 --- a/sim/dex.js +++ b/sim/dex.js @@ -1041,21 +1041,20 @@ class ModdedDex { return false; } - /** @type {DataType[]} */ searchIn = searchIn || ['Pokedex', 'Movedex', 'Abilities', 'Items', 'Natures']; let searchFunctions = {Pokedex: 'getTemplate', Movedex: 'getMove', Abilities: 'getAbility', Items: 'getItem', Natures: 'getNature'}; let searchTypes = {Pokedex: 'pokemon', Movedex: 'move', Abilities: 'ability', Items: 'item', Natures: 'nature'}; /** @type {AnyObject[] | false} */ let searchResults = []; - for (const result of searchIn) { + for (const table of searchIn) { /** @type {AnyObject} */ // @ts-ignore - let res = this[searchFunctions[result]](target); + let res = this[searchFunctions[table]](target); if (res.exists && res.gen <= this.gen) { searchResults.push({ isInexact: isInexact, - searchType: searchTypes[result], + searchType: searchTypes[table], name: res.species ? res.species : res.name, }); } @@ -1077,8 +1076,9 @@ class ModdedDex { maxLd = 2; } searchResults = false; - for (let i = 0; i <= searchIn.length; i++) { - let searchObj = this.data[searchIn[i] || 'Aliases']; + for (const table of [...searchIn, 'Aliases']) { + // @ts-ignore + let searchObj = this.data[table]; if (!searchObj) { continue; } diff --git a/sim/pokemon.js b/sim/pokemon.js index d8d8b1eaf0d1..eb54bbe490fe 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -155,8 +155,8 @@ class Pokemon { this.moveThisTurnResult = undefined; this.lastDamage = 0; - /**@type {?{pokemon: Pokemon, damage: number, thisTurn: boolean, move?: string}} */ - this.lastAttackedBy = null; + /**@type {{source: Pokemon, damage: number, thisTurn: boolean, move?: string}[]} */ + this.hurtBy = []; this.usedItemThisTurn = false; this.newlySwitched = false; this.beingCalledBack = false; @@ -602,12 +602,18 @@ class Pokemon { gotAttacked(move, damage, source) { if (!damage) damage = 0; move = this.battle.getMove(move); - this.lastAttackedBy = { - pokemon: source, + let lastHurtBy = { + source: source, damage: damage, move: move.id, thisTurn: true, }; + this.hurtBy.push(lastHurtBy); + } + + get lastHurtBy() { + if (this.hurtBy.length === 0) return undefined; + return this.hurtBy[this.hurtBy.length - 1]; } /** @@ -1022,7 +1028,7 @@ class Pokemon { this.moveThisTurn = ''; this.lastDamage = 0; - this.lastAttackedBy = null; + this.hurtBy = []; this.newlySwitched = true; this.beingCalledBack = false; @@ -1557,11 +1563,10 @@ class Pokemon { * @param {Side | boolean} side */ getHealthInner(side) { + if (!this.hp) return '0 fnt'; let hpstring; // side === true in replays - if (!this.hp) { - hpstring = '0'; - } else if (side === this.side || side === true) { + if (side === this.side || side === true) { hpstring = '' + this.hp + '/' + this.maxhp; } else { let ratio = this.hp / this.maxhp; diff --git a/tsconfig.json b/tsconfig.json index 5aeb6ed83547..64976f482af0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ }, "types": ["node"], "exclude": [ - "./mods/gen1/pokedex.js" + "./mods/gen1/pokedex.js", + "./mods/pic/*.js" ], "include": [ "./config/formats.js", @@ -38,6 +39,7 @@ "./verifier.js", "./chat.js", "./users.js", + "./chat-plugins/chat-monitor.js", "./chat-plugins/hangman.js", "./chat-plugins/helptickets.js", "./chat-plugins/mafia.js", @@ -46,6 +48,7 @@ "./chat-plugins/poll.js", "./chat-plugins/room-faqs.js", "./chat-plugins/thing-of-the-day.js", + "./chat-plugins/uno.js", "./chat-plugins/wifi.js", "./punishments.js", "./tournaments/generator-round-robin.js" diff --git a/users.js b/users.js index b3cf7976dc15..8a200412bbed 100644 --- a/users.js +++ b/users.js @@ -116,7 +116,7 @@ function merge(user1, user2) { function getUser(name, exactName = false) { if (!name || name === '!') return null; // @ts-ignore - if (name && name.userid) return name; + if (name.userid) return name; let userid = toId(name); let i = 0; if (!exactName) { @@ -513,6 +513,9 @@ class User { this.lockNotified = false; /**@type {string} */ this.autoconfirmed = ''; + // Used in punishments + /** @type {string} */ + this.trackRename = ''; // initialize Users.add(this); } @@ -601,7 +604,8 @@ class User { return true; } - let group = ' '; + /** @type {string} */ + let group; let targetGroup = ''; let targetUser = null; @@ -718,7 +722,9 @@ class User { * @param {Connection} connection The connection asking for the rename */ async rename(name, token, newlyRegistered, connection) { + let userid = toId(name); for (const roomid of this.games) { + if (userid === this.userid) break; const game = Rooms(roomid).game; if (!game || game.ended) continue; // should never happen if (game.allowRenames || !this.named) continue; @@ -744,7 +750,6 @@ class User { return false; } - let userid = toId(name); if (userid.length > 18) { this.send(`|nametaken||Your name must be 18 characters or shorter.`); return false;