`);
let innerStart = start + delimLength;
let innerEnd = i - delimLength;
if (innerStart + 1 >= innerEnd) {
@@ -268,12 +301,20 @@ class TextFormatter {
} else if (this.at(innerEnd - 1) === ' ' && this.at(innerEnd - 2) === '`') {
innerEnd--; // strip ending space
}
+ if (this.showSyntax) this.buffers.push(`${this.slice(start, innerStart)}`);
+ this.buffers.push(``);
this.buffers.push(this.slice(innerStart, innerEnd));
this.buffers.push(`
`);
- this.offset = i;
+ if (this.showSyntax) this.buffers.push(`${this.slice(innerEnd, end)}`);
+ this.offset = end;
}
return true;
case '[':
+ // Link span. Several possiblilities:
+ // [[text ]] - a link with custom text
+ // [[search term]] - Google search
+ // [[wiki: search term]] - Wikipedia search
+ // [[pokemon: species name]] - icon (also item:, type:, category:)
{
if (this.slice(start, start + 2) !== '[[') return false;
let i = start + 2;
@@ -287,6 +328,9 @@ class TextFormatter {
i++;
}
if (this.slice(i, i + 2) !== ']]') return false;
+
+ this.pushSlice(start);
+ this.offset = i + 2;
let termEnd = i;
let uri = '';
if (anglePos >= 0 && this.slice(i - 4, i) === '>') { // `>`
@@ -295,17 +339,21 @@ class TextFormatter {
if (this.at(termEnd - 1) === ' ') termEnd--;
uri = encodeURI(uri.replace(/^([a-z]*[^a-z:])/g, 'http://$1'));
}
- let term = this.slice(start + 2, termEnd).replace(/<\/?a(?: [^>]+)?>/g, '');
- if (uri && !this.isTrusted) {
+ let term = this.slice(start + 2, termEnd).replace(/<\/?[au](?: [^>]+)?>/g, '');
+ if (this.showSyntax) {
+ term += `${this.slice(termEnd, i)}`;
+ } else if (uri && !this.isTrusted) {
const shortUri = uri.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/$/, '');
term += ` <${shortUri}>`;
uri += '" rel="noopener';
}
+
if (colonPos > 0) {
const key = this.slice(start + 2, colonPos).toLowerCase();
switch (key) {
case 'w':
case 'wiki':
+ if (this.showSyntax) break;
term = term.slice(term.charAt(key.length + 1) === ' ' ? key.length + 2 : key.length + 1);
uri = `//en.wikipedia.org/w/index.php?title=Special:Search&search=${this.toUriComponent(term)}`;
term = `wiki: ${term}`;
@@ -314,6 +362,10 @@ class TextFormatter {
case 'item':
case 'type':
case 'category':
+ if (this.showSyntax) {
+ this.buffers.push(`${this.slice(start, this.offset)}`);
+ return true;
+ }
term = term.slice(term.charAt(key.length + 1) === ' ' ? key.length + 2 : key.length + 1);
let display = '';
@@ -334,28 +386,42 @@ class TextFormatter {
if (!uri) {
uri = `//www.google.com/search?ie=UTF-8&btnI&q=${this.toUriComponent(term)}`;
}
- this.pushSlice(start);
- this.buffers.push(`${term}`);
- this.offset = i + 2;
+ if (this.showSyntax) {
+ this.buffers.push(`[[${term}]]`);
+ } else {
+ this.buffers.push(`${term}`);
+ }
}
return true;
case '<':
+ // Roomid-link span. Not to be confused with a URL span.
+ // `<>`
{
if (this.slice(start, start + 8) !== '<<') return false; // <<
let i = start + 8;
while (/[a-z0-9-]/.test(this.at(i))) i++;
if (this.slice(i, i + 8) !== '>>') return false; // >>
+
this.pushSlice(start);
const roomid = this.slice(start + 8, i);
- this.buffers.push(`«${roomid}»`);
+ if (this.showSyntax) {
+ this.buffers.push(`<<${roomid}>>`);
+ } else {
+ this.buffers.push(`«${roomid}»`);
+ }
this.offset = i + 8;
}
return true;
- case 'a':
+ case 'a': case 'u':
+ // URL span. Skip to the end of the link - where `` or `` is.
+ // Nothing inside should be formatted further (obviously we don't want
+ // `example.com/__foo__` to turn `foo` italic).
{
- let i = start + 1;
- while (this.at(i) !== '/' || this.at(i + 1) !== 'a' || this.at(i + 2) !== '>') i++; //
- i += 3;
+ let i = start + 2;
+ // Find or .
+ // We need to check the location of `>` to disambiguate from .
+ while (this.at(i) !== '<' || this.at(i + 1) !== '/' || this.at(i + 3) !== '>') i++;
+ i += 4;
this.pushSlice(i);
}
return true;
@@ -365,7 +431,9 @@ class TextFormatter {
get() {
let beginningOfLine = this.offset;
- // main loop! i tracks our position
+ // main loop! `i` tracks our position
+ // Note that we skip around a lot; `i` is mutated inside the loop
+ // pretty often.
for (let i = beginningOfLine; i < this.str.length; i++) {
const char = this.at(i);
switch (char) {
@@ -375,7 +443,11 @@ class TextFormatter {
case '^':
case '\\':
case '|':
+ // Must be exactly two chars long.
if (this.at(i + 1) === char && this.at(i + 2) !== char) {
+ // This is a completely normal two-char span. Close it if it's
+ // already open, open it if it's not.
+ // The inside of regular spans must not start or end with a space.
if (!(this.at(i - 1) !== ' ' && this.closeSpan(char, i, i + 2))) {
if (this.at(i + 2) !== ' ') this.pushSpan(char, i, i + 2);
}
@@ -387,9 +459,11 @@ class TextFormatter {
while (this.at(i + 1) === char) i++;
break;
case '(':
+ // `(` span - does nothing except end spans
this.stack.push(['(', -1]);
break;
case ')':
+ // end of `(` span
this.closeParenSpan(i);
if (i < this.offset) {
i = this.offset - 1;
@@ -397,6 +471,9 @@ class TextFormatter {
}
break;
case '`':
+ // ` ``code`` ` span. Uses lookahead because its contents are not
+ // formatted.
+ // Must be at least two `` ` `` in a row.
if (this.at(i + 1) === '`') this.runLookahead('`', i);
if (i < this.offset) {
i = this.offset - 1;
@@ -405,6 +482,9 @@ class TextFormatter {
while (this.at(i + 1) === '`') i++;
break;
case '[':
+ // `[` (link) span. Uses lookahead because it might contain a
+ // URL which can't be formatted, or search terms that can't be
+ // formatted.
this.runLookahead('[', i);
if (i < this.offset) {
i = this.offset - 1;
@@ -413,6 +493,9 @@ class TextFormatter {
while (this.at(i + 1) === '[') i++;
break;
case ':':
+ // Looks behind for `spoiler:` or `spoilers:`. Spoiler spans
+ // are also weird because they don't require an ending symbol,
+ // although that's not handled here.
if (i < 7) break;
if (this.slice(i - 7, i + 1).toLowerCase() === 'spoiler:' ||
this.slice(i - 8, i + 1).toLowerCase() === 'spoilers:') {
@@ -421,11 +504,16 @@ class TextFormatter {
}
break;
case '&': // escaped '<' or '>'
+ // greentext or roomid
if (i === beginningOfLine && this.slice(i, i + 4) === '>') {
+ // greentext span, normal except it lacks an ending span
+ // check for certain emoticons like `>_>` or `>w<`
if (!"._/=:;".includes(this.at(i + 4)) && !['w<', 'w>'].includes(this.slice(i + 4, i + 9))) {
this.pushSpan('>', i, i);
}
} else {
+ // completely normal `<>` span
+ // uses lookahead because roomids can't be formatted.
this.runLookahead('<', i);
}
if (i < this.offset) {
@@ -434,7 +522,10 @@ class TextFormatter {
}
while (this.slice(i + 1, i + 5) === 'lt;&') i += 4;
break;
- case '<': // guaranteed to be or
+ // URL span
+ // The constructor has already converted `<` to `<` and URLs
+ // to links, so `<` must be the start of a converted link.
this.runLookahead('a', i);
if (i < this.offset) {
i = this.offset - 1;
@@ -444,6 +535,7 @@ class TextFormatter {
break;
case '\r':
case '\n':
+ // End of the line. No spans span multiple lines.
this.popAllSpans(i);
if (this.replaceLinebreaks) {
this.buffers.push(`
`);
@@ -462,8 +554,8 @@ class TextFormatter {
/**
* Takes a string and converts it to HTML by replacing standard chat formatting with the appropriate HTML tags.
*/
-export function formatText(str: string, isTrusted = false, replaceLinebreaks = false) {
- return new TextFormatter(str, isTrusted, replaceLinebreaks).get();
+export function formatText(str: string, isTrusted = false, replaceLinebreaks = false, showSyntax = false) {
+ return new TextFormatter(str, isTrusted, replaceLinebreaks, showSyntax).get();
}
/**
diff --git a/server/chat-plugins/abuse-monitor.ts b/server/chat-plugins/abuse-monitor.ts
index 2b43e0b9782a..103dae3858c3 100644
--- a/server/chat-plugins/abuse-monitor.ts
+++ b/server/chat-plugins/abuse-monitor.ts
@@ -1679,7 +1679,7 @@ export const commands: Chat.ChatCommands = {
this.refreshPage('abusemonitor-settings');
},
edithistory(target, room, user) {
- this.checkCan('globalban');
+ this.checkCan('lock');
target = toID(target);
if (!target) {
return this.parse(`/help abusemonitor`);
@@ -1688,7 +1688,7 @@ export const commands: Chat.ChatCommands = {
},
ignoremodlog: {
add(target, room, user) {
- this.checkCan('globalban');
+ this.checkCan('lock');
let targetUser: string;
[targetUser, target] = this.splitOne(target).map(f => f.trim());
targetUser = toID(targetUser);
@@ -1721,7 +1721,7 @@ export const commands: Chat.ChatCommands = {
this.refreshPage(`abusemonitor-edithistory-${targetUser}`);
},
remove(target, room, user) {
- this.checkCan('globalban');
+ this.checkCan('lock');
let [targetUser, rawNum] = this.splitOne(target).map(f => f.trim());
targetUser = toID(targetUser);
const num = Number(rawNum);
@@ -2312,7 +2312,7 @@ export const pages: Chat.PageTable = {
return buf;
},
async edithistory(query, user) {
- this.checkCan('globalban');
+ this.checkCan('lock');
const targetUser = toID(query[0]);
if (!targetUser) {
return this.errorReply(`Specify a user.`);
diff --git a/server/chat-plugins/auction.ts b/server/chat-plugins/auction.ts
new file mode 100644
index 000000000000..fd4ce4d7f3db
--- /dev/null
+++ b/server/chat-plugins/auction.ts
@@ -0,0 +1,968 @@
+/**
+ * Chat plugin to run auctions for team tournaments.
+ *
+ * Based on the original Scrappie auction system
+ * https://github.com/Hidden50/Pokemon-Showdown-Node-Bot/blob/master/commands/base-auctions.js
+ * @author Karthik
+ */
+import {Net, Utils} from '../../lib';
+
+interface Player {
+ id: ID;
+ name: string;
+ team?: Team;
+ price: number;
+ tiers?: string[];
+}
+
+interface Manager {
+ id: ID;
+ team: Team;
+}
+
+class Team {
+ id: ID;
+ name: string;
+ credits: number;
+ suspended: boolean;
+ private auction: Auction;
+ constructor(name: string, auction: Auction) {
+ this.id = toID(name);
+ this.name = name;
+ this.credits = auction.startingCredits;
+ this.suspended = false;
+ this.auction = auction;
+ }
+
+ getManagerNames() {
+ const managers = [];
+ for (const id in this.auction.managers) {
+ if (this.auction.managers[id].team !== this) continue;
+ const user = Users.getExact(id);
+ if (user) {
+ managers.push(user.name);
+ } else {
+ managers.push(id);
+ }
+ }
+ return managers;
+ }
+
+ getPlayers() {
+ const players = [];
+ for (const id in this.auction.playerList) {
+ const player = this.auction.playerList[id];
+ if (player.team === this) players.push(player);
+ }
+ return players;
+ }
+
+ isSuspended() {
+ return this.credits < this.auction.minBid || this.suspended;
+ }
+
+ maxBid(credits = this.credits) {
+ return credits + this.auction.minBid * Math.min(0, this.getPlayers().length - this.auction.minPlayers + 1);
+ }
+}
+
+export class Auction extends Rooms.SimpleRoomGame {
+ override readonly gameid = 'auction' as ID;
+ owners: {[k: string]: string};
+ teams: {[k: string]: Team};
+ managers: {[k: string]: Manager};
+ playerList: {[k: string]: Player};
+
+ startingCredits: number;
+ minBid: number;
+ minPlayers: number;
+ blindMode: boolean;
+
+ lastQueue: string[] | null = null;
+ queue: string[] = [];
+ currentTeam: Team;
+ bidTimer: NodeJS.Timer;
+ /** How many seconds have passed since the start of the timer */
+ bidTimeElapsed: number;
+ /** Measured in seconds */
+ bidTimeLimit: number;
+ currentNom: Player;
+ currentBid: number;
+ currentBidder: Team;
+ /** Used for blind mode */
+ bidsPlaced: Set;
+ state: 'setup' | 'nom' | 'bid' = 'setup';
+ constructor(room: Room, startingCredits = 100000) {
+ super(room);
+ this.title = `Auction (${room.title})`;
+ this.owners = {};
+ this.teams = {};
+ this.managers = {};
+ this.playerList = {};
+
+ this.startingCredits = startingCredits;
+ this.minBid = 3000;
+ this.minPlayers = 10;
+ this.blindMode = false;
+
+ this.currentTeam = null!;
+ this.bidTimer = null!;
+ this.bidTimeElapsed = 0;
+ this.bidTimeLimit = 10;
+ this.currentNom = null!;
+ this.currentBid = 0;
+ this.currentBidder = null!;
+ this.bidsPlaced = new Set();
+ }
+
+ sendMessage(message: string) {
+ this.room.add(`|c|&|${message}`).update();
+ }
+
+ sendHTMLBox(htmlContent: string, uhtml?: string) {
+ this.room.add(`|${uhtml ? `uhtml|${uhtml}` : 'html'}|${htmlContent}`).update();
+ }
+
+ checkOwner(user: User) {
+ if (!this.owners[user.id] && !Users.Auth.hasPermission(user, 'declare', null, this.room)) {
+ throw new Chat.ErrorMessage(`You must be an auction owner to use this command.`);
+ }
+ }
+
+ addOwner(user: User) {
+ if (this.owners[user.id]) throw new Chat.ErrorMessage(`${user.name} is already an auction owner.`);
+ this.owners[user.id] = user.id;
+ }
+
+ removeOwner(user: User) {
+ if (!this.owners[user.id]) throw new Chat.ErrorMessage(`${user.name} is not an auction owner.`);
+ delete this.owners[user.id];
+ }
+
+ generateUsernameList(players: (string | Player)[], max = players.length, clickable = false) {
+ let buf = ``;
+ buf += players.slice(0, max).map(p => {
+ if (typeof p === 'object') {
+ return `${Utils.escapeHTML(p.name)} `;
+ }
+ return `${Utils.escapeHTML(p)} `;
+ }).join(', ');
+ if (players.length > max) {
+ buf += ` (+${players.length - max})`;
+ }
+ buf += ``;
+ return buf;
+ }
+
+ generatePriceList() {
+ const draftedPlayers = Utils.sortBy(Object.values(this.playerList).filter(p => p.team), p => -p.price);
+ let buf = '';
+ for (const id in this.teams) {
+ const team = this.teams[id];
+ buf += `${Utils.escapeHTML(team.name)}
`;
+ for (const player of draftedPlayers.filter(p => p.team === team)) {
+ buf += `${Utils.escapeHTML(player.name)} ${player.price} `;
+ }
+ buf += `
`;
+ }
+ buf += `All
`;
+ for (const player of draftedPlayers) {
+ buf += `${Utils.escapeHTML(player.name)} ${player.price} `;
+ }
+ buf += `
`;
+ return buf;
+ }
+
+ generateAuctionTable() {
+ let buf = `Order Teams Credits Players `;
+ const queue = this.queue.filter(id => !this.teams[id].isSuspended());
+ buf += Object.values(this.teams).map(team => {
+ const players = team.getPlayers();
+ let i1 = queue.indexOf(team.id) + 1;
+ let i2 = queue.lastIndexOf(team.id) + 1;
+ if (i1 > queue.length / 2) {
+ [i1, i2] = [i2, i1];
+ }
+ let row = ``;
+ row += `${i1 > 0 ? i1 : '-'} ${i2 > 0 ? i2 : '-'} `;
+ row += `${Utils.escapeHTML(team.name)}
${this.generateUsernameList(team.getManagerNames(), 2, true)} `;
+ row += `${team.credits.toLocaleString()}${team.maxBid() >= this.minBid ? `
Max bid: ${team.maxBid().toLocaleString()}` : ''} `;
+ row += ` `;
+ row += ` `;
+ return row;
+ }).join('');
+ buf += `
`;
+
+ const remainingPlayers = Utils.sortBy(Object.values(this.playerList).filter(p => !p.team), p => p.name);
+ const tierArrays: {[k: string]: Player[]} = {};
+ for (const player of remainingPlayers) {
+ if (!player.tiers?.length) continue;
+ for (const tier of player.tiers) {
+ if (!tierArrays[tier]) tierArrays[tier] = [];
+ tierArrays[tier].push(player);
+ }
+ }
+ const sortedTiers = Object.keys(tierArrays).sort();
+ if (sortedTiers.length) {
+ buf += `Remaining Players (${remainingPlayers.length})
`;
+ buf += `All
${this.generateUsernameList(remainingPlayers)}`;
+ buf += `Tiers
`;
+ for (const tier of sortedTiers) {
+ buf += `${Utils.escapeHTML(tier)} (${tierArrays[tier].length})
${this.generateUsernameList(tierArrays[tier])} `;
+ }
+ buf += `
`;
+ } else {
+ buf += `Remaining Players (${remainingPlayers.length})
${this.generateUsernameList(remainingPlayers)}`;
+ }
+ buf += `Auction Settings
`;
+ buf += `- Minimum bid: ${this.minBid.toLocaleString()}
`;
+ buf += `- Minimum players per team: ${this.minPlayers}
`;
+ buf += `- Blind mode: ${this.blindMode ? 'On' : 'Off'}
`;
+ buf += ``;
+ return buf;
+ }
+
+ generateBidInfo() {
+ let buf = `Player: ${Utils.escapeHTML(this.currentNom.name)} `;
+ buf += `Top bid: ${this.currentBid} `;
+ buf += `Top bidder: ${Utils.escapeHTML(this.currentBidder.name)} `;
+ buf += `Tiers: ${this.currentNom.tiers?.length ? `${Utils.escapeHTML(this.currentNom.tiers.join(', '))}` : 'N/A'}`;
+ return buf;
+ }
+
+ setMinBid(amount: number) {
+ if (this.state !== 'setup') {
+ throw new Chat.ErrorMessage(`You cannot change the minimum bid after the auction has started.`);
+ }
+ if (amount < 500) amount *= 1000;
+ if (isNaN(amount) || amount < 500 || amount > 500000 || amount % 500 !== 0) {
+ throw new Chat.ErrorMessage(`The minimum bid must be a multiple of 500 between 500 and 500,000.`);
+ }
+ this.minBid = amount;
+ }
+
+ setMinPlayers(amount: number) {
+ if (this.state !== 'setup') {
+ throw new Chat.ErrorMessage(`You cannot change the minimum number of players after the auction has started.`);
+ }
+ if (isNaN(amount) || amount < 1 || amount > 30) {
+ throw new Chat.ErrorMessage(`The minimum number of players must be between 1 and 30.`);
+ }
+ this.minPlayers = amount;
+ }
+
+ setBlindMode(blind: boolean) {
+ if (this.state !== 'setup') {
+ throw new Chat.ErrorMessage(`You cannot toggle blind mode after the auction has started.`);
+ }
+ this.blindMode = blind;
+ if (blind) {
+ this.bidTimeLimit = 30;
+ } else {
+ this.bidTimeLimit = 10;
+ }
+ }
+
+ importPlayers(data: string) {
+ if (this.state !== 'setup') {
+ throw new Chat.ErrorMessage(`You cannot import a player list after the auction has started.`);
+ }
+ const rows = data.replace('\r', '').split('\n');
+ const tierNames = rows.shift()!.split('\t').slice(1);
+ const playerList: {[k: string]: Player} = {};
+ for (const row of rows) {
+ const tiers = [];
+ const [name, ...tierData] = row.split('\t');
+ for (let i = 0; i < tierData.length; i++) {
+ if (['y', 'Y', '\u2713', '\u2714'].includes(tierData[i].trim())) {
+ if (!tierNames[i]) throw new Chat.ErrorMessage(`Invalid tier data found in the pastebin.`);
+ if (tierNames[i].length > 30) throw new Chat.ErrorMessage(`Tier names must be 30 characters or less.`);
+ tiers.push(tierNames[i]);
+ }
+ }
+ if (name.length > 25) throw new Chat.ErrorMessage(`Player names must be 25 characters or less.`);
+ const player: Player = {
+ id: toID(name),
+ name,
+ price: 0,
+ };
+ if (tiers.length) player.tiers = tiers;
+ playerList[player.id] = player;
+ }
+ this.playerList = playerList;
+ }
+
+ addPlayerToAuction(name: string, tiers?: string[]) {
+ if (this.state !== 'setup' && this.state !== 'nom') {
+ throw new Chat.ErrorMessage(`You cannot add players to the auction right now.`);
+ }
+ if (name.length > 25) throw new Chat.ErrorMessage(`Player names must be 25 characters or less.`);
+ const player: Player = {
+ id: toID(name),
+ name,
+ price: 0,
+ };
+ if (tiers?.length) {
+ if (tiers.some(tier => tier.length > 30)) {
+ throw new Chat.ErrorMessage(`Tier names must be 30 characters or less.`);
+ }
+ player.tiers = tiers;
+ }
+ this.playerList[player.id] = player;
+ return player;
+ }
+
+ removePlayerFromAuction(name: string) {
+ if (this.state !== 'setup' && this.state !== 'nom') {
+ throw new Chat.ErrorMessage(`You cannot remove players from the auction right now.`);
+ }
+ const player = this.playerList[toID(name)];
+ if (!player) throw new Chat.ErrorMessage(`Player "${name}" not found.`);
+ delete this.playerList[player.id];
+ if (this.state !== 'setup' && !Object.values(this.playerList).filter(p => !p.team).length) {
+ this.end('The auction has ended because there are no players remaining in the draft pool.');
+ }
+ return player;
+ }
+
+ assignPlayer(name: string, teamName?: string) {
+ if (this.state !== 'setup' && this.state !== 'nom') {
+ throw new Chat.ErrorMessage(`You cannot assign players to a team right now.`);
+ }
+ const player = this.playerList[toID(name)];
+ if (!player) throw new Chat.ErrorMessage(`Player "${name}" not found.`);
+ if (teamName) {
+ const team = this.teams[toID(teamName)];
+ if (!team) throw new Chat.ErrorMessage(`Team "${teamName}" not found.`);
+ player.team = team;
+ if (!Object.values(this.playerList).filter(p => !p.team).length) {
+ return this.end('There are no players remaining in the draft pool, so the auction has ended.');
+ }
+ } else {
+ delete player.team;
+ }
+ this.sendHTMLBox(this.generateAuctionTable());
+ }
+
+ addTeam(name: string) {
+ if (this.state !== 'setup') throw new Chat.ErrorMessage(`You cannot add teams after the auction has started.`);
+ if (name.length > 40) throw new Chat.ErrorMessage(`Team names must be 40 characters or less.`);
+ const team = new Team(name, this);
+ this.teams[team.id] = team;
+ this.queue = Object.values(this.teams).map(toID).concat(Object.values(this.teams).map(toID).reverse());
+ return team;
+ }
+
+ removeTeam(name: string) {
+ if (this.state !== 'setup') throw new Chat.ErrorMessage(`You cannot remove teams after the auction has started.`);
+ const team = this.teams[toID(name)];
+ if (!team) throw new Chat.ErrorMessage(`Team "${name}" not found.`);
+ this.queue = this.queue.filter(id => id !== team.id);
+ delete this.teams[team.id];
+ return team;
+ }
+
+ suspendTeam(name: string) {
+ if (this.state !== 'setup' && this.state !== 'nom') {
+ throw new Chat.ErrorMessage(`You cannot suspend teams right now.`);
+ }
+ const team = this.teams[toID(name)];
+ if (!team) throw new Chat.ErrorMessage(`Team "${name}" not found.`);
+ if (team.suspended) throw new Chat.ErrorMessage(`Team ${name} is already suspended.`);
+ if (this.currentTeam === team) throw new Chat.ErrorMessage(`You cannot suspend the current nominating team.`);
+ team.suspended = true;
+ }
+
+ unsuspendTeam(name: string) {
+ if (this.state !== 'setup' && this.state !== 'nom') {
+ throw new Chat.ErrorMessage(`You cannot unsuspend teams right now.`);
+ }
+ const team = this.teams[toID(name)];
+ if (!team) throw new Chat.ErrorMessage(`Team "${name}" not found.`);
+ if (!team.suspended) throw new Chat.ErrorMessage(`Team ${name} is not suspended.`);
+ team.suspended = false;
+ }
+
+ addManagers(teamName: string, managers: string[]) {
+ const team = this.teams[toID(teamName)];
+ if (!team) throw new Chat.ErrorMessage(`Team "${teamName}" not found.`);
+ for (const manager of managers) {
+ const user = Users.getExact(manager);
+ if (!user) throw new Chat.ErrorMessage(`User "${manager}" not found.`);
+ if (!this.managers[user.id]) {
+ this.managers[user.id] = {id: user.id, team};
+ } else {
+ this.managers[user.id].team = team;
+ }
+ }
+ }
+
+ removeManagers(managers: string[]) {
+ for (const manager of managers) {
+ if (!this.managers[toID(manager)]) throw new Chat.ErrorMessage(`Manager "${manager}" not found`);
+ delete this.managers[toID(manager)];
+ }
+ }
+
+ addCreditsToTeam(teamName: string, amount: number) {
+ if (this.state !== 'setup' && this.state !== 'nom') {
+ throw new Chat.ErrorMessage(`You cannot add credits to a team right now.`);
+ }
+ const team = this.teams[toID(teamName)];
+ if (!team) throw new Chat.ErrorMessage(`Team "${teamName}" not found.`);
+ if (isNaN(amount) || amount % 500 !== 0) {
+ throw new Chat.ErrorMessage(`The amount of credits must be a multiple of 500.`);
+ }
+ const newCredits = team.credits + amount;
+ if (newCredits <= 0 || newCredits > 10000000) {
+ throw new Chat.ErrorMessage(`A team must have between 0 and 10,000,000 credits.`);
+ }
+ if (team.maxBid(newCredits) < this.minBid) {
+ throw new Chat.ErrorMessage(`A team must have enough credits to draft the minimum amount of players.`);
+ }
+ team.credits = newCredits;
+ }
+
+ start() {
+ if (this.state !== 'setup') throw new Chat.ErrorMessage(`The auction has already been started.`);
+ if (Object.keys(this.teams).length < 2) throw new Chat.ErrorMessage(`The auction needs at least 2 teams to start.`);
+ const problemTeams = [];
+ for (const id in this.teams) {
+ const team = this.teams[id];
+ if (team.maxBid() < this.minBid) problemTeams.push(team.name);
+ }
+ if (problemTeams.length) {
+ throw new Chat.ErrorMessage(`The following teams do not have enough credits to draft the minimum amount of players: ${problemTeams.join(', ')}`);
+ }
+ this.next();
+ }
+
+ reset() {
+ for (const id in this.teams) {
+ const team = this.teams[id];
+ team.credits = this.startingCredits;
+ team.suspended = false;
+ for (const player of team.getPlayers()) {
+ delete player.team;
+ player.price = 0;
+ }
+ }
+ this.lastQueue = null;
+ this.queue = Object.values(this.teams).map(toID).concat(Object.values(this.teams).map(toID).reverse());
+ this.clearTimer();
+ this.state = 'setup';
+ this.sendHTMLBox(this.generateAuctionTable());
+ }
+
+ next() {
+ this.state = 'nom';
+ if (!this.queue.filter(id => !this.teams[id].isSuspended()).length) {
+ return this.end('The auction has ended because there are no teams remaining that can draft players.');
+ }
+ if (!Object.values(this.playerList).filter(p => !p.team).length) {
+ return this.end('The auction has ended because there are no players remaining in the draft pool.');
+ }
+ do {
+ this.currentTeam = this.teams[this.queue.shift()!];
+ this.queue.push(this.currentTeam.id);
+ } while (this.currentTeam.isSuspended());
+ this.sendHTMLBox(this.generateAuctionTable());
+ this.sendMessage(`It is now **${this.currentTeam.name}**'s turn to nominate a player. Managers: ${Chat.toListString(this.currentTeam.getManagerNames())}`);
+ }
+
+ nominate(user: User, target: string) {
+ if (this.state !== 'nom') throw new Chat.ErrorMessage(`You cannot nominate players right now.`);
+ if (!this.managers[user.id]) this.checkOwner(user);
+
+ // For undo
+ this.lastQueue = this.queue.slice();
+ this.lastQueue.unshift(this.lastQueue.pop()!);
+
+ const player = this.playerList[toID(target)];
+ if (!player) throw new Chat.ErrorMessage(`${target} is not a valid player.`);
+ if (player.team) throw new Chat.ErrorMessage(`${player.name} has already been drafted.`);
+ this.currentNom = player;
+ this.state = 'bid';
+ this.currentBid = this.minBid;
+ this.currentBidder = this.currentTeam;
+ this.sendMessage(`${user.name}${this.managers[user.id]?.team === this.currentTeam ? ` from **${this.currentTeam.name}**` : ''} has nominated **${player.name}** for auction. Use /bid to place a bid!`);
+ if (!this.blindMode) this.sendHTMLBox(this.generateBidInfo(), 'bid');
+ this.bidTimer = setInterval(() => this.pokeBidTimer(), 1000);
+ }
+
+ bid(user: User, amount: number) {
+ if (this.state !== 'bid') throw new Chat.ErrorMessage(`There are no players up for auction right now.`);
+ const team = this.managers[user.id]?.team;
+ if (!team) throw new Chat.ErrorMessage(`Only managers can bid on players.`);
+
+ if (amount < 500) amount *= 1000;
+ if (isNaN(amount) || amount % 500 !== 0) throw new Chat.ErrorMessage(`Your bid must be a multiple of 500.`);
+ if (amount > team.maxBid()) throw new Chat.ErrorMessage(`You cannot afford to bid that much.`);
+
+ if (this.blindMode) {
+ if (this.bidsPlaced.has(team)) throw new Chat.ErrorMessage(`Your team has already placed a bid.`);
+ if (amount <= this.minBid) throw new Chat.ErrorMessage(`Your bid must be higher than the minimum bid.`);
+ this.bidsPlaced.add(team);
+ for (const id in this.managers) {
+ if (this.managers[id].team === team) {
+ Users.getExact(id)?.sendTo(this.room, `Your team placed a bid of **${amount}** on **${this.currentNom}**.`);
+ }
+ }
+ if (amount > this.currentBid) {
+ this.currentBid = amount;
+ this.currentBidder = team;
+ }
+ if (this.bidsPlaced.size === Object.keys(this.teams).length) {
+ this.finishCurrentNom();
+ }
+ } else {
+ if (amount <= this.currentBid) throw new Chat.ErrorMessage(`Your bid must be higher than the current bid.`);
+ this.currentBid = amount;
+ this.currentBidder = team;
+ this.sendMessage(`${user.name}[${team.name}]: **${amount}**`);
+ this.sendHTMLBox(this.generateBidInfo(), 'bid');
+ this.clearTimer();
+ this.bidTimer = setInterval(() => this.pokeBidTimer(), 1000);
+ }
+ }
+
+ finishCurrentNom() {
+ this.sendMessage(`**${this.currentBidder.name}** has bought **${this.currentNom.name}** for **${this.currentBid}** credits!`);
+ this.currentBidder.credits -= this.currentBid;
+ this.currentNom.team = this.currentBidder;
+ this.currentNom.price = this.currentBid;
+ this.bidsPlaced.clear();
+ this.clearTimer();
+ this.next();
+ }
+
+ undoLastNom() {
+ if (this.state !== 'nom') throw new Chat.ErrorMessage(`You cannot undo a nomination right now.`);
+ if (!this.lastQueue) throw new Chat.ErrorMessage(`You cannot undo more than one nomination at a time.`);
+ this.queue = this.lastQueue;
+ this.lastQueue = null;
+ this.currentBidder.credits += this.currentBid;
+ delete this.currentNom.team;
+ this.currentNom.price = 0;
+ this.next();
+ }
+
+ clearTimer() {
+ clearInterval(this.bidTimer);
+ this.bidTimeElapsed = 0;
+ }
+
+ pokeBidTimer() {
+ this.bidTimeElapsed++;
+ const timeRemaining = this.bidTimeLimit - this.bidTimeElapsed;
+ if (timeRemaining === 0) {
+ this.finishCurrentNom();
+ } else if (timeRemaining % 10 === 0 || timeRemaining === 5) {
+ this.sendMessage(`__${this.bidTimeLimit - this.bidTimeElapsed} seconds left!__`);
+ }
+ }
+
+ end(message?: string) {
+ this.sendHTMLBox(this.generateAuctionTable());
+ this.sendHTMLBox(this.generatePriceList());
+ if (message) this.sendMessage(message);
+ this.destroy();
+ }
+
+ destroy() {
+ clearInterval(this.bidTimer);
+ super.destroy();
+ }
+}
+
+export const commands: Chat.ChatCommands = {
+ auction: {
+ create(target, room, user) {
+ room = this.requireRoom();
+ this.checkCan('minigame', null, room);
+ if (room.game) return this.errorReply(`There is already a game of ${room.game.title} in progress in this room.`);
+ if (room.settings.auctionDisabled) return this.errorReply('Auctions are currently disabled in this room.');
+
+ let startingCredits;
+ if (target) {
+ startingCredits = parseInt(target);
+ if (startingCredits < 500) startingCredits *= 1000;
+ if (
+ isNaN(startingCredits) ||
+ startingCredits < 10000 || startingCredits > 10000000 ||
+ startingCredits % 500 !== 0
+ ) {
+ return this.errorReply(`Starting credits must be a multiple of 500 between 10,000 and 10,000,000.`);
+ }
+ }
+ this.addModAction(`An auction was created by ${user.name}.`);
+ this.modlog(`AUCTION CREATE`);
+ const auction = new Auction(room, startingCredits);
+ room.game = auction;
+ auction.addOwner(user);
+ },
+ createhelp: [
+ `/auction create [startingcredits] - Creates an auction. Requires: % @ # &`,
+ ],
+ start(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ auction.start();
+ this.addModAction(`The auction was started by ${user.name}.`);
+ this.modlog(`AUCTION START`);
+ },
+ reset(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ auction.reset();
+ this.addModAction(`The auction was reset by ${user.name}.`);
+ this.modlog(`AUCTION RESET`);
+ },
+ delete: 'end',
+ stop: 'end',
+ end(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ auction.end();
+ this.addModAction(`The auction was ended by ${user.name}.`);
+ this.modlog('AUCTION END');
+ },
+ info: 'display',
+ display(target, room, user) {
+ this.runBroadcast();
+ const auction = this.requireGame(Auction);
+ this.sendReplyBox(auction.generateAuctionTable());
+ },
+ pricelist(target, room, user) {
+ this.runBroadcast();
+ const auction = this.requireGame(Auction);
+ this.sendReplyBox(auction.generatePriceList());
+ },
+ minbid(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction minbid');
+ const amount = parseInt(target);
+ auction.setMinBid(amount);
+ this.addModAction(`${user.name} set the minimum bid to ${amount}.`);
+ this.modlog('AUCTION MINBID', null, `${amount}`);
+ },
+ minbidhelp: [
+ `/auction minbid [amount] - Sets the minimum bid. Requires: # & auction owner`,
+ ],
+ minplayers(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction minplayers');
+ const amount = parseInt(target);
+ auction.setMinPlayers(amount);
+ this.addModAction(`${user.name} set the minimum number of players to ${amount}.`);
+ },
+ minplayershelp: [
+ `/auction minplayers [amount] - Sets the minimum number of players. Requires: # & auction owner`,
+ ],
+ blindmode(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction blindmode');
+ if (this.meansYes(target)) {
+ auction.setBlindMode(true);
+ this.addModAction(`${user.name} turned on blind mode.`);
+ } else if (this.meansNo(target)) {
+ auction.setBlindMode(false);
+ this.addModAction(`${user.name} turned off blind mode.`);
+ }
+ },
+ blindmodehelp: [
+ `/auction blindmode [on/off] - Enables or disables blind mode. Requires: # & auction owner`,
+ `When blind mode is enabled, teams may only place one bid per nomination and only the highest bid is revealed once the timer runs out or after all teams have placed a bid.`,
+ ],
+ async importplayers(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction importplayers');
+ if (!/^https?:\/\/pastebin\.com\/[a-zA-Z0-9]+$/.test(target)) {
+ return this.errorReply('Invalid pastebin URL.');
+ }
+ let data = '';
+ try {
+ data = await Net(`https://pastebin.com/raw/${target.split('/').pop()}`).get();
+ } catch {}
+ if (!data) return this.errorReply('Error fetching data from pastebin.');
+
+ auction.importPlayers(data);
+ this.addModAction(`${user.name} imported the player list from ${target}.`);
+ },
+ importplayershelp: [
+ `/auction importplayers [pastebin url] - Imports a list of players from a pastebin. Requires: # & auction owner`,
+ `The pastebin should be a list of tab-separated values with the first row containing tier names and subsequent rows containing the player names and a Y in the column corresponding to the tier.`,
+ `See https://pastebin.com/jPTbJBva for an example.`,
+ ],
+ addplayer(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction addplayer');
+ const [name, ...tiers] = target.split(',').map(x => x.trim());
+ const player = auction.addPlayerToAuction(name, tiers);
+ this.addModAction(`${user.name} added player ${player.name} to the auction.`);
+ },
+ addplayerhelp: [
+ `/auction addplayer [name], [tier1], [tier2], ... - Adds a player to the auction. Requires: # & auction owner`,
+ ],
+ removeplayer(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction removeplayer');
+ const player = auction.removePlayerFromAuction(target);
+ this.addModAction(`${user.name} removed player ${player.name} from the auction.`);
+ },
+ removeplayerhelp: [
+ `/auction removeplayer [name] - Removes a player from the auction. Requires: # & auction owner`,
+ ],
+ assignplayer(target, room, user) {
+ if (!target) return this.parse('/help auction assignplayer');
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ const [player, team] = target.split(',').map(x => x.trim());
+ if (team) {
+ auction.assignPlayer(player, team);
+ this.addModAction(`${user.name} assigned player ${player} to team ${team}.`);
+ } else {
+ auction.assignPlayer(player);
+ this.sendReply(`${user.name} returned player ${player} to draft pool.`);
+ }
+ },
+ assignplayerhelp: [
+ `/auction assignplayer [player], [team] - Assigns a player to a team. If team is blank, returns player to draft pool. Requires: # & auction owner`,
+ ],
+ addteam(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ const [name, ...managers] = target.split(',').map(x => x.trim());
+ if (!name) return this.parse('/help auction addteam');
+ const team = auction.addTeam(name);
+ auction.addManagers(team.name, managers);
+ this.addModAction(`${user.name} added team ${team.name} to the auction.`);
+ },
+ addteamhelp: [
+ `/auction addteam [name], [manager1], [manager2], ... - Adds a team to the auction. Requires: # & auction owner`,
+ ],
+ removeteam(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction removeteam');
+ const team = auction.removeTeam(target);
+ this.addModAction(`${user.name} removed team ${team.name} from the auction.`);
+ },
+ removeteamhelp: [
+ `/auction removeteam [team] - Removes a team from the auction. Requires: # & auction owner`,
+ ],
+ suspendteam(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction suspendteam');
+ auction.suspendTeam(target);
+ const team = auction.teams[toID(target)];
+ this.addModAction(`${user.name} suspended team ${team.name}.`);
+ },
+ suspendteamhelp: [
+ `/auction suspendteam [team] - Suspends a team from the auction. Requires: # & auction owner`,
+ ],
+ unsuspendteam(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ if (!target) return this.parse('/help auction unsuspendteam');
+ auction.unsuspendTeam(target);
+ const team = auction.teams[toID(target)];
+ this.addModAction(`${user.name} unsuspended team ${team.name}.`);
+ },
+ unsuspendteamhelp: [
+ `/auction unsuspendteam [team] - Unsuspends a team from the auction. Requires: # & auction owner`,
+ ],
+ addmanager: 'addmanagers',
+ addmanagers(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ const [teamName, ...managers] = target.split(',').map(x => x.trim());
+ if (!teamName || !managers.length) return this.parse('/help auction addmanagers');
+ auction.addManagers(teamName, managers);
+ const team = auction.teams[toID(teamName)];
+ this.addModAction(`${user.name} added ${Chat.toListString(managers.map(m => Users.getExact(m)!.name))} as manager${Chat.plural(managers.length)} for team ${team.name}.`);
+ },
+ addmanagershelp: [
+ `/auction addmanagers [team], [manager1], [manager2], ... - Adds managers to a team. Requires: # & auction owner`,
+ ],
+ removemanager: 'removemanagers',
+ removemanagers(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ const [...managers] = target.split(',').map(x => x.trim());
+ if (!managers.length) return this.parse('/help auction removemanagers');
+ auction.removeManagers(managers);
+ this.addModAction(`${user.name} removed ${Chat.toListString(managers.map(m => Users.getExact(m)!.name))} as manager${Chat.plural(managers.length)}.`);
+ },
+ removemanagershelp: [
+ `/auction removemanagers [manager1], [manager2], ... - Removes managers. Requires: # & auction owner`,
+ ],
+ addcredits(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ const [teamName, amount] = target.split(',').map(x => x.trim());
+ if (!teamName || !amount) return this.parse('/help auction addcredits');
+ auction.addCreditsToTeam(teamName, parseInt(amount));
+ const team = auction.teams[toID(teamName)];
+ this.addModAction(`${user.name} added ${amount} credits to team ${team.name}.`);
+ },
+ addcreditshelp: [
+ `/auction addcredits [team], [amount] - Adds credits to a team. Requires: # & auction owner`,
+ ],
+ nom: 'nominate',
+ nominate(target, room, user) {
+ const auction = this.requireGame(Auction);
+ if (!target) return this.parse('/help auction nominate');
+ auction.nominate(user, target);
+ },
+ nominatehelp: [
+ `/auction nominate OR /nom [player] - Nominates a player for auction.`,
+ ],
+ bid(target, room, user) {
+ const auction = this.requireGame(Auction);
+ if (!target) return this.parse('/help auction bid');
+ auction.bid(user, parseFloat(target));
+ },
+ bidhelp: [
+ `/auction bid OR /bid [amount] - Bids on a player for the specified amount. If the amount is less than 500, it will be multiplied by 1000.`,
+ ],
+ undo(target, room, user) {
+ room = this.requireRoom();
+ const auction = this.requireGame(Auction);
+ auction.checkOwner(user);
+
+ auction.undoLastNom();
+ this.addModAction(`${user.name} undid the last nomination.`);
+ },
+ disable(target, room, user) {
+ room = this.requireRoom();
+ this.checkCan('gamemanagement', null, room);
+ if (room.settings.auctionDisabled) {
+ return this.errorReply('Auctions are already disabled.');
+ }
+ room.settings.auctionDisabled = true;
+ room.saveSettings();
+ this.sendReply('Auctions have been disabled for this room.');
+ },
+ enable(target, room, user) {
+ room = this.requireRoom();
+ this.checkCan('gamemanagement', null, room);
+ if (!room.settings.auctionDisabled) {
+ return this.errorReply('Auctions are already enabled.');
+ }
+ delete room.settings.auctionDisabled;
+ room.saveSettings();
+ this.sendReply('Auctions have been enabled for this room.');
+ },
+ ongoing: 'running',
+ running(target, room, user) {
+ room = this.requireRoom();
+ if (!this.runBroadcast()) return;
+ const runningAuctions = [];
+ for (const auctionRoom of Rooms.rooms.values()) {
+ const auction = auctionRoom.getGame(Auction);
+ if (!auction) continue;
+ runningAuctions.push(auctionRoom.title);
+ }
+ this.sendReply(`Running auctions: ${runningAuctions.join(', ') || 'None'}`);
+ },
+ '': 'help',
+ help(target, room, user) {
+ this.parse('/help auction');
+ },
+ },
+ auctionhelp(target, room, user) {
+ if (!this.runBroadcast()) return;
+ this.sendReplyBox(
+ `Auction commands
` +
+ `- create [startingcredits]: Creates an auction.
` +
+ `- start: Starts the auction.
` +
+ `- reset: Resets the auction.
` +
+ `- end: Ends the auction.
` +
+ `- running: Shows a list of rooms with running auctions.
` +
+ `- display: Displays the current state of the auction.
` +
+ `- pricelist: Displays the current prices of players by team.
` +
+ `- nom [player]: Nominates a player for auction.
` +
+ `- bid [amount]: Bids on a player for the specified amount. If the amount is less than 500, it will be multiplied by 1000.
` +
+ `You may use /bid and /nom directly without the /auction prefix.
` +
+ `Configuration Commands
` +
+ `- minbid [amount]: Sets the minimum bid.
` +
+ `- minplayers [amount]: Sets the minimum number of players.
` +
+ `- blindmode [on/off]: Enables or disables blind mode.
` +
+ `- importplayers [pastebin url]: Imports a list of players from a pastebin.
` +
+ `- addplayer [name], [tier1], [tier2], ...: Adds a player to the auction.
` +
+ `- removeplayer [name]: Removes a player from the auction.
` +
+ `- assignplayer [player], [team]: Assigns a player to a team. If team is blank, returns player to draft pool.
` +
+ `- addteam [name], [manager1], [manager2], ...: Adds a team to the auction.
` +
+ `- removeteam [name]: Removes the given team from the auction.
` +
+ `- suspendteam [name]: Suspends the given team from the auction.
` +
+ `- unsuspendteam [name]: Unsuspends the given team from the auction.
` +
+ `- addmanagers [team], [manager1], [manager2], ...: Adds managers to a team.
` +
+ `- removemanagers [manager1], [manager2], ...: Removes managers.
` +
+ `- addcredits [team], [amount]: Adds credits to a team.
` +
+ `- undo: Undoes the last nomination.
` +
+ `- [enable/disable]: Enables or disables auctions from being started in a room.
` +
+ ``
+ );
+ },
+ nom(target) {
+ this.parse(`/auction nominate ${target}`);
+ },
+ bid(target) {
+ this.parse(`/auction bid ${target}`);
+ },
+ overpay() {
+ this.requireGame(Auction);
+ return '/announce OVERPAY!';
+ },
+};
+
+export const roomSettings: Chat.SettingsHandler = room => ({
+ label: "Auction",
+ permission: 'editroom',
+ options: [
+ [`disabled`, room.settings.auctionDisabled || 'auction disable'],
+ [`enabled`, !room.settings.auctionDisabled || 'auction enable'],
+ ],
+});
diff --git a/server/chat-plugins/chat-monitor.ts b/server/chat-plugins/chat-monitor.ts
index e22135150c82..42a74907de15 100644
--- a/server/chat-plugins/chat-monitor.ts
+++ b/server/chat-plugins/chat-monitor.ts
@@ -428,7 +428,7 @@ export const namefilter: Chat.NameFilter = (name, user) => {
if (Punishments.namefilterwhitelist.has(id)) return name;
if (Monitor.forceRenames.has(id)) {
if (typeof Monitor.forceRenames.get(id) === 'number') {
- // we check this for hotpatching reasons, since on the initial chat patch this will still be a Utils.MultiSet
+ // we check this for hotpatching reasons, since on the initial chat patch this will still be a Utils.Multiset
// we're gonna assume no one has seen it since that covers people who _haven't_ actually, and those who have
// likely will not be attempting to log into it
Monitor.forceRenames.set(id, false);
diff --git a/server/chat-plugins/chatlog.ts b/server/chat-plugins/chatlog.ts
index 2ceb33b09d9e..9783b7323510 100644
--- a/server/chat-plugins/chatlog.ts
+++ b/server/chat-plugins/chatlog.ts
@@ -87,9 +87,10 @@ export class LogReaderRoom {
async getLog(day: string) {
if (roomlogTable) {
+ const [dayStart, dayEnd] = LogReader.dayToRange(day);
const logs = await roomlogTable.selectAll(
['log', 'time']
- )`WHERE roomid = ${this.roomid} AND time::DATE = ${day}`;
+ )`WHERE roomid = ${this.roomid} AND time BETWEEN ${dayStart}::int::timestamp AND ${dayEnd}::int::timestamp`;
return new Streams.ObjectReadStream({
read(this: Streams.ObjectReadStream) {
for (const {log, time} of logs) {
@@ -177,6 +178,23 @@ export const LogReader = new class {
return {official, normal, hidden, secret, deleted, personal, deletedPersonal};
}
+ /** @returns [dayStart, dayEnd] as seconds (NOT milliseconds) since Unix epoch */
+ dayToRange(day: string): [number, number] {
+ const nextDay = LogReader.nextDay(day);
+ return [
+ Math.trunc(new Date(day).getTime() / 1000),
+ Math.trunc(new Date(nextDay).getTime() / 1000),
+ ];
+ }
+ /** @returns [monthStart, monthEnd] as seconds (NOT milliseconds) since Unix epoch */
+ monthToRange(month: string): [number, number] {
+ const nextMonth = LogReader.nextMonth(month);
+ return [
+ Math.trunc(new Date(`${month}-01`).getTime() / 1000),
+ Math.trunc(new Date(`${nextMonth}-01`).getTime() / 1000),
+ ];
+ }
+
getMonth(day?: string) {
if (!day) day = Chat.toTimestamp(new Date()).split(' ')[0];
return day.slice(0, 7);
@@ -803,14 +821,15 @@ export class RipgrepLogSearcher extends FSLogSearcher {
}
export class DatabaseLogSearcher extends Searcher {
- async searchLinecounts(roomid: RoomID, monthString: string, user?: ID) {
+ async searchLinecounts(roomid: RoomID, month: string, user?: ID) {
user = toID(user);
if (!Rooms.Roomlogs.table) throw new Error(`Database search made while database is disabled.`);
const results: {[date: string]: {[user: string]: number}} = {};
- const [year, month] = monthString.split('-').map(Number);
+ const [monthStart, monthEnd] = LogReader.monthToRange(month);
const rows = await Rooms.Roomlogs.table.selectAll()`
- WHERE EXTRACT("year" FROM time::DATE) = ${year} AND EXTRACT("month" FROM time::DATE) = ${month} AND
- roomid = ${roomid} AND type = ${'c'}${user ? SQL` AND userid = ${user}` : SQL``}
+ WHERE ${user ? SQL`userid = ${user} AND ` : SQL``}roomid = ${roomid} AND
+ time BETWEEN ${monthStart}::int::timestamp AND ${monthEnd}::int::timestamp AND
+ type = ${'c'}
`;
for (const row of rows) {
@@ -823,7 +842,7 @@ export class DatabaseLogSearcher extends Searcher {
results[day][row.userid]++;
}
- return this.renderLinecountResults(results, roomid, monthString, user);
+ return this.renderLinecountResults(results, roomid, month, user);
}
activityStats(room: RoomID, month: string): Promise<{average: RoomStats, days: RoomStats[]}> {
throw new Chat.ErrorMessage('This is not yet implemented for the new logs database.');
diff --git a/server/chat-plugins/friends.ts b/server/chat-plugins/friends.ts
index 53cfe28aacb5..fc9b827518e4 100644
--- a/server/chat-plugins/friends.ts
+++ b/server/chat-plugins/friends.ts
@@ -145,9 +145,7 @@ export const Friends = new class {
buf += `On an alternate account
`;
}
if (login && typeof login === 'number' && !user?.connected) {
- // THIS IS A TERRIBLE HACK BUT IT WORKS OKAY
- const time = Chat.toTimestamp(new Date(Number(login)), {human: true});
- buf += `Last seen: ${time.split(' ').reverse().join(', on ')}`;
+ buf += `Last seen: `;
buf += ` (${Chat.toDurationString(Date.now() - login, {precision: 1})} ago)`;
} else if (typeof login === 'string') {
buf += `${login}`;
diff --git a/server/chat-plugins/modlog-viewer.ts b/server/chat-plugins/modlog-viewer.ts
index cef81f7ea48a..3afcdbef1a8b 100644
--- a/server/chat-plugins/modlog-viewer.ts
+++ b/server/chat-plugins/modlog-viewer.ts
@@ -389,7 +389,7 @@ export const pages: Chat.PageTable = {
if (entry.ip) {
let ipTable = punishmentsByIp.get(entry.ip);
if (!ipTable) {
- ipTable = new Utils.Multiset();
+ ipTable = new Utils.Multiset();
punishmentsByIp.set(entry.ip, ipTable);
}
ipTable.add(entry.action);
@@ -448,7 +448,7 @@ export const pages: Chat.PageTable = {
for (const [ip, table] of punishmentsByIp) {
buf += `${ip} `;
for (const key of keys) {
- buf += `${table.get(key) || 0} `;
+ buf += `${table.get(key)} `;
}
buf += ` `;
}
diff --git a/server/chat-plugins/othermetas.ts b/server/chat-plugins/othermetas.ts
index 7a4905469ace..3878a8b8a049 100644
--- a/server/chat-plugins/othermetas.ts
+++ b/server/chat-plugins/othermetas.ts
@@ -2,7 +2,7 @@
* Other Metagames chat plugin
* Lets users see elements of Pokemon in various Other Metagames.
* Originally by Spandan.
- * @author Kris
+ * @author dhelmise
*/
import {Utils} from '../../lib';
diff --git a/server/chat-plugins/randombattles/index.ts b/server/chat-plugins/randombattles/index.ts
index 5d86f13f24e2..580b22f2baae 100644
--- a/server/chat-plugins/randombattles/index.ts
+++ b/server/chat-plugins/randombattles/index.ts
@@ -1,6 +1,6 @@
/**
* Random Battles chat-plugin
- * Written by Kris with inspiration from sirDonovan and The Immortal
+ * Written by dhelmise with inspiration from sirDonovan and The Immortal
*
* Set probability code written by Annika
*/
@@ -143,6 +143,11 @@ function formatItem(item: Item | string) {
}
}
+function formatType(type: TypeInfo | string) {
+ type = Dex.types.get(type);
+ return type.name;
+}
+
/**
* Gets the sets for a Pokemon for a format that uses the new schema.
* Old formats will use getData()
@@ -155,8 +160,11 @@ function getSets(species: string | Species, format: string | Format = 'gen9rando
format = Dex.formats.get(format);
species = dex.species.get(species);
const isDoubles = format.gameType === 'doubles';
+ let folderName = format.mod;
+ if (format.team === 'randomBaby') folderName += 'baby';
+ if (species.isNonstandard === 'CAP') folderName += 'cap';
const setsFile = JSON.parse(
- FS(`data/random-battles/${format.mod}/${isDoubles ? `doubles-` : ``}sets.json`)
+ FS(`data/random-battles/${folderName}/${isDoubles ? 'doubles-' : ''}sets.json`)
.readIfExistsSync() || '{}'
);
const data = setsFile[species.id];
@@ -316,41 +324,79 @@ function battleFactorySets(species: string | Species, tier: string | null, gen =
const format = Dex.formats.get(`${gen}bssfactory`);
if (!(species.id in statsFile)) return {e: `${species.name} doesn't have any sets in ${format.name}.`};
const setObj = statsFile[species.id];
- buf += `Sets for ${species.name} in ${format.name}:
`;
- for (const [i, set] of setObj.sets.entries()) {
- buf += `Set ${i + 1}
`;
- buf += ``;
- buf += `- ${set.species}${set.gender ? ` (${set.gender})` : ``} @ ${Array.isArray(set.item) ? set.item.map(formatItem).join(" / ") : formatItem(set.item)}
`;
- buf += `- Ability: ${Array.isArray(set.ability) ? set.ability.map(formatAbility).join(" / ") : formatAbility(set.ability)}
`;
- if (!set.level) buf += `- Level: 50
`;
- if (set.level && set.level < 50) buf += `- Level: ${set.level}
`;
- if (set.shiny) buf += `- Shiny: Yes
`;
- if (set.happiness) buf += `- Happiness: ${set.happiness}
`;
- if (set.evs) {
- buf += `- EVs: `;
- const evs: string[] = [];
- let ev: string;
- for (ev in set.evs) {
- if (set.evs[ev] === 0) continue;
- evs.push(`${set.evs[ev]} ${STAT_NAMES[ev]}`);
+ if (genNum >= 9) {
+ buf += `Species rarity: ${setObj.weight} (higher is more common, max 10)
`;
+ buf += `Sets for ${species.name} in ${format.name}:
`;
+ for (const [i, set] of setObj.sets.entries()) {
+ buf += `Set ${i + 1} (${set.weight}%)
`;
+ buf += ``;
+ buf += `- ${Dex.forFormat(format).species.get(set.species).name} @ ${set.item.map(formatItem).join(" / ")}
`;
+ buf += `- Ability: ${set.ability.map(formatAbility).join(" / ")}
`;
+ buf += `- Level: 50
`;
+ buf += `- Tera Type: ${set.teraType.map(formatType).join(' / ')}
`;
+ if (set.evs) {
+ buf += `- EVs: `;
+ const evs: string[] = [];
+ let ev: string;
+ for (ev in set.evs) {
+ if (!set.evs[ev]) continue;
+ evs.push(`${set.evs[ev]} ${STAT_NAMES[ev]}`);
+ }
+ buf += `${evs.join(" / ")}
`;
}
- buf += `${evs.join(" / ")}
`;
- }
- buf += `- ${Array.isArray(set.nature) ? set.nature.map(formatNature).join(" / ") : formatNature(set.nature)} Nature
`;
- if (set.ivs) {
- buf += `- IVs: `;
- const ivs: string[] = [];
- let iv: string;
- for (iv in set.ivs) {
- if (set.ivs[iv] === 31) continue;
- ivs.push(`${set.ivs[iv]} ${STAT_NAMES[iv]}`);
+ buf += `
- ${formatNature(set.nature)} Nature
`;
+ if (set.ivs) {
+ buf += `- IVs: `;
+ const ivs: string[] = [];
+ let iv: string;
+ for (iv in set.ivs) {
+ if (set.ivs[iv] === 31) continue;
+ ivs.push(`${set.ivs[iv]} ${STAT_NAMES[iv]}`);
+ }
+ buf += `${ivs.join(" / ")}
`;
}
- buf += `${ivs.join(" / ")}`;
+ for (const moveSlot of set.moves) {
+ buf += `- - ${moveSlot.map(formatMove).join(' / ')}
`;
+ }
+ buf += `
`;
}
- for (const moveid of set.moves) {
- buf += `- ${Array.isArray(moveid) ? moveid.map(formatMove).join(" / ") : formatMove(moveid)} `;
+ } else {
+ buf += `Sets for ${species.name} in ${format.name}:
`;
+ for (const [i, set] of setObj.sets.entries()) {
+ buf += `Set ${i + 1}
`;
+ buf += ``;
+ buf += `- ${set.species}${set.gender ? ` (${set.gender})` : ``} @ ${Array.isArray(set.item) ? set.item.map(formatItem).join(" / ") : formatItem(set.item)}
`;
+ buf += `- Ability: ${Array.isArray(set.ability) ? set.ability.map(formatAbility).join(" / ") : formatAbility(set.ability)}
`;
+ if (!set.level) buf += `- Level: 50
`;
+ if (set.level && set.level < 50) buf += `- Level: ${set.level}
`;
+ if (set.shiny) buf += `- Shiny: Yes
`;
+ if (set.happiness) buf += `- Happiness: ${set.happiness}
`;
+ if (set.evs) {
+ buf += `- EVs: `;
+ const evs: string[] = [];
+ let ev: string;
+ for (ev in set.evs) {
+ if (set.evs[ev] === 0) continue;
+ evs.push(`${set.evs[ev]} ${STAT_NAMES[ev]}`);
+ }
+ buf += `${evs.join(" / ")}
`;
+ }
+ buf += `- ${Array.isArray(set.nature) ? set.nature.map(formatNature).join(" / ") : formatNature(set.nature)} Nature
`;
+ if (set.ivs) {
+ buf += `- IVs: `;
+ const ivs: string[] = [];
+ let iv: string;
+ for (iv in set.ivs) {
+ if (set.ivs[iv] === 31) continue;
+ ivs.push(`${set.ivs[iv]} ${STAT_NAMES[iv]}`);
+ }
+ buf += `${ivs.join(" / ")}
`;
+ }
+ for (const moveid of set.moves) {
+ buf += `- - ${Array.isArray(moveid) ? moveid.map(formatMove).join(" / ") : formatMove(moveid)}
`;
+ }
+ buf += `
`;
}
- buf += ``;
}
}
return buf;
@@ -414,16 +460,20 @@ export const commands: Chat.ChatCommands = {
randbats: 'randombattles',
randomdoublesbattle: 'randombattles',
randdubs: 'randombattles',
+ babyrandombattle: 'randombattles',
+ babyrands: 'randombattles',
// randombattlenodmax: 'randombattles',
// randsnodmax: 'randombattles',
randombattles(target, room, user, connection, cmd) {
if (!this.runBroadcast()) return;
const battle = room?.battle;
let isDoubles = cmd === 'randomdoublesbattle' || cmd === 'randdubs';
+ let isBaby = cmd === 'babyrandombattle' || cmd === 'babyrands';
let isNoDMax = cmd.includes('nodmax');
if (battle) {
if (battle.format.includes('nodmax')) isNoDMax = true;
if (battle.format.includes('doubles') || battle.gameType === 'freeforall') isDoubles = true;
+ if (battle.format.includes('baby')) isBaby = true;
}
const args = target.split(',');
@@ -445,9 +495,11 @@ export const commands: Chat.ChatCommands = {
}
const species = dex.species.get(searchResults[0].name);
const extraFormatModifier = isLetsGo ? 'letsgo' : (dex.currentMod === 'gen8bdsp' ? 'bdsp' : '');
+ const babyModifier = isBaby ? 'baby' : '';
const doublesModifier = isDoubles ? 'doubles' : '';
const noDMaxModifier = isNoDMax ? 'nodmax' : '';
- const format = dex.formats.get(`gen${dex.gen}${extraFormatModifier}random${doublesModifier}battle${noDMaxModifier}`);
+ const formatName = `gen${dex.gen}${extraFormatModifier}${babyModifier}random${doublesModifier}battle${noDMaxModifier}`;
+ const format = dex.formats.get(formatName);
const movesets = [];
let setCount = 0;
@@ -543,7 +595,7 @@ export const commands: Chat.ChatCommands = {
if (!species.exists) {
return this.errorReply(`Error: Pok\u00e9mon '${args[0].trim()}' not found.`);
}
- let mod = 'gen8';
+ let mod = 'gen9';
if (args[1] && toID(args[1]) in Dex.dexes && Dex.dexes[toID(args[1])].gen >= 7) mod = toID(args[1]);
const bssSets = battleFactorySets(species, null, mod, true);
if (!bssSets) return this.parse(`/help battlefactory`);
@@ -589,7 +641,7 @@ export const commands: Chat.ChatCommands = {
battlefactoryhelp: [
`/battlefactory [pokemon], [tier], [gen] - Displays a Pok\u00e9mon's Battle Factory sets. Supports Gens 6-8. Defaults to Gen 8. If no tier is provided, defaults to OU.`,
`- Supported tiers: OU, Ubers, UU, RU, NU, PU, Monotype (Gen 7 only), LC (Gen 7 only)`,
- `/bssfactory [pokemon], [gen] - Displays a Pok\u00e9mon's BSS Factory sets. Supports Gen 7-8. Defaults to Gen 8.`,
+ `/bssfactory [pokemon], [gen] - Displays a Pok\u00e9mon's BSS Factory sets. Supports Gen 7-9. Defaults to Gen 9.`,
],
cap1v1(target, room, user) {
diff --git a/server/chat-plugins/randombattles/winrates.ts b/server/chat-plugins/randombattles/winrates.ts
index 4e3ba6cf1466..b2a9ac8a307a 100644
--- a/server/chat-plugins/randombattles/winrates.ts
+++ b/server/chat-plugins/randombattles/winrates.ts
@@ -46,6 +46,7 @@ function getDefaultStats() {
// so i'm not spending the time to add commands to toggle this
gen9randombattle: {mons: {}},
gen9randomdoublesbattle: {mons: {}},
+ gen9babyrandombattle: {mons: {}},
gen9superstaffbrosultimate: {mons: {}},
gen8randombattle: {mons: {}},
gen7randombattle: {mons: {}},
@@ -135,6 +136,16 @@ function getSpeciesName(set: PokemonSet, format: Format) {
return item.megaStone;
} else if (species === "Rayquaza" && moves.includes('Dragon Ascent') && !item.zMove && megaRayquazaPossible) {
return "Rayquaza-Mega";
+ } else if (species === "Poltchageist-Artisan") { // Babymons from here on out
+ return "Poltchageist";
+ } else if (species === "Shellos-East") {
+ return "Shellos";
+ } else if (species === "Sinistea-Antique") {
+ return "Sinistea";
+ } else if (species.startsWith("Deerling-")) {
+ return "Deerling";
+ } else if (species.startsWith("Flabe\u0301be\u0301-")) {
+ return "Flabe\u0301be\u0301";
} else {
return species;
}
@@ -168,8 +179,8 @@ async function collectStats(battle: RoomBattle, winner: ID, players: ID[]) {
eloFloor = 1150;
} else if (format.mod !== `gen${Dex.gen}`) {
eloFloor = 1300;
- } else if (format.gameType === 'doubles') {
- // may need to be raised again if doubles ladder takes off
+ } else if (format.gameType === 'doubles' || format.team === 'randomBaby') {
+ // may need to be raised again if either ladder takes off
eloFloor = 1300;
}
if (!formatData || (format.mod !== 'gen9ssb' && battle.rated < eloFloor) || !winner) return;
diff --git a/server/chat-plugins/scavenger-games.ts b/server/chat-plugins/scavenger-games.ts
index 2cd9cc628586..927ed3c75158 100644
--- a/server/chat-plugins/scavenger-games.ts
+++ b/server/chat-plugins/scavenger-games.ts
@@ -92,10 +92,9 @@ class Leaderboard {
async htmlLadder(): Promise {
const data = await this.visualize('points');
- const display = `Rank Name Points ${data.map(line =>
+ return `Rank Name Points ${data.map(line =>
`${line.rank} ${line.name} ${line.points} `).join('')
}
`;
- return display;
}
}
@@ -517,11 +516,8 @@ const TWISTS: {[k: string]: Twist} = {
const mines: {mine: string, users: string[]}[][] = [];
- for (let index = 0; index < this.mines.length; index++) {
- mines[index] = [];
- for (const mine of this.mines[index]) {
- mines[index].push({mine: mine.substr(1), users: []});
- }
+ for (const mineSet of this.mines as string[][]) {
+ mines.push(mineSet.map(mine => ({mine: mine.substr(1), users: [] as string[]})));
}
for (const player of Object.values(this.playerTable)) {
@@ -555,6 +551,7 @@ const TWISTS: {[k: string]: Twist} = {
const mines: string[] = this.mines[q];
for (const [playerId, guesses] of Object.entries(guessObj)) {
const player = this.playerTable[playerId];
+ if (!player) continue;
if (!player.mines) player.mines = [];
(player.mines as {index: number, mine: string}[]).push(...mines
.filter(mine => (guesses as Set).has(toID(mine)))
diff --git a/server/chat-plugins/seasons.ts b/server/chat-plugins/seasons.ts
index 3f4ef88d5b5b..06a20e427145 100644
--- a/server/chat-plugins/seasons.ts
+++ b/server/chat-plugins/seasons.ts
@@ -62,6 +62,17 @@ export function getBadges(user: User, curFormat: string) {
return userBadges;
}
+function getUserHTML(user: User, format: string) {
+ const buf = `${user.name} `;
+ const badgeType = getBadges(user, format).filter(x => x.format === format)[0]?.type;
+ if (badgeType) {
+ let formatType = format.split(/gen\d+/)[1];
+ if (!['ou', 'randombattle'].includes(formatType)) formatType = 'rotating';
+ return `` + buf;
+ }
+ return buf;
+}
+
export function setFormatSchedule() {
// guard heavily against this being overwritten
if (data.current.formatsGeneratedAt === getYear()) return;
@@ -346,10 +357,14 @@ export const handlers: Chat.Handlers = {
room.setPrivate(false);
const seasonRoom = Rooms.search('seasondiscussion');
if (seasonRoom) {
- const players = Object.keys(room.battle.playerTable).map(toID);
+ const p1html = getUserHTML(user, room.battle.format);
+ const otherPlayer = user.id === room.battle.p1.id ? room.battle.p2 : room.battle.p1;
+ const otherUser = otherPlayer.getUser();
+ const p2html = otherUser ? getUserHTML(otherUser, room.battle.format) : `${otherPlayer.name} `;
+ const formatName = Dex.formats.get(room.battle.format).name;
seasonRoom.add(
- `|raw|Battle started between ` +
- `${players[0]} and ${players[1]} . (rating: ${room.battle.rated})`
+ `|raw|${formatName} battle started between ` +
+ `${p1html} and ${p2html}. (rating: ${Math.floor(room.battle.rated)})`
).update();
}
}
diff --git a/server/chat-plugins/teams.ts b/server/chat-plugins/teams.ts
index cd60f605c0f1..280d288e736d 100644
--- a/server/chat-plugins/teams.ts
+++ b/server/chat-plugins/teams.ts
@@ -225,6 +225,10 @@ export const TeamsHandler = new class {
return null;
}
rawTeam = Teams.pack(team);
+ if (!rawTeam.trim()) { // extra sanity check
+ connection.popup("Invalid team provided.");
+ return null;
+ }
// the && existing doesn't really matter because we've verified it above, this is just for TS
if (isUpdate && existing) {
const differenceExists = (
diff --git a/server/chat-plugins/the-studio.ts b/server/chat-plugins/the-studio.ts
index aeb7e8b6e71a..604eeb722d20 100644
--- a/server/chat-plugins/the-studio.ts
+++ b/server/chat-plugins/the-studio.ts
@@ -2,8 +2,8 @@
* The Studio room chat-plugin.
* Supports scrobbling and searching for music from last.fm.
* Also supports storing and suggesting recommendations.
- * Written by Kris, loosely based on the concept from bumbadadabum.
- * @author Kris
+ * Written by dhelmise, loosely based on the concept from bumbadadabum.
+ * @author dhelmise
*/
import {FS, Net, Utils} from '../../lib';
diff --git a/server/chat-plugins/wifi.tsx b/server/chat-plugins/wifi.tsx
index cba8b948c9e4..56a85901cbb3 100644
--- a/server/chat-plugins/wifi.tsx
+++ b/server/chat-plugins/wifi.tsx
@@ -1,7 +1,8 @@
/**
* Wi-Fi chat-plugin. Only works in a room with id 'wifi'
* Handles giveaways in the formats: question, lottery, gts
- * Written by Kris and bumbadadabum, based on the original plugin as written by Codelegend, SilverTactic, DanielCranham
+ * Written by dhelmise and bumbadadabum, based on the original
+ * plugin as written by Codelegend, SilverTactic, DanielCranham
*/
import {FS, Utils} from '../../lib';
@@ -449,7 +450,7 @@ export class QuestionGiveaway extends Giveaway {
if (Giveaway.checkBanned(this.room, user)) return user.sendTo(this.room, "You are banned from entering giveaways.");
if (this.checkExcluded(user)) return user.sendTo(this.room, "You are disallowed from entering the giveaway.");
- if ((this.answered.get(user.id) ?? 0) >= 3) {
+ if (this.answered.get(user.id) >= 3) {
return user.sendTo(
this.room,
"You have already guessed three times. You cannot guess anymore in this.giveaway."
@@ -468,7 +469,7 @@ export class QuestionGiveaway extends Giveaway {
this.joined.set(user.latestIp, user.id);
this.answered.add(user.id);
- if ((this.answered.get(user.id) ?? 0) >= 3) {
+ if (this.answered.get(user.id) >= 3) {
user.sendTo(
this.room,
`Your guess '${guess}' is wrong. You have used up all of your guesses. Better luck next time!`
@@ -1024,7 +1025,7 @@ export const commands: Chat.ChatCommands = {
},
},
gtshelp: [
- `GTS giveaways are currently disabled. If you are a Room Owner and would like them to be re-enabled, contact Kris.`,
+ `GTS giveaways are currently disabled. If you are a Room Owner and would like them to be re-enabled, contact dhelmise.`,
],
ga: 'giveaway',
giveaway: {
diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts
index 78cf21258ede..d92c9c17f36d 100644
--- a/server/chat-plugins/youtube.ts
+++ b/server/chat-plugins/youtube.ts
@@ -10,7 +10,9 @@ import {Utils, FS, Net} from '../../lib';
const ROOT = 'https://www.googleapis.com/youtube/v3/';
const STORAGE_PATH = 'config/chat-plugins/youtube.json';
-const GROUPWATCH_ROOMS = ['youtube', 'pokemongames', 'videogames', 'smashbros', 'pokemongo', 'hindi'];
+const GROUPWATCH_ROOMS = [
+ 'youtube', 'pokemongames', 'videogames', 'smashbros', 'pokemongo', 'hindi', 'franais', 'arcade',
+];
export const videoDataCache: Map = Chat.oldPlugins.youtube?.videoDataCache || new Map();
export const searchDataCache: Map = Chat.oldPlugins.youtube?.searchDataCache || new Map();
diff --git a/server/global-types.ts b/server/global-types.ts
index 2ab3c4cb2594..d03b45680294 100644
--- a/server/global-types.ts
+++ b/server/global-types.ts
@@ -37,7 +37,7 @@ type RoomGame = Rooms.RoomGame;
type MinorActivity = Rooms.MinorActivity;
type RoomBattle = Rooms.RoomBattle;
type Room = Rooms.Room;
-type RoomID = "" | "lobby" | "staff" | "upperstaff" | "development" | string & {__isRoomID: true};
+type RoomID = "" | "lobby" | "staff" | "upperstaff" | "development" | Lowercase & {__isRoomID: true};
namespace Rooms {
export type GlobalRoomState = import('./rooms').GlobalRoomState;
export type ChatRoom = import('./rooms').ChatRoom;
diff --git a/server/room-battle.ts b/server/room-battle.ts
index d151b5c3c3ba..7dc5fb835fc0 100644
--- a/server/room-battle.ts
+++ b/server/room-battle.ts
@@ -18,6 +18,7 @@ import {RoomGamePlayer, RoomGame} from "./room-game";
import type {Tournament} from './tournaments/index';
import type {RoomSettings} from './rooms';
import type {BestOfGame} from './room-battle-bestof';
+import type {GameTimerSettings} from '../sim/dex-formats';
type ChannelIndex = 0 | 1 | 2 | 3 | 4;
export type PlayerIndex = 1 | 2 | 3 | 4;
diff --git a/server/room-game.ts b/server/room-game.ts
index e93e5620a6fe..0c725312b955 100644
--- a/server/room-game.ts
+++ b/server/room-game.ts
@@ -62,6 +62,7 @@ export class RoomGamePlayer {
* `this.getUser().games`.
*/
readonly id: ID;
+ completed?: boolean;
constructor(user: User | string | null, game: GameClass, num = 0) {
this.num = num;
if (!user) user = num ? `Player ${num}` : `Player`;
diff --git a/server/roomlogs.ts b/server/roomlogs.ts
index 8aea6334e1f1..15c99dae71b2 100644
--- a/server/roomlogs.ts
+++ b/server/roomlogs.ts
@@ -17,7 +17,6 @@ interface RoomlogOptions {
noLogTimes?: boolean;
}
-
interface RoomlogRow {
type: string;
roomid: string;
@@ -28,7 +27,10 @@ interface RoomlogRow {
content: string | null;
}
-export const roomlogDB = global.Config?.replaysdb ? new PGDatabase(Config.replaysdb) : null;
+export const roomlogDB = (() => {
+ if (!global.Config || !Config.replaysdb || Config.disableroomlogdb) return null;
+ return new PGDatabase(Config.replaysdb);
+})();
export const roomlogTable = roomlogDB?.getTable('roomlogs');
/**
@@ -78,6 +80,10 @@ export class Roomlog {
* null = disabled
*/
roomlogStream?: Streams.WriteStream | null;
+ /**
+ * Takes precedence over roomlogStream if it exists.
+ */
+ roomlogTable: typeof roomlogTable;
roomlogFilename: string;
numTruncatedLines: number;
@@ -96,9 +102,7 @@ export class Roomlog {
this.numTruncatedLines = 0;
- if (!Config.replaysdb) {
- void this.setupRoomlogStream(true);
- }
+ this.setupRoomlogStream();
}
getScrollback(channel = 0) {
let log = this.log;
@@ -121,13 +125,14 @@ export class Roomlog {
}
return log.join('\n') + '\n';
}
- async setupRoomlogStream(sync = false) {
- if (this.roomlogStream === null || roomlogTable) return;
- if (!Config.logchat) {
+ setupRoomlogStream() {
+ if (this.roomlogStream === null) return;
+ if (!Config.logchat || this.roomid.startsWith('battle-') || this.roomid.startsWith('game-')) {
this.roomlogStream = null;
return;
}
- if (this.roomid.startsWith('battle-')) {
+ if (roomlogTable) {
+ this.roomlogTable = roomlogTable;
this.roomlogStream = null;
return;
}
@@ -139,12 +144,7 @@ export class Roomlog {
if (relpath === this.roomlogFilename) return;
- if (sync) {
- Monitor.logPath(basepath + monthString).mkdirpSync();
- } else {
- await Monitor.logPath(basepath + monthString).mkdirp();
- if (this.roomlogStream === null) return;
- }
+ Monitor.logPath(basepath + monthString).mkdirpSync();
this.roomlogFilename = relpath;
if (this.roomlogStream) void this.roomlogStream.writeEnd();
this.roomlogStream = Monitor.logPath(basepath + relpath).createAppendStream();
@@ -157,7 +157,7 @@ export class Roomlog {
Monitor.logPath(link0).symlinkToSync(relpath); // intentionally a relative link
Monitor.logPath(link0).renameSync(basepath + 'today.txt');
} catch {} // OS might not support symlinks or atomic rename
- if (!Roomlogs.rollLogTimer) void Roomlogs.rollLogs();
+ if (!Roomlogs.rollLogTimer) Roomlogs.rollLogs();
}
add(message: string) {
this.roomlog(message);
@@ -200,7 +200,8 @@ export class Roomlog {
const userid = toID(parsed.user);
if (userids.includes(userid)) {
if (!cleared.includes(userid)) cleared.push(userid);
- if (this.roomid.startsWith('battle-')) return true; // Don't remove messages in battle rooms to preserve evidence
+ // Don't remove messages in battle rooms to preserve evidence
+ if (!this.roomlogStream && !this.roomlogTable) return true;
if (clearAll) return false;
if (lineCount > 0) {
lineCount--;
@@ -250,8 +251,9 @@ export class Roomlog {
}
}
roomlog(message: string, date = new Date()) {
- message = message.replace(/]* src="data:image\/png;base64,[^">]+"[^>]*>/g, '');
- if (roomlogTable && !(!Config.logchat || this.roomid.startsWith('battle-'))) {
+ if (!Config.logchat) return;
+ message = message.replace(/]* src="data:image\/png;base64,[^">]+"[^>]*>/g, '[img]');
+ if (this.roomlogTable) {
const chatData = this.parseChatLine(message);
const type = message.split('|')[1] || "";
void this.insertLog(SQL`INSERT INTO roomlogs (${{
@@ -274,9 +276,8 @@ export class Roomlog {
}
}
private async insertLog(query: SQLStatement, ignoreFailure = false): Promise {
- if (!roomlogTable) return;
try {
- await roomlogTable.query(query);
+ await this.roomlogTable?.query(query);
} catch (e: any) {
if (e?.code === '42P01') { // table not found
await roomlogDB!._query(FS('databases/schemas/roomlogs.sql').readSync(), []);
@@ -292,13 +293,13 @@ export class Roomlog {
void Rooms.Modlog.write(this.roomid, entry, overrideID);
}
async rename(newID: RoomID): Promise {
- if (roomlogTable) {
- await roomlogTable.updateAll({roomid: this.roomid})`WHERE roomid = ${this.roomid}`;
- return true;
+ await Rooms.Modlog.rename(this.roomid, newID);
+ const roomlogStreamExisted = this.roomlogStream !== null;
+ await this.destroy();
+ if (this.roomlogTable) {
+ await this.roomlogTable.updateAll({roomid: newID})`WHERE roomid = ${this.roomid}`;
} else {
const roomlogPath = `chat`;
- const roomlogStreamExisted = this.roomlogStream !== null;
- await this.destroy();
const [roomlogExists, newRoomlogExists] = await Promise.all([
Monitor.logPath(roomlogPath + `/${this.roomid}`).exists(),
Monitor.logPath(roomlogPath + `/${newID}`).exists(),
@@ -306,30 +307,29 @@ export class Roomlog {
if (roomlogExists && !newRoomlogExists) {
await Monitor.logPath(roomlogPath + `/${this.roomid}`).rename(Monitor.logPath(roomlogPath + `/${newID}`).path);
}
- await Rooms.Modlog.rename(this.roomid, newID);
- this.roomid = newID;
- Roomlogs.roomlogs.set(newID, this);
if (roomlogStreamExisted) {
this.roomlogStream = undefined;
this.roomlogFilename = "";
- await this.setupRoomlogStream(true);
+ this.setupRoomlogStream();
}
- return true;
}
+ Roomlogs.roomlogs.set(newID, this);
+ this.roomid = newID;
+ return true;
}
- static async rollLogs() {
+ static rollLogs() {
if (Roomlogs.rollLogTimer === true) return;
if (Roomlogs.rollLogTimer) {
clearTimeout(Roomlogs.rollLogTimer);
}
Roomlogs.rollLogTimer = true;
for (const log of Roomlogs.roomlogs.values()) {
- await log.setupRoomlogStream();
+ log.setupRoomlogStream();
}
const time = Date.now();
const nextMidnight = new Date(time + 24 * 60 * 60 * 1000);
nextMidnight.setHours(0, 0, 1);
- Roomlogs.rollLogTimer = setTimeout(() => void Roomlog.rollLogs(), nextMidnight.getTime() - time);
+ Roomlogs.rollLogTimer = setTimeout(() => Roomlog.rollLogs(), nextMidnight.getTime() - time);
}
truncate() {
if (this.noAutoTruncate) return;
diff --git a/server/rooms.ts b/server/rooms.ts
index 273b983ed018..4b53568a4ca7 100644
--- a/server/rooms.ts
+++ b/server/rooms.ts
@@ -105,8 +105,8 @@ export interface RoomSettings {
jeopardyDisabled?: boolean;
mafiaDisabled?: boolean;
unoDisabled?: boolean;
- blackjackDisabled?: boolean;
hangmanDisabled?: boolean;
+ auctionDisabled?: boolean;
gameNumber?: number;
highTraffic?: boolean;
spotlight?: string;
@@ -1396,7 +1396,8 @@ export class GlobalRoomState {
for (const room of Rooms.rooms.values()) {
const player = room.game && !room.game.ended && room.game.playerTable[user.id];
if (!player) continue;
-
+ // prevents players from being re-added to games like Scavengers after they've finished
+ if (player.completed) continue;
user.games.add(room.roomid);
player.name = user.name;
user.joinRoom(room.roomid);
diff --git a/server/users.ts b/server/users.ts
index 5f1447e98290..2f29d3480300 100644
--- a/server/users.ts
+++ b/server/users.ts
@@ -188,7 +188,7 @@ function isUsername(name: string) {
function isTrusted(userid: ID) {
if (globalAuth.has(userid)) return userid;
for (const room of Rooms.global.chatRooms) {
- if (room.persist && room.settings.isPrivate !== true && room.auth.isStaff(userid)) {
+ if (room.persist && !room.settings.isPrivate && room.auth.isStaff(userid)) {
return userid;
}
}
diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts
index 9da6173cc418..b2fe1f440574 100644
--- a/sim/battle-actions.ts
+++ b/sim/battle-actions.ts
@@ -790,7 +790,7 @@ export class BattleActions {
boosts[statName2] = 0;
}
target.setBoost(boosts);
- if (move.id === "Spectral Thief") {
+ if (move.id === "spectralthief") {
this.battle.addMove('-anim', pokemon, "Spectral Thief", target);
}
}
diff --git a/sim/battle.ts b/sim/battle.ts
index 99c1286addbf..3a464b7d9252 100644
--- a/sim/battle.ts
+++ b/sim/battle.ts
@@ -1377,7 +1377,7 @@ export class Battle {
if (!this.ended && side.requestState) {
side.emitRequest({wait: true, side: side.getRequestData()});
side.clearChoice();
- if (this.allChoicesDone()) this.commitDecisions();
+ if (this.allChoicesDone()) this.commitChoices();
}
return true;
}
@@ -1439,7 +1439,7 @@ export class Battle {
pokemon.faint(source, effect);
}
- nextTurn() {
+ endTurn() {
this.turn++;
this.lastSuccessfulMoveThisTurn = null;
@@ -1613,7 +1613,7 @@ export class Battle {
// Please remove me once there is client support.
if (this.ruleTable.has('crazyhouserule')) {
for (const side of this.sides) {
- let buf = `raw|${side.name}'s team:
`;
+ let buf = `raw|${Utils.escapeHTML(side.name)}'s team:
`;
for (const pokemon of side.pokemon) {
if (!buf.endsWith('
')) buf += '/';
if (pokemon.fainted) {
@@ -1774,11 +1774,11 @@ export class Battle {
this.add('rated', typeof this.rated === 'string' ? this.rated : '');
}
- if (format.onBegin) format.onBegin.call(this);
+ format.onBegin?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBegin) subFormat.onBegin.call(this);
+ subFormat.onBegin?.call(this);
}
if (this.sides.some(side => !side.pokemon[0])) {
@@ -1789,16 +1789,16 @@ export class Battle {
this.checkEVBalance();
}
- if (format.onTeamPreview) format.onTeamPreview.call(this);
+ format.onTeamPreview?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onTeamPreview) subFormat.onTeamPreview.call(this);
+ subFormat.onTeamPreview?.call(this);
}
this.queue.addChoice({choice: 'start'});
this.midTurn = true;
- if (!this.requestState) this.go();
+ if (!this.requestState) this.turnLoop();
}
restart(send?: (type: string, data: string | string[]) => void) {
@@ -1826,9 +1826,9 @@ export class Battle {
effect: Effect | null = null, isSecondary = false, isSelf = false
) {
if (this.event) {
- if (!target) target = this.event.target;
- if (!source) source = this.event.source;
- if (!effect) effect = this.effect;
+ target ||= this.event.target;
+ source ||= this.event.source;
+ effect ||= this.effect;
}
if (!target?.hp) return 0;
if (!target.isActive) return false;
@@ -2017,18 +2017,18 @@ export class Battle {
effect: 'drain' | 'recoil' | Effect | null = null, instafaint = false
) {
if (this.event) {
- if (!target) target = this.event.target;
- if (!source) source = this.event.source;
- if (!effect) effect = this.effect;
+ target ||= this.event.target;
+ source ||= this.event.source;
+ effect ||= this.effect;
}
return this.spreadDamage([damage], [target], source, effect, instafaint)[0];
}
directDamage(damage: number, target?: Pokemon, source: Pokemon | null = null, effect: Effect | null = null) {
if (this.event) {
- if (!target) target = this.event.target;
- if (!source) source = this.event.source;
- if (!effect) effect = this.effect;
+ target ||= this.event.target;
+ source ||= this.event.source;
+ effect ||= this.effect;
}
if (!target?.hp) return 0;
if (!damage) return 0;
@@ -2078,9 +2078,9 @@ export class Battle {
heal(damage: number, target?: Pokemon, source: Pokemon | null = null, effect: 'drain' | Effect | null = null) {
if (this.event) {
- if (!target) target = this.event.target;
- if (!source) source = this.event.source;
- if (!effect) effect = this.effect;
+ target ||= this.event.target;
+ source ||= this.event.source;
+ effect ||= this.effect;
}
if (effect === 'drain') effect = this.dex.conditions.getByID(effect as ID);
if (damage && damage <= 1) damage = 1;
@@ -2136,22 +2136,21 @@ export class Battle {
return ((previousMod * nextMod + 2048) >> 12) / 4096; // M'' = ((M * M') + 0x800) >> 12
}
- chainModify(numerator: number | number[], denominator?: number) {
+ chainModify(numerator: number | number[], denominator = 1) {
const previousMod = this.trunc(this.event.modifier * 4096);
if (Array.isArray(numerator)) {
denominator = numerator[1];
numerator = numerator[0];
}
- const nextMod = this.trunc(numerator * 4096 / (denominator || 1));
+ const nextMod = this.trunc(numerator * 4096 / denominator);
this.event.modifier = ((previousMod * nextMod + 2048) >> 12) / 4096;
}
- modify(value: number, numerator: number | number[], denominator?: number) {
+ modify(value: number, numerator: number | number[], denominator = 1) {
// You can also use:
// modify(value, [numerator, denominator])
// modify(value, fraction) - assuming you trust JavaScript's floating-point handler
- if (!denominator) denominator = 1;
if (Array.isArray(numerator)) {
denominator = numerator[1];
numerator = numerator[0];
@@ -2413,14 +2412,10 @@ export class Battle {
}
checkWin(faintData?: Battle['faintQueue'][0]) {
- let team1PokemonLeft = this.sides[0].pokemonLeft;
- let team2PokemonLeft = this.sides[1].pokemonLeft;
+ const team1PokemonLeft = this.sides[0].pokemonLeft + (this.sides[0].allySide?.pokemonLeft || 0);
+ const team2PokemonLeft = this.sides[1].pokemonLeft + (this.sides[1].allySide?.pokemonLeft || 0);
const team3PokemonLeft = this.gameType === 'freeforall' && this.sides[2]!.pokemonLeft;
const team4PokemonLeft = this.gameType === 'freeforall' && this.sides[3]!.pokemonLeft;
- if (this.gameType === 'multi') {
- team1PokemonLeft += this.sides[2]!.pokemonLeft;
- team2PokemonLeft += this.sides[3]!.pokemonLeft;
- }
if (!team1PokemonLeft && !team2PokemonLeft && !team3PokemonLeft && !team4PokemonLeft) {
this.win(faintData && this.gen > 4 ? faintData.target.side : null);
return true;
@@ -2777,7 +2772,14 @@ export class Battle {
return false;
}
- go() {
+ /**
+ * Generally called at the beginning of a turn, to go through the
+ * turn one action at a time.
+ *
+ * If there is a mid-turn decision (like U-Turn), this will return
+ * and be called again later to resume the turn.
+ */
+ turnLoop() {
this.add('');
this.add('t:', Math.floor(Date.now() / 1000));
if (this.requestState) this.requestState = '';
@@ -2794,7 +2796,7 @@ export class Battle {
if (this.requestState || this.ended) return;
}
- this.nextTurn();
+ this.endTurn();
this.midTurn = false;
this.queue.clear();
}
@@ -2812,7 +2814,7 @@ export class Battle {
side.emitChoiceError(`Incomplete choice: ${input} - missing other pokemon`);
return false;
}
- if (this.allChoicesDone()) this.commitDecisions();
+ if (this.allChoicesDone()) this.commitChoices();
return true;
}
@@ -2829,12 +2831,16 @@ export class Battle {
side.autoChoose();
}
}
- this.commitDecisions();
+ this.commitChoices();
}
- commitDecisions() {
+ commitChoices() {
this.updateSpeed();
+ // Sometimes you need to make switch choices mid-turn (e.g. U-turn,
+ // fainting). When this happens, the rest of the turn is saved (and not
+ // re-sorted), but the new switch choices are sorted and inserted before
+ // the rest of the turn.
const oldQueue = this.queue.list;
this.queue.clear();
if (!this.allChoicesDone()) throw new Error("Not all choices done");
@@ -2856,7 +2862,9 @@ export class Battle {
side.activeRequest = null;
}
- this.go();
+ this.turnLoop();
+
+ // workaround for tests
if (this.log.length - this.sentLogPos > 500) this.sendUpdates();
}
diff --git a/sim/dex-abilities.ts b/sim/dex-abilities.ts
index ed49ffd0d90a..1de3c5b4f181 100644
--- a/sim/dex-abilities.ts
+++ b/sim/dex-abilities.ts
@@ -1,4 +1,4 @@
-import {PokemonEventMethods} from './dex-conditions';
+import type {PokemonEventMethods, ConditionData} from './dex-conditions';
import {BasicEffect, toID} from './dex-data';
interface AbilityEventMethods {
@@ -25,6 +25,8 @@ export interface AbilityData extends Partial, AbilityEventMethods, Poke
}
export type ModdedAbilityData = AbilityData | Partial & {inherit: true};
+export interface AbilityDataTable {[abilityid: IDEntry]: AbilityData}
+export interface ModdedAbilityDataTable {[abilityid: IDEntry]: ModdedAbilityData}
export class Ability extends BasicEffect implements Readonly {
declare readonly effectType: 'Ability';
diff --git a/sim/dex-conditions.ts b/sim/dex-conditions.ts
index 6cf73e5b6858..ac0f2f856cea 100644
--- a/sim/dex-conditions.ts
+++ b/sim/dex-conditions.ts
@@ -607,6 +607,8 @@ export interface FieldConditionData extends
export type ConditionData = PokemonConditionData | SideConditionData | FieldConditionData;
export type ModdedConditionData = ConditionData & {inherit?: true};
+export interface ConditionDataTable {[id: IDEntry]: ConditionData}
+export interface ModdedConditionDataTable {[id: IDEntry]: ModdedConditionData}
export class Condition extends BasicEffect implements
Readonly {
diff --git a/sim/dex-data.ts b/sim/dex-data.ts
index fc9cd8c2160b..2a61a3189459 100644
--- a/sim/dex-data.ts
+++ b/sim/dex-data.ts
@@ -145,6 +145,17 @@ export class Nature extends BasicEffect implements Readonly> & {inherit: true};
+
+export interface NatureDataTable {[natureid: IDEntry]: NatureData}
+
+
export class DexNatures {
readonly dex: ModdedDex;
readonly natureCache = new Map();
@@ -193,6 +204,17 @@ export class DexNatures {
}
}
+export interface TypeData {
+ damageTaken: {[attackingTypeNameOrEffectid: string]: number};
+ HPdvs?: SparseStatsTable;
+ HPivs?: SparseStatsTable;
+ isNonstandard?: Nonstandard | null;
+}
+
+export type ModdedTypeData = TypeData | Partial> & {inherit: true};
+export interface TypeDataTable {[typeid: IDEntry]: TypeData}
+export interface ModdedTypeDataTable {[typeid: IDEntry]: ModdedTypeData}
+
type TypeInfoEffectType = 'Type' | 'EffectType';
export class TypeInfo implements Readonly {
@@ -308,7 +330,7 @@ export class DexTypes {
}
const idsCache: readonly StatID[] = ['hp', 'atk', 'def', 'spa', 'spd', 'spe'];
-const reverseCache: {readonly [k: string]: StatID} = {
+const reverseCache: {readonly [k: IDEntry]: StatID} = {
__proto: null as any,
"hitpoints": 'hp',
"attack": 'atk',
diff --git a/sim/dex-formats.ts b/sim/dex-formats.ts
index df7baaf03a65..e0bdcb44e673 100644
--- a/sim/dex-formats.ts
+++ b/sim/dex-formats.ts
@@ -11,6 +11,8 @@ export interface FormatData extends Partial, EventMethods {
export type FormatList = (FormatData | {section: string, column?: number})[];
export type ModdedFormatData = FormatData | Omit & {inherit: true};
+export interface FormatDataTable {[id: IDEntry]: FormatData}
+export interface ModdedFormatDataTable {[id: IDEntry]: ModdedFormatData}
type FormatEffectType = 'Format' | 'Ruleset' | 'Rule' | 'ValidatorRule';
@@ -18,6 +20,18 @@ type FormatEffectType = 'Format' | 'Ruleset' | 'Rule' | 'ValidatorRule';
export type ComplexBan = [string, string, number, string[]];
export type ComplexTeamBan = ComplexBan;
+export interface GameTimerSettings {
+ dcTimer: boolean;
+ dcTimerBank: boolean;
+ starting: number;
+ grace: number;
+ addPerTurn: number;
+ maxPerTurn: number;
+ maxFirstTurn: number;
+ timeoutAutoChoose: boolean;
+ accelerate: boolean;
+}
+
/**
* A RuleTable keeps track of the rules that a format has. The key can be:
* - '[ruleid]' the ID of a rule in effect
@@ -69,13 +83,13 @@ export class RuleTable extends Map {
if (this.has(`+basepokemon:${toID(species.baseSpecies)}`)) return false;
if (this.has(`-basepokemon:${toID(species.baseSpecies)}`)) return true;
for (const tagid in Tags) {
- const tag = Tags[tagid];
+ const tag = Tags[tagid as ID];
if (this.has(`-pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return true;
}
}
for (const tagid in Tags) {
- const tag = Tags[tagid];
+ const tag = Tags[tagid as ID];
if (this.has(`+pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return false;
}
@@ -94,13 +108,13 @@ export class RuleTable extends Map {
if (this.has(`+basepokemon:${toID(species.baseSpecies)}`)) return false;
if (this.has(`*basepokemon:${toID(species.baseSpecies)}`)) return true;
for (const tagid in Tags) {
- const tag = Tags[tagid];
+ const tag = Tags[tagid as ID];
if (this.has(`*pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return true;
}
}
for (const tagid in Tags) {
- const tag = Tags[tagid];
+ const tag = Tags[tagid as ID];
if (this.has(`+pokemontag:${tagid}`)) {
if ((tag.speciesFilter || tag.genericFilter)!(species)) return false;
}
diff --git a/sim/dex-items.ts b/sim/dex-items.ts
index 6e67352ebd98..5a1b1c172fbb 100644
--- a/sim/dex-items.ts
+++ b/sim/dex-items.ts
@@ -1,4 +1,4 @@
-import {PokemonEventMethods} from './dex-conditions';
+import type {PokemonEventMethods, ConditionData} from './dex-conditions';
import {BasicEffect, toID} from './dex-data';
interface FlingData {
@@ -17,6 +17,9 @@ export type ModdedItemData = ItemData | Partial> & {
onCustap?: (this: Battle, pokemon: Pokemon) => void,
};
+export interface ItemDataTable {[itemid: IDEntry]: ItemData}
+export interface ModdedItemDataTable {[itemid: IDEntry]: ModdedItemData}
+
export class Item extends BasicEffect implements Readonly {
declare readonly effectType: 'Item';
diff --git a/sim/dex-moves.ts b/sim/dex-moves.ts
index 0bbc84bc81f0..f274dc3c8914 100644
--- a/sim/dex-moves.ts
+++ b/sim/dex-moves.ts
@@ -1,4 +1,5 @@
import {Utils} from '../lib';
+import type {ConditionData} from './dex-conditions';
import {BasicEffect, toID} from './dex-data';
/**
@@ -171,10 +172,10 @@ export interface MoveData extends EffectData, MoveEventMethods, HitEffect {
* ID of the Z-Crystal that calls the move.
* `true` for Z-Powered status moves like Z-Encore.
*/
- isZ?: boolean | string;
+ isZ?: boolean | IDEntry;
zMove?: {
basePower?: number,
- effect?: string,
+ effect?: IDEntry,
boost?: SparseBoostsTable,
};
@@ -182,7 +183,7 @@ export interface MoveData extends EffectData, MoveEventMethods, HitEffect {
// -------------
/**
* `true` for Max moves like Max Airstream. If its a G-Max moves, this is
- * the species ID of the Gigantamax Pokemon that can use this G-Max move.
+ * the species name of the Gigantamax Pokemon that can use this G-Max move.
*/
isMax?: boolean | string;
maxMove?: {
@@ -191,7 +192,7 @@ export interface MoveData extends EffectData, MoveEventMethods, HitEffect {
// Hit effects
// -----------
- ohko?: boolean | string;
+ ohko?: boolean | 'Ice';
thawsTarget?: boolean;
heal?: number[] | null;
forceSwitch?: boolean;
@@ -243,17 +244,17 @@ export interface MoveData extends EffectData, MoveEventMethods, HitEffect {
ignoreAccuracy?: boolean;
ignoreDefensive?: boolean;
ignoreEvasion?: boolean;
- ignoreImmunity?: boolean | {[k: string]: boolean};
+ ignoreImmunity?: boolean | {[typeName: string]: boolean};
ignoreNegativeOffensive?: boolean;
ignoreOffensive?: boolean;
ignorePositiveDefensive?: boolean;
ignorePositiveEvasion?: boolean;
multiaccuracy?: boolean;
multihit?: number | number[];
- multihitType?: string;
+ multihitType?: 'parentalbond';
noDamageVariance?: boolean;
- nonGhostTarget?: string;
- pressureTarget?: string;
+ nonGhostTarget?: MoveTarget;
+ pressureTarget?: MoveTarget;
spreadModifier?: number;
sleepUsable?: boolean;
/**
@@ -266,6 +267,7 @@ export interface MoveData extends EffectData, MoveEventMethods, HitEffect {
*/
tracksTarget?: boolean;
willCrit?: boolean;
+ callsMove?: boolean;
// Mechanics flags
// ---------------
@@ -273,7 +275,7 @@ export interface MoveData extends EffectData, MoveEventMethods, HitEffect {
isConfusionSelfHit?: boolean;
noSketch?: boolean;
stallingMove?: boolean;
- baseMove?: string;
+ baseMove?: ID;
}
export type ModdedMoveData = MoveData | Partial> & {
@@ -285,6 +287,9 @@ export type ModdedMoveData = MoveData | Partial> & {
gen?: number,
};
+export interface MoveDataTable {[moveid: IDEntry]: MoveData}
+export interface ModdedMoveDataTable {[moveid: IDEntry]: ModdedMoveData}
+
export interface Move extends Readonly {
readonly effectType: 'Move';
}
@@ -304,8 +309,7 @@ interface MoveHitData {
}
type MutableMove = BasicEffect & MoveData;
-type RuinableMove = {[k in `ruined${'Atk' | 'Def' | 'SpA' | 'SpD'}`]?: Pokemon;};
-export interface ActiveMove extends MutableMove, RuinableMove {
+export interface ActiveMove extends MutableMove {
readonly name: string;
readonly effectType: 'Move';
readonly id: ID;
@@ -338,6 +342,10 @@ export interface ActiveMove extends MutableMove, RuinableMove {
typeChangerBoosted?: Effect;
willChangeForme?: boolean;
infiltrates?: boolean;
+ ruinedAtk?: Pokemon;
+ ruinedDef?: Pokemon;
+ ruinedSpA?: Pokemon;
+ ruinedSpD?: Pokemon;
/**
* Has this move been boosted by a Z-crystal or used by a Dynamax Pokemon? Usually the same as
@@ -363,7 +371,7 @@ export class DataMove extends BasicEffect implements Readonly {
declare readonly effectType: 'Pokemon';
/**
@@ -175,6 +211,8 @@ export class Species extends BasicEffect implements Readonly {
- [key: string]: T;
-}
+/** Unfortunately we do for..in too much to want to deal with the casts */
+export interface DexTable {[id: string]: T}
+export interface AliasesTable {[id: IDEntry]: string}
interface DexTableData {
- Abilities: DexTable;
- Aliases: {[id: string]: string};
- Rulesets: DexTable;
- FormatsData: DexTable;
- Items: DexTable;
- Learnsets: DexTable;
- Moves: DexTable;
- Natures: DexTable;
- Pokedex: DexTable;
- PokemonGoData: DexTable;
+ Abilities: DexTable;
+ Aliases: DexTable;
+ Rulesets: DexTable;
+ Items: DexTable;
+ Learnsets: DexTable;
+ Moves: DexTable;
+ Natures: DexTable;
+ Pokedex: DexTable;
+ FormatsData: DexTable;
+ PokemonGoData: DexTable;
Scripts: DexTable;
- Conditions: DexTable;
- TypeChart: DexTable;
+ Conditions: DexTable;
+ TypeChart: DexTable;
}
interface TextTableData {
Abilities: DexTable;
@@ -123,6 +123,7 @@ export class ModdedDex {
deepClone = Utils.deepClone;
deepFreeze = Utils.deepFreeze;
+ Multiset = Utils.Multiset;
readonly formats: DexFormats;
readonly abilities: DexAbilities;
@@ -316,7 +317,7 @@ export class ModdedDex {
return moveCopy;
}
- getHiddenPower(ivs: AnyObject) {
+ getHiddenPower(ivs: StatsTable) {
const hpTypes = [
'Fighting', 'Flying', 'Poison', 'Ground', 'Rock', 'Bug', 'Ghost', 'Steel',
'Fire', 'Water', 'Grass', 'Electric', 'Psychic', 'Ice', 'Dragon', 'Dark',
@@ -341,8 +342,8 @@ export class ModdedDex {
let hpPowerX = 0;
let i = 1;
for (const s in stats) {
- hpTypeX += i * (ivs[s] % 2);
- hpPowerX += i * (tr(ivs[s] / 2) % 2);
+ hpTypeX += i * (ivs[s as StatID] % 2);
+ hpPowerX += i * (tr(ivs[s as StatID] / 2) % 2);
i *= 2;
}
return {
@@ -377,7 +378,7 @@ export class ModdedDex {
} as const;
let searchResults: AnyObject[] | null = [];
for (const table of searchIn) {
- const res: AnyObject = this[searchObjects[table]].get(target);
+ const res = this[searchObjects[table]].get(target);
if (res.exists && res.gen <= this.gen) {
searchResults.push({
isInexact,
@@ -399,14 +400,14 @@ export class ModdedDex {
maxLd = 2;
}
searchResults = null;
- for (const table of [...searchIn, 'Aliases'] as DataType[]) {
- const searchObj = this.data[table];
+ for (const table of [...searchIn, 'Aliases'] as const) {
+ const searchObj = this.data[table] as DexTable;
if (!searchObj) continue;
for (const j in searchObj) {
const ld = Utils.levenshtein(cmpTarget, j, maxLd);
if (ld <= maxLd) {
- const word = (searchObj[j] as DexTable).name || (searchObj[j] as DexTable).species || j;
+ const word = searchObj[j].name || j;
const results = this.dataSearch(word, searchIn, word);
if (results) {
searchResults = results;
diff --git a/sim/global-types.ts b/sim/global-types.ts
index 39da583bf3ab..76e322cfbccf 100644
--- a/sim/global-types.ts
+++ b/sim/global-types.ts
@@ -19,8 +19,10 @@ type TeamValidator = import('./team-validator').TeamValidator;
type PokemonSources = import('./team-validator').PokemonSources;
/** An ID must be lowercase alphanumeric. */
-type ID = '' | string & {__isID: true};
-type PokemonSlot = '' | string & {__isSlot: true};
+type ID = '' | Lowercase & {__isID: true};
+/** Like ID, but doesn't require you to type `as ID` to define it. For data files and object keys. */
+type IDEntry = Lowercase;
+type PokemonSlot = '' | IDEntry & {__isSlot: true};
interface AnyObject {[k: string]: any}
type GenderName = 'M' | 'F' | 'N' | '';
@@ -36,30 +38,6 @@ type Nonstandard = 'Past' | 'Future' | 'Unobtainable' | 'CAP' | 'LGPE' | 'Custom
type PokemonSet = import('./teams').PokemonSet;
-/**
- * Describes a possible way to get a move onto a pokemon.
- *
- * First character is a generation number, 1-7.
- * Second character is a source ID, one of:
- *
- * - M = TM/HM
- * - T = tutor
- * - L = start or level-up, 3rd char+ is the level
- * - R = restricted (special moves like Rotom moves)
- * - E = egg
- * - D = Dream World, only 5D is valid
- * - S = event, 3rd char+ is the index in .eventData
- * - V = Virtual Console or Let's Go transfer, only 7V/8V is valid
- * - C = NOT A REAL SOURCE, see note, only 3C/4C is valid
- *
- * C marks certain moves learned by a pokemon's prevo. It's used to
- * work around the chainbreeding checker's shortcuts for performance;
- * it lets the pokemon be a valid father for teaching the move, but
- * is otherwise ignored by the learnset checker (which will actually
- * check prevos for compatibility).
- */
-type MoveSource = string;
-
namespace TierTypes {
export type Singles = "AG" | "Uber" | "(Uber)" | "OU" | "(OU)" | "UUBL" | "UU" | "RUBL" | "RU" | "NUBL" | "NU" |
"(NU)" | "PUBL" | "PU" | "(PU)" | "ZUBL" | "ZU" | "NFE" | "LC";
@@ -78,10 +56,10 @@ interface EventInfo {
perfectIVs?: number;
/** true: has hidden ability, false | undefined: never has hidden ability */
isHidden?: boolean;
- abilities?: string[];
+ abilities?: IDEntry[];
maxEggMoves?: number;
- moves?: string[];
- pokeball?: string;
+ moves?: IDEntry[];
+ pokeball?: IDEntry;
from?: string;
/** Japan-only events can't be transferred to international games in Gen 1 */
japan?: boolean;
@@ -145,63 +123,25 @@ interface BasicEffect extends EffectData {
toString: () => string;
}
-type ConditionData = import('./dex-conditions').ConditionData;
-type ModdedConditionData = import('./dex-conditions').ModdedConditionData;
type Condition = import('./dex-conditions').Condition;
-type MoveData = import('./dex-moves').MoveData;
-type ModdedMoveData = import('./dex-moves').ModdedMoveData;
type ActiveMove = import('./dex-moves').ActiveMove;
type Move = import('./dex-moves').Move;
type MoveTarget = import('./dex-moves').MoveTarget;
-type ItemData = import('./dex-items').ItemData;
-type ModdedItemData = import('./dex-items').ModdedItemData;
type Item = import('./dex-items').Item;
-type AbilityData = import('./dex-abilities').AbilityData;
-type ModdedAbilityData = import('./dex-abilities').ModdedAbilityData;
type Ability = import('./dex-abilities').Ability;
-type SpeciesData = import('./dex-species').SpeciesData;
-type ModdedSpeciesData = import('./dex-species').ModdedSpeciesData;
-type SpeciesFormatsData = import('./dex-species').SpeciesFormatsData;
-type ModdedSpeciesFormatsData = import('./dex-species').ModdedSpeciesFormatsData;
-type LearnsetData = import('./dex-species').LearnsetData;
-type ModdedLearnsetData = import('./dex-species').ModdedLearnsetData;
type Species = import('./dex-species').Species;
-type PokemonGoData = import('./dex-species').PokemonGoData;
-type FormatData = import('./dex-formats').FormatData;
-type FormatList = import('./dex-formats').FormatList;
-type ModdedFormatData = import('./dex-formats').ModdedFormatData;
type Format = import('./dex-formats').Format;
-interface NatureData {
- name: string;
- plus?: StatIDExceptHP;
- minus?: StatIDExceptHP;
-}
-
-type ModdedNatureData = NatureData | Partial> & {inherit: true};
-
type Nature = import('./dex-data').Nature;
type GameType = 'singles' | 'doubles' | 'triples' | 'rotation' | 'multi' | 'freeforall';
type SideID = 'p1' | 'p2' | 'p3' | 'p4';
-interface GameTimerSettings {
- dcTimer: boolean;
- dcTimerBank: boolean;
- starting: number;
- grace: number;
- addPerTurn: number;
- maxPerTurn: number;
- maxFirstTurn: number;
- timeoutAutoChoose: boolean;
- accelerate: boolean;
-}
-
type SpreadMoveTargets = (Pokemon | false | null)[];
type SpreadMoveDamage = (number | boolean | undefined)[];
type ZMoveOptions = ({move: string, target: MoveTarget} | null)[];
@@ -407,7 +347,7 @@ interface ModdedBattleScriptsData extends Partial {
this: Battle, trappedBySide: boolean[], stalenessBySide: ('internal' | 'external' | undefined)[]
) => boolean | undefined;
natureModify?: (this: Battle, stats: StatsTable, set: PokemonSet) => StatsTable;
- nextTurn?: (this: Battle) => void;
+ endTurn?: (this: Battle) => void;
runAction?: (this: Battle, action: Action) => void;
spreadModify?: (this: Battle, baseStats: StatsTable, set: PokemonSet) => StatsTable;
start?: (this: Battle) => void;
@@ -422,15 +362,6 @@ interface ModdedBattleScriptsData extends Partial {
checkWin?: (this: Battle, faintQueue?: Battle['faintQueue'][0]) => true | undefined;
}
-interface TypeData {
- damageTaken: {[attackingTypeNameOrEffectid: string]: number};
- HPdvs?: SparseStatsTable;
- HPivs?: SparseStatsTable;
- isNonstandard?: Nonstandard | null;
-}
-
-type ModdedTypeData = TypeData | Partial> & {inherit: true};
-
type TypeInfo = import('./dex-data').TypeInfo;
interface PlayerOptions {
@@ -441,11 +372,11 @@ interface PlayerOptions {
seed?: PRNGSeed;
}
-interface TextObject {
+interface BasicTextData {
desc?: string;
shortDesc?: string;
}
-interface Plines {
+interface ConditionTextData extends BasicTextData {
activate?: string;
addItem?: string;
block?: string;
@@ -460,19 +391,7 @@ interface Plines {
transform?: string;
}
-interface TextFile extends TextObject {
- name: string;
- gen1?: ModdedTextObject;
- gen2?: ModdedTextObject;
- gen3?: ModdedTextObject;
- gen4?: ModdedTextObject;
- gen5?: ModdedTextObject;
- gen6?: ModdedTextObject;
- gen7?: ModdedTextObject;
- gen8?: ModdedTextObject;
-}
-
-interface MovePlines extends Plines {
+interface MoveTextData extends ConditionTextData {
alreadyStarted?: string;
blockSelf?: string;
clearBoost?: string;
@@ -492,24 +411,28 @@ interface MovePlines extends Plines {
upkeep?: string;
}
-interface AbilityText extends TextFile, Plines {
- activateFromItem?: string;
- activateNoTarget?: string;
- copyBoost?: string;
- transformEnd?: string;
-}
-
-/* eslint-disable @typescript-eslint/no-empty-interface */
-interface MoveText extends TextFile, MovePlines {}
-
-interface ItemText extends TextFile, Plines {}
-
-interface PokedexText extends TextFile {}
-
-interface DefaultText extends AnyObject {}
+type TextFile = T & {
+ name: string,
+ gen1?: T,
+ gen2?: T,
+ gen3?: T,
+ gen4?: T,
+ gen5?: T,
+ gen6?: T,
+ gen7?: T,
+ gen8?: T,
+};
-interface ModdedTextObject extends TextObject, Plines {}
-/* eslint-enable @typescript-eslint/no-empty-interface */
+type AbilityText = TextFile;
+type MoveText = TextFile;
+type ItemText = TextFile;
+type PokedexText = TextFile;
+type DefaultText = AnyObject;
namespace RandomTeamsTypes {
export interface TeamDetails {
@@ -534,8 +457,10 @@ namespace RandomTeamsTypes {
export interface FactoryTeamDetails {
megaCount?: number;
zCount?: number;
+ wantsTeraCount?: number;
forceResult: boolean;
weather?: string;
+ terrain?: string[];
typeCount: {[k: string]: number};
typeComboCount: {[k: string]: number};
baseFormes: {[k: string]: number};
@@ -577,6 +502,8 @@ namespace RandomTeamsTypes {
moves: string[];
dynamaxLevel?: number;
gigantamax?: boolean;
+ wantsTera?: boolean;
+ teraType?: string;
}
export interface RandomSetData {
role: Role;
diff --git a/sim/pokemon.ts b/sim/pokemon.ts
index b85d19b630a5..359fc01d78cd 100644
--- a/sim/pokemon.ts
+++ b/sim/pokemon.ts
@@ -951,13 +951,23 @@ export class Pokemon {
moveName += ' ' + basePowerCallback(this);
}
let target = moveSlot.target;
- if (moveSlot.id === 'curse') {
+ switch (moveSlot.id) {
+ case 'curse':
if (!this.hasType('Ghost')) {
- target = this.battle.dex.moves.get('curse').nonGhostTarget || moveSlot.target;
+ target = this.battle.dex.moves.get('curse').nonGhostTarget;
}
- // Heal Block only prevents Pollen Puff from targeting an ally when the user has Heal Block
- } else if (moveSlot.id === 'pollenpuff' && this.volatiles['healblock']) {
- target = 'adjacentFoe';
+ break;
+ case 'pollenpuff':
+ // Heal Block only prevents Pollen Puff from targeting an ally when the user has Heal Block
+ if (this.volatiles['healblock']) {
+ target = 'adjacentFoe';
+ }
+ break;
+ case 'terastarstorm':
+ if (this.species.name === 'Terapagos-Stellar') {
+ target = 'allAdjacentFoes';
+ }
+ break;
}
let disabled = moveSlot.disabled;
if (this.volatiles['dynamax']) {
@@ -1942,9 +1952,9 @@ export class Pokemon {
if (!this.hp) return false;
status = this.battle.dex.conditions.get(status) as Effect;
if (!this.volatiles[status.id]) return false;
- this.battle.singleEvent('End', status, this.volatiles[status.id], this);
const linkedPokemon = this.volatiles[status.id].linkedPokemon;
const linkedStatus = this.volatiles[status.id].linkedStatus;
+ this.battle.singleEvent('End', status, this.volatiles[status.id], this);
delete this.volatiles[status.id];
if (linkedPokemon) {
this.removeLinkedVolatiles(linkedStatus, linkedPokemon);
diff --git a/sim/side.ts b/sim/side.ts
index b5144666da6c..829f6aea64a9 100644
--- a/sim/side.ts
+++ b/sim/side.ts
@@ -117,10 +117,9 @@ export class Side {
this.team = team;
this.pokemon = [];
- for (let i = 0; i < this.team.length && i < 24; i++) {
+ for (const set of this.team) {
// console.log("NEW POKEMON: " + (this.team[i] ? this.team[i].name : '[unidentified]'));
- this.pokemon.push(new Pokemon(this.team[i], this));
- this.pokemon[i].position = i;
+ this.addPokemon(set);
}
switch (this.battle.gameType) {
@@ -176,6 +175,15 @@ export class Side {
return 'move';
}
+ addPokemon(set: PokemonSet) {
+ if (this.pokemon.length >= 24) return null;
+ const newPokemon = new Pokemon(set, this);
+ newPokemon.position = this.pokemon.length;
+ this.pokemon.push(newPokemon);
+ this.pokemonLeft++;
+ return newPokemon;
+ }
+
canDynamaxNow(): boolean {
if (this.battle.gen !== 8) return false;
// In multi battles, players on a team are alternatingly given the option to dynamax each turn
diff --git a/sim/team-validator.ts b/sim/team-validator.ts
index b15f06eef5ec..cb535e52f415 100644
--- a/sim/team-validator.ts
+++ b/sim/team-validator.ts
@@ -8,6 +8,7 @@
*/
import {Dex, toID} from './dex';
+import type {MoveSource} from './dex-species';
import {Utils} from '../lib';
import {Tags} from '../data/tags';
import {Teams} from './teams';
@@ -101,6 +102,10 @@ export class PokemonSources {
* `null` = definitely not an event egg that can be used with the Pomeg glitch
*/
pomegEventEgg?: string | null;
+ /**
+ * For event-only Pokemon that do not have a minimum source gen identified by its moves
+ */
+ eventOnlyMinSourceGen?: number;
/**
* Some Pokemon evolve by having a move in their learnset (like Piloswine
* with Ancient Power). These can only carry three other moves from their
@@ -151,6 +156,7 @@ export class PokemonSources {
this.limitedEggMoves = null;
}
minSourceGen() {
+ if (this.eventOnlyMinSourceGen) return this.eventOnlyMinSourceGen;
if (this.sourcesBefore) return this.sourcesAfter || 1;
let min = 10;
for (const source of this.sources) {
@@ -877,6 +883,7 @@ export class TeamValidator {
let legal = false;
for (const event of eventData) {
if (this.validateEvent(set, setSources, event, eventSpecies)) continue;
+ setSources.eventOnlyMinSourceGen = event.generation;
legal = true;
break;
}
@@ -1348,7 +1355,7 @@ export class TeamValidator {
const fathers: ID[] = [];
// Gen 6+ don't have egg move incompatibilities
// (except for certain cases with baby Pokemon not handled here)
- if (!getAll && eggGen >= 6 && !setSources.levelUpEggMoves) return true;
+ if (!getAll && eggGen >= 6 && !setSources.levelUpEggMoves && !species.mother) return true;
let eggMoves = setSources.limitedEggMoves;
if (eggGen === 3) eggMoves = eggMoves?.filter(eggMove => !setSources.pomegEggMoves?.includes(eggMove));
@@ -1487,6 +1494,15 @@ export class TeamValidator {
return true;
}
+ motherCanLearn(species: ID, move: ID) {
+ if (!species) return false;
+ const fullLearnset = this.dex.species.getFullLearnset(species);
+ for (const {learnset} of fullLearnset) {
+ if (learnset[move]) return true;
+ }
+ return false;
+ }
+
validateForme(set: PokemonSet) {
const dex = this.dex;
const name = set.name || set.species;
@@ -1731,7 +1747,7 @@ export class TeamValidator {
for (const ruleid of ruleTable.tagRules) {
if (ruleid.startsWith('*')) continue;
- const tagid = ruleid.slice(12);
+ const tagid = ruleid.slice(12) as ID;
const tag = Tags[tagid];
if ((tag.speciesFilter || tag.genericFilter)!(tierSpecies)) {
const existenceTag = EXISTENCE_TAG.includes(tagid);
@@ -2049,7 +2065,7 @@ export class TeamValidator {
if (canBottleCap) {
// IVs can be overridden but Hidden Power type can't
if (Object.keys(eventData.ivs).length >= 6) {
- const requiredHpType = dex.getHiddenPower(eventData.ivs).type;
+ const requiredHpType = dex.getHiddenPower(eventData.ivs as StatsTable).type;
if (set.hpType && set.hpType !== requiredHpType) {
if (fastReturn) return true;
problems.push(`${name} can only have Hidden Power ${requiredHpType}${etc}.`);
@@ -2415,6 +2431,15 @@ export class TeamValidator {
}
}
+ let formeCantInherit = false;
+ let nextSpecies = dex.species.learnsetParent(baseSpecies);
+ while (nextSpecies) {
+ if (nextSpecies.name === species.name) break;
+ nextSpecies = dex.species.learnsetParent(nextSpecies);
+ }
+ if (checkingPrevo && !nextSpecies) formeCantInherit = true;
+ if (formeCantInherit && dex.gen < 9) break;
+
let sources = learnset[moveid] || [];
if (moveid === 'sketch') {
sketch = true;
@@ -2462,6 +2487,8 @@ export class TeamValidator {
continue;
}
+ if (formeCantInherit && (learned.charAt(1) !== 'E' || learnedGen < 9)) continue;
+
// redundant
if (learnedGen <= moveSources.sourcesBefore) continue;
@@ -2515,11 +2542,16 @@ export class TeamValidator {
// falls through to LMT check below
} else if (level >= 5 && learnedGen === 3 && species.canHatch) {
// Pomeg Glitch
- learned = learnedGen + 'Epomeg';
- } else if ((!species.gender || species.gender === 'F') &&
+ learned = learnedGen + 'Epomeg' as MoveSource;
+ } else if (species.gender !== 'N' &&
learnedGen >= 2 && species.canHatch && !setSources.isFromPokemonGo) {
// available as egg move
- learned = learnedGen + 'Eany';
+ if (species.gender === 'M' && !this.motherCanLearn(toID(species.mother), moveid)) {
+ // male-only Pokemon can have level-up egg moves if it can have a mother that learns the move
+ cantLearnReason = `is learned at level ${parseInt(learned.substr(2))}.`;
+ continue;
+ }
+ learned = learnedGen + 'Eany' as MoveSource;
// falls through to E check below
} else {
// this move is unavailable, skip it
@@ -2566,10 +2598,10 @@ export class TeamValidator {
// Pomeg glitched moves have to be from an egg but since they aren't true egg moves,
// there should be no breeding restrictions
moveSources.pomegEggMoves = [move.id];
- } else if (learnedGen < 6) {
+ } else if (learnedGen < 6 || (species.mother && !this.motherCanLearn(toID(species.mother), moveid))) {
limitedEggMove = move.id;
}
- learned = learnedGen + 'E' + (species.prevo ? species.id : '');
+ learned = learnedGen + 'E' + (species.prevo ? species.id : '') as MoveSource;
if (tradebackEligible && learnedGen === 2 && move.gen <= 1) {
// can tradeback
moveSources.add('1ET' + learned.slice(2), limitedEggMove);
diff --git a/sim/teams.ts b/sim/teams.ts
index 56e5d59fdfa6..0b39a6f67108 100644
--- a/sim/teams.ts
+++ b/sim/teams.ts
@@ -622,6 +622,10 @@ export const Teams = new class Teams {
TeamGenerator = require(Dex.forFormat(format).dataDir + '/cg-teams').default;
} else if (toID(format).includes('gen9superstaffbrosultimate')) {
TeamGenerator = require(`../data/mods/gen9ssb/random-teams`).default;
+ } else if (toID(format).includes('gen9babyrandombattle')) {
+ TeamGenerator = require(`../data/random-battles/gen9baby/teams`).default;
+ } else if (toID(format).includes('gen9caprandombattle')) {
+ TeamGenerator = require(`../data/random-battles/gen9cap/teams`).default;
} else {
TeamGenerator = require(`../data/random-battles/${format.mod}/teams`).default;
}
diff --git a/test/random-battles/all-gens.js b/test/random-battles/all-gens.js
index 711bbedfcc58..b9a4526a3437 100644
--- a/test/random-battles/all-gens.js
+++ b/test/random-battles/all-gens.js
@@ -113,47 +113,149 @@ describe('value rule support (slow)', () => {
}
});
-describe("New set format", () => {
- const files = ['../../data/random-battles/gen9/sets.json', '../../data/random-battles/gen9/doubles-sets.json'];
- for (const filename of files) {
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- let validRoles = [];
- if (filename === '../../data/random-battles/gen9/sets.json') {
- validRoles = ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user",
- "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"];
- } else {
- validRoles = ["Doubles Fast Attacker", "Doubles Setup Sweeper", "Doubles Wallbreaker", "Tera Blast user",
- "Doubles Bulky Attacker", "Doubles Bulky Setup", "Offensive Protect", "Bulky Protect", "Doubles Support", "Choice Item user"];
- }
+describe("New set format (slow)", () => {
+ // formatInfo lists filenames and roles for each format
+ const formatInfo = {
+ "gen2randombattle": {
+ filename: "gen2/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Generalist", "Thief user"],
+ },
+ "gen3randombattle": {
+ filename: "gen3/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Generalist", "Berry Sweeper"],
+ },
+ "gen4randombattle": {
+ filename: "gen4/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner"],
+ },
+ "gen5randombattle": {
+ filename: "gen5/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner"],
+ },
+ "gen6randombattle": {
+ filename: "gen6/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"],
+ },
+ "gen7randombattle": {
+ filename: "gen7/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Z-Move user", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"],
+ },
+ "gen9randombattle": {
+ filename: "gen9/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"],
+ },
+ "gen9randomdoublesbattle": {
+ filename: "gen9/doubles-sets",
+ roles: ["Doubles Fast Attacker", "Doubles Setup Sweeper", "Doubles Wallbreaker", "Tera Blast user", "Doubles Bulky Attacker", "Doubles Bulky Setup", "Offensive Protect", "Bulky Protect", "Doubles Support", "Choice Item user"],
+ },
+ "gen9babyrandombattle": {
+ filename: "gen9baby/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Fast Support"],
+ },
+ "gen9caprandombattle": {
+ filename: "gen9cap/sets",
+ roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"],
+ },
+ };
+ for (const format of Object.keys(formatInfo)) {
+ const filename = formatInfo[format].filename;
+ const setsJSON = require(`../../dist/data/random-battles/${filename}.json`);
+ const mod = filename.split('/')[0];
+ const genNum = parseInt(mod[3]);
+ const rounds = 100;
+ const dex = Dex.forFormat(format);
+ it(`${filename}.json should have valid set data`, () => {
+ const validRoles = formatInfo[format].roles;
for (const [id, sets] of Object.entries(setsJSON)) {
const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
+ assert(species.exists, `In ${format}, misspelled species ID: ${id}`);
assert(Array.isArray(sets.sets));
for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- assert.equal(set.role === "Tera Blast user", set.movepool.includes("Tera Blast"),
- `Set for ${species.name} has inconsistent Tera Blast user status`);
+ assert(validRoles.includes(set.role), `In ${format}, set for ${species.name} has invalid role: ${set.role}`);
for (const move of set.movepool) {
const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert.equal(move, dexMove.name, `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'anythinggoes', 'gen9'), `${species.name} can't learn ${move}`);
+ assert(dexMove.exists, `In ${format}, ${species.name} has invalid move: ${move}`);
+ // Old gens have moves in id form, currently.
+ if (genNum === 9) {
+ assert.equal(move, dexMove.name, `In ${format}, ${species.name} has misformatted move: ${move}`);
+ } else {
+ assert(move === dexMove.id || move.startsWith('hiddenpower'), `In ${format}, ${species.name} has misformatted move: ${move}`);
+ }
+ assert(validateLearnset(dexMove, {species}, 'ubers', `gen${genNum}`), `In ${format}, ${species.name} can't learn ${move}`);
}
for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
+ assert(set.movepool[i + 1] > set.movepool[i], `In ${format}, ${species.name} movepool should be sorted alphabetically`);
}
- for (const type of set.teraTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Tera Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Tera Type: ${type}`);
+ if (set.teraTypes) {
+ for (const type of set.teraTypes) {
+ const dexType = Dex.types.get(type);
+ assert(dexType.exists, `In ${format}, ${species.name} has invalid Tera Type: ${type}`);
+ assert.equal(type, dexType.name, `In ${format}, ${species.name} has misformatted Tera Type: ${type}`);
+ }
+ for (let i = 0; i < set.teraTypes.length - 1; i++) {
+ assert(set.teraTypes[i + 1] > set.teraTypes[i], `In ${format}, ${species.name} teraTypes should be sorted alphabetically`);
+ }
}
- for (let i = 0; i < set.teraTypes.length - 1; i++) {
- assert(set.teraTypes[i + 1] > set.teraTypes[i], `${species} teraTypes should be sorted alphabetically`);
+ if (set.preferredTypes) {
+ for (const type of set.preferredTypes) {
+ const dexType = Dex.types.get(type);
+ assert(dexType.exists, `In ${format}, ${species.name} has invalid Preferred Type: ${type}`);
+ assert.equal(type, dexType.name, `In ${format}, ${species.name} has misformatted Preferred Type: ${type}`);
+ }
+ for (let i = 0; i < set.preferredTypes.length - 1; i++) {
+ assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `In ${format}, ${species.name} preferredTypes should be sorted alphabetically`);
+ }
}
}
}
});
+ it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
+ testTeam({format, rounds}, team => {
+ for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4, `In ${format}, ${pokemon.name} can generate with ${pokemon.moves.length} moves`);
+ });
+ });
+ it('all moves on all sets should exist and be obtainable', function () {
+ const generator = Teams.getGenerator(format);
+ for (const pokemon of Object.keys(setsJSON)) {
+ const species = dex.species.get(pokemon);
+ assert(species.exists, `In ${format}, Pokemon ${species} does not exist`);
+ const sets = setsJSON[pokemon]["sets"];
+ const types = species.types;
+ const abilities = new Set(Object.values(species.abilities));
+ if (species.unreleasedHidden) abilities.delete(species.abilities.H);
+ for (const set of sets) {
+ assert(set.movepool.every(m => dex.moves.get(m).exists), `In ${format}, for Pokemon ${species}, one of ${set.movepool} does not exist.`);
+ const role = set.role;
+ const moves = new Set(set.movepool.map(m => (m.startsWith('hiddenpower') ? m : dex.moves.get(m).id)));
+ const specialTypes = genNum === 9 ? set.teraTypes : set.preferredTypes;
+ // Go through all possible teamDetails combinations, if necessary
+ for (let j = 0; j < rounds; j++) {
+ // In Gens 2-3, if a set has multiple preferred types, we enforce moves of all the types.
+ const specialType = specialTypes ? (genNum > 3 ? specialTypes[j % specialTypes.length] : specialTypes.join()) : '';
+ // Generate a moveset for each combination of relevant teamDetails. Spikes is relevant for Gen 2.
+ for (let i = 0; i < 16; i++) {
+ const rapidSpin = i % 2;
+ const stealthRock = Math.floor(i / 2) % 2;
+ const stickyWeb = Math.floor(i / 4) % 2;
+ const spikes = Math.floor(i / 8) % 2;
+ const teamDetails = {rapidSpin, stealthRock, stickyWeb, spikes};
+ // randomMoveset() deletes moves from the movepool, so recreate it every time
+ const movePool = set.movepool.map(m => (m.startsWith('hiddenpower') ? m : dex.moves.get(m).id));
+ let moveSet;
+ if (genNum === 9) {
+ moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, format.includes('doubles'), movePool, specialType, role);
+ } else {
+ moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, specialType, role);
+ }
+ for (const move of moveSet) moves.delete(move);
+ if (!moves.size) break;
+ }
+ if (!moves.size) break;
+ }
+ assert.false(moves.size, `In ${format}, the following moves on ${species.name} are unused: ${[...moves].join(', ')}`);
+ }
+ }
+ });
}
});
@@ -183,7 +285,7 @@ describe('Battle Factory and BSS Factory data should be valid (slow)', () => {
this.timeout(0);
const setsJSON = require(`../../dist/data/random-battles/${filename}.json`);
const mod = filename.split('/')[0] || 'gen' + Dex.gen;
- const genNum = isNaN(mod[3]) ? Dex.gen : mod[3];
+ const genNum = isNaN(mod[3]) ? Dex.gen : parseInt(mod[3]);
for (const type in setsJSON) {
const typeTable = filename.includes('bss-factory-sets') ? setsJSON : setsJSON[type];
@@ -246,3 +348,71 @@ describe('Battle Factory and BSS Factory data should be valid (slow)', () => {
});
}
});
+
+describe('[Gen 9] BSS Factory data should be valid (slow)', () => {
+ it(`gen9/bss-factory-sets.json should contain valid sets`, function () {
+ this.timeout(0);
+ const setsJSON = require(`../../dist/data/random-battles/gen9/bss-factory-sets.json`);
+ const mod = 'gen9';
+ const genNum = 9;
+
+ for (const speciesid in setsJSON) {
+ const vType = 'battlestadiumsingles';
+ let totalWeight = 0;
+ for (const set of setsJSON[speciesid].sets) {
+ totalWeight += set.weight;
+ const species = Dex.species.get(set.species);
+ assert(species.exists, `invalid species "${set.species}" of ${speciesid}`);
+ assert(!species.isNonstandard, `illegal species "${set.species}" of ${speciesid}`);
+ assert.equal(species.name, set.species, `miscapitalized species "${set.species}" of ${speciesid}`);
+
+ assert(species.id.startsWith(toID(species.baseSpecies)), `non-matching species "${set.species}" of ${speciesid}`);
+
+ assert(!species.battleOnly, `invalid battle-only forme "${set.species}" of ${speciesid}`);
+
+ for (const itemName of [].concat(set.item)) {
+ if (!itemName && [].concat(...set.moves).includes("Acrobatics")) continue;
+ const item = Dex.forGen(genNum).items.get(itemName);
+ assert(item.exists, `invalid item "${itemName}" of ${speciesid}`);
+ assert.equal(item.name, itemName, `miscapitalized item "${itemName}" of ${speciesid}`);
+ }
+
+ for (const abilityName of [].concat(set.ability)) {
+ const ability = Dex.forGen(genNum).abilities.get(abilityName);
+ assert(ability.exists, `invalid ability "${abilityName}" of ${speciesid}`);
+ assert.equal(ability.name, abilityName, `miscapitalized ability "${abilityName}" of ${speciesid}`);
+ }
+
+ for (const natureName of [].concat(set.nature)) {
+ const nature = Dex.forGen(genNum).natures.get(natureName);
+ assert(nature.exists, `invalid nature "${natureName}" of ${speciesid}`);
+ assert.equal(nature.name, natureName, `miscapitalized nature "${natureName}" of ${speciesid}`);
+ }
+
+ for (const moveSpec of set.moves) {
+ for (const moveName of [].concat(moveSpec)) {
+ const move = Dex.forGen(genNum).moves.get(moveName);
+ assert(move.exists, `invalid move "${moveName}" of ${speciesid}`);
+ assert.equal(move.name, moveName, `miscapitalized move "${moveName}" ≠ "${move.name}" of ${speciesid}`);
+ assert(validateLearnset(move, set, vType, mod), `illegal move "${moveName}" of ${speciesid}`);
+ }
+ }
+
+ assert(!!set.evs, `Set of ${speciesid} has no EVs specified`);
+ const keys = Object.keys(set.evs);
+ let totalEVs = 0;
+ for (const ev of keys) {
+ assert(Dex.stats.ids().includes(ev), `Invalid EV key (${ev}) on set of ${speciesid}`);
+ totalEVs += set.evs[ev];
+ assert.equal(set.evs[ev] % 4, 0, `EVs of ${ev} not divisible by 4 on ${speciesid}`);
+ }
+ const sortedKeys = Utils.sortBy([...keys], ev => Dex.stats.ids().indexOf(ev));
+ assert.deepEqual(keys, sortedKeys, `EVs out of order on set of ${speciesid}, possibly because one of them is for the wrong stat`);
+ assert(totalEVs <= 510, `more than 510 EVs on set of ${speciesid}`);
+ }
+ // Some species have 1/3 probability for each set
+ if (totalWeight === 99) totalWeight += 1;
+ assert.equal(totalWeight, 100, `Total set weight for ${speciesid} is ${totalWeight < 100 ? 'less' : 'greater'} than 100%`);
+ }
+ });
+});
diff --git a/test/random-battles/gen2.js b/test/random-battles/gen2.js
deleted file mode 100644
index dbf1547ae643..000000000000
--- a/test/random-battles/gen2.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * Tests for Gen 2 randomized formats
- */
-'use strict';
-
-const assert = require('../assert');
-const {testTeam, validateLearnset} = require('./tools');
-
-describe('[Gen 2] Random Battle (slow)', () => {
- const options = {format: 'gen2randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen2/sets.json`);
- const dex = Dex.forFormat(options.format);
-
- describe("New set format", () => {
- const filename = '../../data/random-battles/gen2/sets.json';
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- const validRoles = [
- "Fast Attacker", "Setup Sweeper", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Generalist", "Thief user",
- ];
- for (const [id, sets] of Object.entries(setsJSON)) {
- const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
- assert(Array.isArray(sets.sets));
- for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- for (const move of set.movepool) {
- const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert(move === dexMove.id || move.startsWith('hiddenpower'), `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'ubers', 'gen2'), `${species.name} can't learn ${move}`);
- }
- for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
- }
- if (set.preferredTypes) {
- for (const type of set.preferredTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Preferred Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Preferred Type: ${type}`);
- }
- for (let i = 0; i < set.preferredTypes.length - 1; i++) {
- assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `${species} preferredTypes should be sorted alphabetically`);
- }
- }
- }
- }
- });
- });
-
- it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const preferredTypes = set.preferredTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- const preferredType = preferredTypes ? preferredTypes[j % preferredTypes.length] : '';
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 4; i++) {
- const rapidSpin = i % 2;
- const spikes = Math.floor(i / 2) % 2;
- teamDetails = {rapidSpin, spikes};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- assert(!moves.size, species);
- }
- }
- });
-});
diff --git a/test/random-battles/gen3.js b/test/random-battles/gen3.js
deleted file mode 100644
index a49354915f3c..000000000000
--- a/test/random-battles/gen3.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Tests for Gen 3 randomized formats
- */
-'use strict';
-
-const assert = require('../assert');
-const {testTeam, validateLearnset} = require('./tools');
-
-describe('[Gen 3] Random Battle (slow)', () => {
- const options = {format: 'gen3randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen3/sets.json`);
- const dex = Dex.forFormat(options.format);
-
- describe("New set format", () => {
- const filename = '../../data/random-battles/gen3/sets.json';
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- const validRoles = [
- "Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker",
- "Bulky Setup", "Staller", "Bulky Support", "Generalist", "Berry Sweeper",
- ];
- for (const [id, sets] of Object.entries(setsJSON)) {
- const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
- assert(Array.isArray(sets.sets));
- for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- for (const move of set.movepool) {
- const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert(move === dexMove.id || move.startsWith('hiddenpower'), `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'anythinggoes', 'gen3'), `${species.name} can't learn ${move}`);
- }
- for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
- }
- if (set.preferredTypes) {
- for (const type of set.preferredTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Preferred Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Preferred Type: ${type}`);
- }
- for (let i = 0; i < set.preferredTypes.length - 1; i++) {
- assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `${species} preferredTypes should be sorted alphabetically`);
- }
- }
- }
- }
- });
- });
-
- it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const preferredTypes = set.preferredTypes;
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const preferredType = preferredTypes ? preferredTypes.join() : '';
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, false, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- assert(!moves.size, species);
- }
- }
- });
-});
diff --git a/test/random-battles/gen4.js b/test/random-battles/gen4.js
deleted file mode 100644
index 1612f07033ea..000000000000
--- a/test/random-battles/gen4.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Tests for Gen 4 randomized formats
- */
-'use strict';
-
-const assert = require('../assert');
-const {testTeam, testSet, testHiddenPower, validateLearnset} = require('./tools');
-
-describe('[Gen 4] Random Battle (slow)', () => {
- const options = {format: 'gen4randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen4/sets.json`);
- const dex = Dex.forFormat(options.format);
-
- describe("New set format", () => {
- const filename = '../../data/random-battles/gen4/sets.json';
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- const validRoles = [
- "Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker",
- "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner",
- ];
- for (const [id, sets] of Object.entries(setsJSON)) {
- const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
- assert(Array.isArray(sets.sets));
- for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- for (const move of set.movepool) {
- const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert(move === dexMove.id || move.startsWith('hiddenpower'), `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'anythinggoes', 'gen4'), `${species.name} can't learn ${move}`);
- }
- for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
- }
- if (set.preferredTypes) {
- for (const type of set.preferredTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Preferred Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Preferred Type: ${type}`);
- }
- for (let i = 0; i < set.preferredTypes.length - 1; i++) {
- assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `${species} preferredTypes should be sorted alphabetically`);
- }
- }
- }
- }
- });
- });
-
- it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const preferredTypes = set.preferredTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset as the lead, teamDetails is always empty for this
- const preferredType = preferredTypes ? preferredTypes[j % preferredTypes.length] : '';
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, true, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 4; i++) {
- const rapidSpin = i % 2;
- const stealthRock = Math.floor(i / 2) % 2;
- teamDetails = {rapidSpin, stealthRock};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- assert(!moves.size, species);
- }
- }
- });
-
- it('should not generate Shaymin-Sky without Air Slash', () => {
- testSet('shayminsky', options, set => assert(set.moves.includes('airslash'), `got ${set.moves}`));
- });
-
- it('should prevent double Hidden Power', () => testHiddenPower('magnezone', options));
-
- it('should give Yanmega Speed Boost if it has Protect', () => {
- testSet('yanmega', options, set => {
- if (set.ability !== 'Speed Boost') return;
- assert(set.moves.includes('protect'), `got ${set.moves}`);
- });
- });
-});
diff --git a/test/random-battles/gen5.js b/test/random-battles/gen5.js
index 75d6b32fcc61..8d501077a9ec 100644
--- a/test/random-battles/gen5.js
+++ b/test/random-battles/gen5.js
@@ -4,110 +4,10 @@
'use strict';
const assert = require('../assert');
-const {testTeam, testSet, testHiddenPower, testAlwaysHasMove, validateLearnset} = require('./tools');
+const {testSet} = require('./tools');
describe('[Gen 5] Random Battle (slow)', () => {
const options = {format: 'gen5randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen5/sets.json`);
- const dex = Dex.forFormat(options.format);
-
- describe("New set format", () => {
- const filename = '../../data/random-battles/gen5/sets.json';
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- const validRoles = [
- "Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker",
- "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner",
- ];
- for (const [id, sets] of Object.entries(setsJSON)) {
- const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
- assert(Array.isArray(sets.sets));
- for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- for (const move of set.movepool) {
- const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert(move === dexMove.id || move.startsWith('hiddenpower'), `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'anythinggoes', 'gen5'), `${species.name} can't learn ${move}`);
- }
- for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
- }
- if (set.preferredTypes) {
- for (const type of set.preferredTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Preferred Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Preferred Type: ${type}`);
- }
- for (let i = 0; i < set.preferredTypes.length - 1; i++) {
- assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `${species} preferredTypes should be sorted alphabetically`);
- }
- }
- }
- }
- });
- });
-
- it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const preferredTypes = set.preferredTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset as the lead, teamDetails is always empty for this
- const preferredType = preferredTypes ? preferredTypes[j % preferredTypes.length] : '';
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, true, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 4; i++) {
- const rapidSpin = i % 2;
- const stealthRock = Math.floor(i / 2) % 2;
- teamDetails = {rapidSpin, stealthRock};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- assert(!moves.size, species);
- }
- }
- });
-
- it('should prevent double Hidden Power', () => {
- testHiddenPower('ampharos', options);
- testHiddenPower('venusaur', options);
- });
-
- it('should give Venusaur four moves', () => {
- testSet(
- 'venusaur',
- {format: 'gen5randombattle', rounds: 1, seed: [2201, 2201, 2201, 2201]},
- set => assert.equal(set.moves.length, 4, `got ${JSON.stringify(set.moves)}`)
- );
- });
it('should prevent unreleased HAs from being used', () => {
testSet('chandelure', options, set => assert.notEqual(set.ability, 'Shadow Tag'));
@@ -116,8 +16,4 @@ describe('[Gen 5] Random Battle (slow)', () => {
it('should not give Ursaring Eviolite', () => {
testSet('ursaring', options, set => assert.notEqual(set.item, 'Eviolite'));
});
-
- it('should always give Watchog Return', () => {
- testAlwaysHasMove('watchog', options, 'return');
- });
});
diff --git a/test/random-battles/gen6.js b/test/random-battles/gen6.js
index 2560355163be..e5b1fb8853d1 100644
--- a/test/random-battles/gen6.js
+++ b/test/random-battles/gen6.js
@@ -4,98 +4,10 @@
'use strict';
const assert = require('../assert');
-const {testTeam, testNotBothMoves, testSet, testHiddenPower, testAlwaysHasMove, validateLearnset} = require('./tools');
+const {testSet} = require('./tools');
describe('[Gen 6] Random Battle (slow)', () => {
const options = {format: 'gen6randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen6/sets.json`);
- const dex = Dex.forFormat(options.format);
-
- describe("New set format", () => {
- const filename = '../../data/random-battles/gen6/sets.json';
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- const validRoles = [
- "Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker",
- "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot",
- ];
- for (const [id, sets] of Object.entries(setsJSON)) {
- const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
- assert(Array.isArray(sets.sets));
- for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- for (const move of set.movepool) {
- const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert(move === dexMove.id || move.startsWith('hiddenpower'), `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'anythinggoes', 'gen6'), `${species.name} can't learn ${move}`);
- }
- for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
- }
- if (set.preferredTypes) {
- for (const type of set.preferredTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Preferred Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Preferred Type: ${type}`);
- }
- for (let i = 0; i < set.preferredTypes.length - 1; i++) {
- assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `${species} preferredTypes should be sorted alphabetically`);
- }
- }
- }
- }
- });
- });
-
- it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const preferredTypes = set.preferredTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset as the lead, teamDetails is always empty for this
- const preferredType = preferredTypes ? preferredTypes[j % preferredTypes.length] : '';
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, true, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 8; i++) {
- const defog = i % 2;
- const stealthRock = Math.floor(i / 2) % 2;
- const stickyWeb = Math.floor(i / 4) % 2;
- teamDetails = {defog, stealthRock, stickyWeb};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- assert(!moves.size, species);
- }
- }
- });
it('should not give mega evolution abilities to base formes', () => {
testSet('manectricmega', {rounds: 1, ...options}, set => {
@@ -103,40 +15,10 @@ describe('[Gen 6] Random Battle (slow)', () => {
});
});
- it('should not select Air Slash and Hurricane together', () => {
- testNotBothMoves('swanna', options, 'hurricane', 'airslash');
- });
-
- it('should enforce STAB properly', () => {
- testAlwaysHasMove('hariyama', options, 'closecombat');
- testAlwaysHasMove('rapidash', options, 'flareblitz');
- });
-
- it('should give Drifblim only one Ghost-type attack', () => {
- testSet('drifblim', options, set => {
- assert.equal(set.moves.filter(m => {
- const move = Dex.moves.get(m);
- return move.type === 'Ghost' && move.category !== 'Status';
- }).length, 1, `got ${JSON.stringify(set.moves)}`);
- });
- });
-
- it('should prevent double Hidden Power', () => testHiddenPower('thundurustherian', options));
-
- it('should always give Mega Glalie Return', () => testAlwaysHasMove('glaliemega', options, 'return'));
-
it('should not give Ursaring Eviolite', () => {
testSet('ursaring', options, set => assert.notEqual(set.item, 'Eviolite'));
});
- it('should always give Quagsire Unaware', () => {
- testSet('quagsire', options, set => assert.equal(set.ability, 'Unaware'));
- });
-
- it('should always give Quagsire Recover', () => {
- testAlwaysHasMove('quagsire', options, 'recover');
- });
-
it('should not give Raikou Volt Absorb', () => {
testSet('raikou', options, set => assert.notEqual(set.ability, 'Volt Absorb'));
});
@@ -148,8 +30,4 @@ describe('[Gen 6] Random Battle (slow)', () => {
it('should not give Entei Flash Fire', () => {
testSet('entei', options, set => assert.notEqual(set.ability, 'Flash Fire'));
});
-
- it('should only give Charizard one of Air Slash and Acrobatics', () => {
- testNotBothMoves('charizard', options, 'airslash', 'acrobatics');
- });
});
diff --git a/test/random-battles/gen7.js b/test/random-battles/gen7.js
index eef3342028b3..e7bbc7f112b8 100644
--- a/test/random-battles/gen7.js
+++ b/test/random-battles/gen7.js
@@ -4,163 +4,18 @@
'use strict';
const assert = require('../assert');
-const {testTeam, testNotBothMoves, testSet, testHiddenPower, testAlwaysHasMove, validateLearnset} = require('./tools');
+const {testSet} = require('./tools');
describe('[Gen 7] Random Battle (slow)', () => {
const options = {format: 'gen7randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen7/sets.json`);
- const dex = Dex.forFormat(options.format);
- describe("New set format", () => {
- const filename = '../../data/random-battles/gen7/sets.json';
- it(`${filename} should have valid set data`, () => {
- const setsJSON = require(filename);
- const validRoles = [
- "Fast Attacker", "Setup Sweeper", "Wallbreaker", "Z-Move user", "Bulky Attacker",
- "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot",
- ];
- for (const [id, sets] of Object.entries(setsJSON)) {
- const species = Dex.species.get(id);
- assert(species.exists, `Misspelled species ID: ${id}`);
- assert(Array.isArray(sets.sets));
- for (const set of sets.sets) {
- assert(validRoles.includes(set.role), `Set for ${species.name} has invalid role: ${set.role}`);
- for (const move of set.movepool) {
- const dexMove = Dex.moves.get(move);
- assert(dexMove.exists, `${species.name} has invalid move: ${move}`);
- assert(move === dexMove.id || move.startsWith('hiddenpower'), `${species.name} has misformatted move: ${move}`);
- assert(validateLearnset(dexMove, {species}, 'anythinggoes', 'gen7'), `${species.name} can't learn ${move}`);
- }
- for (let i = 0; i < set.movepool.length - 1; i++) {
- assert(set.movepool[i + 1] > set.movepool[i], `${species} movepool should be sorted alphabetically`);
- }
- if (set.preferredTypes) {
- for (const type of set.preferredTypes) {
- const dexType = Dex.types.get(type);
- assert(dexType.exists, `${species.name} has invalid Preferred Type: ${type}`);
- assert.equal(type, dexType.name, `${species.name} has misformatted Preferred Type: ${type}`);
- }
- for (let i = 0; i < set.preferredTypes.length - 1; i++) {
- assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `${species} preferredTypes should be sorted alphabetically`);
- }
- }
- }
- }
+ it('should not give mega evolution abilities to base formes', () => {
+ testSet('manectricmega', {rounds: 1, ...options}, set => {
+ assert(set.ability !== 'Intimidate', 'Mega Manectric should not have Intimidate before it mega evolves');
});
});
- it('all Pokemon should have 4 moves, except for Ditto and Unown', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const preferredTypes = set.preferredTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset as the lead, teamDetails is always empty for this
- const preferredType = preferredTypes ? preferredTypes[j % preferredTypes.length] : '';
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, true, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 8; i++) {
- const defog = i % 2;
- const stealthRock = Math.floor(i / 2) % 2;
- const stickyWeb = Math.floor(i / 4) % 2;
- teamDetails = {defog, stealthRock, stickyWeb};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, preferredType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- if (moves.size) console.log(moves, species);
- assert(!moves.size, species);
- }
- }
- });
-
- it('should not generate Calm Mind + Yawn', () => {
- testNotBothMoves('chimecho', options, 'calmmind', 'yawn');
- });
-
- it('should give Azumarill Aqua Jet', () => {
- testSet('azumarill', options, set => {
- assert(set.moves.includes('aquajet'), `Azumarill: got ${set.moves}`);
- });
- });
-
- it('should give Typhlosion Eruption', () => {
- testSet('typhlosion', options, set => {
- assert(set.moves.includes('eruption'), `Typhlosion: got ${set.moves}`);
- });
- });
-
- it('should not generate Dragon Tail as the only STAB move', () => {
- // Mono-Dragon Pokémon chosen as test dummies for simplicity
- testSet('druddigon', options, set => {
- if (set.moves.includes('dragontail')) {
- assert(set.moves.includes('outrage'), `Druddigon: got ${set.moves}`);
- }
- });
-
- testSet('goodra', options, set => {
- if (set.moves.includes('dragontail')) {
- assert(set.moves.includes('dracometeor') || set.moves.includes('dragonpulse'), `Goodra: got ${set.moves}`);
- }
- });
- });
-
- it('should not generate Swords Dance + Ice Beam', () => {
- testNotBothMoves('arceusground', options, 'swordsdance', 'icebeam');
- });
-
- it('should prevent double Hidden Power', () => testHiddenPower('thundurustherian', options));
-
- it('should never give Xerneas Assault Vest', () => {
- testSet('xerneas', options, set => assert.notEqual(set.item, 'Assault Vest'));
- });
-
- it('should always give Gastrodon Recover', () => {
- testAlwaysHasMove('gastrodon', options, 'recover');
- });
-
- it('should never give Poliwrath both Rain Dance and Rest', () => {
- testNotBothMoves('poliwrath', options, 'raindance', 'rest');
- });
-
it('should not give Ursaring Eviolite', () => {
testSet('ursaring', options, set => assert.notEqual(set.item, 'Eviolite'));
});
-
- it('should always give Mega Glalie Return', () => testAlwaysHasMove('glaliemega', options, 'return'));
-
- it('should not give Zebstrika Thunderbolt and Wild Charge', () => {
- testNotBothMoves('zebstrika', options, 'thunderbolt', 'wildcharge');
- });
-
- it('should always give Mega Diancie Moonblast if it has Calm Mind', () => {
- testSet('dianciemega', options, set => {
- if (!set.moves.includes('calmmind')) return;
- assert(set.moves.includes('moonblast'), `Diancie: got ${set.moves}`);
- });
- });
});
diff --git a/test/random-battles/gen9.js b/test/random-battles/gen9.js
index daf5f9467d99..fd7f4a5f8b67 100644
--- a/test/random-battles/gen9.js
+++ b/test/random-battles/gen9.js
@@ -5,62 +5,9 @@
const {testTeam, testAlwaysHasMove} = require('./tools');
const assert = require('../assert');
-const Teams = require('./../../dist/sim/teams').Teams;
-const Dex = require('./../../dist/sim/dex').Dex;
describe('[Gen 9] Random Battle (slow)', () => {
const options = {format: 'gen9randombattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen9/sets.json`);
- const dex = Dex.forFormat(options.format);
-
- it('all Pokemon should have 4 moves, except for Ditto', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const teraTypes = set.teraTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset as the lead, teamDetails is always empty for this
- const teraType = teraTypes[j % teraTypes.length];
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, true, false, movePool, teraType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 8; i++) {
- const defog = i % 2;
- const stealthRock = Math.floor(i / 2) % 2;
- const stickyWeb = Math.floor(i / 4) % 2;
- teamDetails = {defog, stealthRock, stickyWeb};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, false, movePool, teraType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- assert.false(moves.size, `The following moves on ${species.name} are unused: ${[...moves].join(', ')}`);
- }
- }
- });
-
it("should always give Iron Bundle Freeze-Dry", () => {
testAlwaysHasMove('ironbundle', options, 'freezedry');
});
@@ -75,57 +22,3 @@ describe('[Gen 9] Monotype Random Battle (slow)', () => {
});
});
});
-
-describe('[Gen 9] Random Doubles Battle (slow)', () => {
- const options = {format: 'gen9randomdoublesbattle'};
- const setsJSON = require(`../../dist/data/random-battles/gen9/doubles-sets.json`);
- const dex = Dex.forFormat(options.format);
-
- it('all Pokemon should have 4 moves, except for Ditto', function () {
- // This test takes more than 2000ms
- testTeam({...options, rounds: 100}, team => {
- for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.moves.length === 4);
- });
- });
-
- it('all moves on all sets should be obtainable', function () {
- const generator = Teams.getGenerator(options.format);
- const rounds = 100;
- for (const pokemon of Object.keys(setsJSON)) {
- const species = dex.species.get(pokemon);
- const sets = setsJSON[pokemon]["sets"];
- const types = species.types;
- const abilities = new Set(Object.values(species.abilities));
- if (species.unreleasedHidden) abilities.delete(species.abilities.H);
- for (const set of sets) {
- const role = set.role;
- const moves = new Set(set.movepool.map(m => dex.moves.get(m).id));
- const teraTypes = set.teraTypes;
- let teamDetails = {};
- // Go through all possible teamDetails combinations, if necessary
- for (let j = 0; j < rounds; j++) {
- // Generate a moveset as the lead, teamDetails is always empty for this
- const teraType = teraTypes[j % teraTypes.length];
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, {}, species, true, true, movePool, teraType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- // Generate a moveset for each combination of relevant teamDetails
- for (let i = 0; i < 8; i++) {
- const defog = i % 2;
- const stealthRock = Math.floor(i / 2) % 2;
- const stickyWeb = Math.floor(i / 4) % 2;
- teamDetails = {defog, stealthRock, stickyWeb};
- // randomMoveset() deletes moves from the movepool, so recreate it every time
- const movePool = set.movepool.map(m => dex.moves.get(m).id);
- const moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, true, movePool, teraType, role);
- for (const move of moveSet) moves.delete(move);
- if (!moves.size) break;
- }
- if (!moves.size) break;
- }
- assert(!moves.size);
- }
- }
- });
-});
diff --git a/test/sim/abilities/asone.js b/test/sim/abilities/asone.js
new file mode 100644
index 000000000000..80d0084e85fb
--- /dev/null
+++ b/test/sim/abilities/asone.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const assert = require('./../../assert');
+const common = require('./../../common');
+
+let battle;
+
+describe(`As One`, function () {
+ afterEach(function () {
+ battle.destroy();
+ });
+
+ it(`should work if the user is Transformed`, function () {
+ battle = common.createBattle([[
+ {species: 'ditto', ability: 'imposter', moves: ['transform']},
+ ], [
+ {species: 'calyrexshadow', ability: 'asonespectrier', item: 'cheriberry', moves: ['glare', 'sleeptalk', 'astralbarrage']},
+ {species: 'wynaut', moves: ['sleeptalk']},
+ ]]);
+
+ const ditto = battle.p1.active[0];
+ const calyrex = battle.p2.active[0];
+ battle.makeChoices('move glare', 'move sleeptalk');
+ assert.equal(calyrex.status, 'par', `Calyrex should not have eaten its Berry, being affected by Ditto-Calyrex's Unnerve`);
+
+ battle.makeChoices('move astralbarrage', 'move sleeptalk');
+ assert.statStage(ditto, 'spa', 1);
+ });
+});
diff --git a/test/sim/abilities/download.js b/test/sim/abilities/download.js
new file mode 100644
index 000000000000..4e9725208b61
--- /dev/null
+++ b/test/sim/abilities/download.js
@@ -0,0 +1,92 @@
+'use strict';
+
+const assert = require('./../../assert');
+const common = require('./../../common');
+
+let battle;
+
+describe('Download', () => {
+ afterEach(() => battle.destroy());
+
+ it('should boost based on which defensive stat is higher', () => {
+ battle = common.createBattle([[
+ {species: 'porygon', moves: ['sleeptalk'], ability: 'download'},
+ {species: 'furret', moves: ['sleeptalk']},
+ ], [
+ {species: 'stonjourner', moves: ['sleeptalk']},
+ {species: 'chansey', moves: ['sleeptalk']},
+ ]]);
+ assert.statStage(battle.p1.active[0], 'spa', 1);
+ battle.makeChoices('switch 2', 'switch 2');
+ battle.makeChoices('switch 2', 'auto');
+ assert.statStage(battle.p1.active[0], 'atk', 1);
+ });
+
+ it('should boost Special Attack if both stats are tied', () => {
+ battle = common.createBattle([[
+ {species: 'porygon', moves: ['sleeptalk'], ability: 'download'},
+ ], [
+ {species: 'mew', moves: ['sleeptalk']},
+ ]]);
+ assert.statStage(battle.p1.active[0], 'spa', 1);
+ assert.statStage(battle.p1.active[0], 'atk', 0);
+ });
+
+ it('should boost based on the total of both foes in a Double Battle', () => {
+ battle = common.createBattle({gameType: 'doubles'}, [[
+ {species: 'porygon', moves: ['sleeptalk'], ability: 'download'},
+ {species: 'blissey', moves: ['sleeptalk']},
+ ], [
+ {species: 'blissey', level: 1, moves: ['sleeptalk']},
+ {species: 'stonjourner', moves: ['sleeptalk']},
+ ]]);
+ assert.statStage(battle.p1.active[0], 'spa', 1);
+ assert.statStage(battle.p1.active[0], 'atk', 0);
+ });
+
+ it('should trigger even if the foe is behind a Substitute', () => {
+ battle = common.createBattle([[
+ {species: 'furret', moves: ['sleeptalk']},
+ {species: 'porygon', ability: 'download', moves: ['sleeptalk']},
+ ], [
+ {species: 'blissey', moves: ['substitute']},
+ ]]);
+ battle.makeChoices();
+ battle.makeChoices('switch 2', 'auto');
+ assert.statStage(battle.p1.active[0], 'atk', 1);
+ });
+
+ describe('Gen 4', () => {
+ it('should not trigger if the foe is behind a Substitute', () => {
+ battle = common.gen(4).createBattle([[
+ {species: 'furret', moves: ['sleeptalk']},
+ {species: 'porygon', ability: 'download', moves: ['sleeptalk']},
+ ], [
+ {species: 'ampharos', moves: ['substitute']},
+ ]]);
+ battle.makeChoices();
+ battle.makeChoices('switch 2', 'auto');
+ assert.statStage(battle.p1.active[0], 'atk', 0);
+ assert.statStage(battle.p1.active[0], 'spa', 0);
+ });
+
+ it('in Double Battles, should only account for foes not behind a Substitute', () => {
+ battle = common.gen(4).createBattle({gameType: 'doubles'}, [[
+ {species: 'furret', moves: ['sleeptalk']},
+ {species: 'ampharos', moves: ['sleeptalk']},
+ {species: 'porygon', ability: 'download', moves: ['sleeptalk']},
+ ], [
+ {species: 'blissey', moves: ['substitute']},
+ {species: 'furret', moves: ['sleeptalk', 'substitute']},
+ ]]);
+ battle.makeChoices();
+ battle.makeChoices('move 1, switch 3', 'auto');
+ assert.statStage(battle.p1.active[1], 'atk', 0);
+ assert.statStage(battle.p1.active[1], 'spa', 1);
+ battle.makeChoices('move 1, switch 3', 'move 1, move 2');
+ battle.makeChoices('move 1, switch 3', 'auto');
+ assert.statStage(battle.p1.active[1], 'atk', 0);
+ assert.statStage(battle.p1.active[1], 'spa', 0);
+ });
+ });
+});
diff --git a/test/sim/abilities/protean.js b/test/sim/abilities/protean.js
index f7d0b6847a23..8d91bbe3cea1 100644
--- a/test/sim/abilities/protean.js
+++ b/test/sim/abilities/protean.js
@@ -22,6 +22,19 @@ describe('Protean', function () {
assert(cinder.hasType('Fighting'));
});
+ it(`should change the user's type for submoves to the type of that submove, not the move calling it`, function () {
+ battle = common.gen(6).createBattle([[
+ {species: 'Wynaut', ability: 'protean', moves: ['sleeptalk', 'flamethrower']},
+ ], [
+ {species: 'Regieleki', moves: ['spore']},
+ ]]);
+
+ battle.makeChoices();
+ const wynaut = battle.p1.active[0];
+ assert(battle.log.every(line => !line.includes('|Normal|')), `It should not temporarily become Normal-type`);
+ assert(wynaut.hasType('Fire'));
+ });
+
it(`should not change the user's type when using moves that fail earlier than Protean will activate`, function () {
battle = common.createBattle([[
{species: 'Kecleon', ability: 'protean', moves: ['fling', 'suckerpunch', 'steelroller', 'aurawheel']},
diff --git a/test/sim/abilities/slowstart.js b/test/sim/abilities/slowstart.js
index 8241342d36a0..81971e3ac33e 100644
--- a/test/sim/abilities/slowstart.js
+++ b/test/sim/abilities/slowstart.js
@@ -10,7 +10,7 @@ describe(`Slow Start`, function () {
battle.destroy();
});
- it(`should not delay activation on switch-in, unlike Speed Boost`, function () {
+ it(`should delay activation on switch-in, like Speed Boost`, function () {
battle = common.createBattle([[
{species: 'diglett', moves: ['sleeptalk']},
{species: 'regigigas', ability: 'slowstart', item: 'normaliumz', moves: ['sleeptalk']},
@@ -19,9 +19,14 @@ describe(`Slow Start`, function () {
]]);
battle.makeChoices('switch 2', 'auto');
for (let i = 0; i < 4; i++) { battle.makeChoices(); }
- const log = battle.getDebugLog();
- const slowStartEnd = log.indexOf('|-end|p1a: Regigigas|Slow Start');
- assert(slowStartEnd > -1, 'Slow Start should end in 5 turns, including the turn it switched in.');
+ let log = battle.getDebugLog();
+ let slowStartEnd = log.indexOf('|-end|p1a: Regigigas|Slow Start');
+ assert.false(slowStartEnd > -1, 'Slow Start should remain in effect after 4 active turns.');
+
+ battle.makeChoices();
+ log = battle.getDebugLog();
+ slowStartEnd = log.indexOf('|-end|p1a: Regigigas|Slow Start');
+ assert(slowStartEnd > -1, 'Slow Start should not be in effect after 5 active turns.');
});
it(`[Gen 7] should halve the user's Special Attack when using a special Z-move`, function () {
diff --git a/test/sim/abilities/terashell.js b/test/sim/abilities/terashell.js
index ab43a4eb9613..6625ffee518c 100644
--- a/test/sim/abilities/terashell.js
+++ b/test/sim/abilities/terashell.js
@@ -87,4 +87,81 @@ describe('Tera Shell', function () {
damage = espeon.maxhp - espeon.hp;
assert.bounded(damage, [33, 39], `Tera Shell should have activated because current species is Terapagos`);
});
+
+ it(`should not weaken the damage from Struggle`, function () {
+ battle = common.createBattle([[
+ {species: 'Terapagos', ability: 'terashift', moves: ['luckychant']},
+ ], [
+ {species: 'Slowking', item: 'assaultvest', moves: ['sleeptalk']},
+ ]]);
+
+ battle.makeChoices();
+
+ const terapagos = battle.p1.active[0];
+ const damage = terapagos.maxhp - terapagos.hp;
+ assert.bounded(damage, [27, 32], `Tera Shell should not have reduced the damage Struggle dealt`);
+ });
+
+ it.skip(`should not continue to weaken attacks after taking damage from a Future attack`, function () {
+ battle = common.createBattle([[
+ {species: 'Terapagos', ability: 'terashift', moves: ['sleeptalk']},
+ {species: 'Espeon', moves: ['sleeptalk']},
+ ], [
+ {species: 'Slowking', moves: ['sleeptalk', 'wickedblow', 'futuresight']},
+ ]]);
+
+ battle.makeChoices('auto', 'move futuresight');
+ battle.makeChoices();
+ battle.makeChoices();
+
+ const terapagos = battle.p1.active[0];
+ let damage = terapagos.maxhp - terapagos.hp;
+ assert.bounded(damage, [59, 70], `Tera Shell should have reduced the damage Future Sight dealt`);
+
+ battle.makeChoices('switch 2', 'auto');
+ battle.makeChoices('switch 2', 'move wickedblow');
+ damage = terapagos.maxhp - terapagos.hp - damage;
+ assert.bounded(damage, [59, 70], `Tera Shell should not have reduced the damage Wicked Blow dealt`);
+ });
+
+ it.skip(`should activate, but not weaken, moves with fixed damage`, function () {
+ battle = common.createBattle([[
+ {species: 'Terapagos', ability: 'terashift', evs: {hp: 252}, moves: ['recover', 'seismictoss']},
+ {species: 'Magikarp', moves: ['sleeptalk']},
+ ], [
+ {species: 'Slowpoke', ability: 'noguard', moves: ['seismictoss', 'superfang', 'counter']},
+ {species: 'Shuckle', moves: ['finalgambit']},
+ {species: 'Wynaut', ability: 'noguard', moves: ['sheercold']},
+ ]]);
+
+ const terapagos = battle.p1.active[0];
+
+ battle.makeChoices('auto', 'move seismictoss');
+ let damage = terapagos.maxhp - terapagos.hp;
+ assert.equal(damage, 100, `Tera Shell should not have reduced the damage Seismic Toss dealt`);
+ assert(battle.log[battle.lastMoveLine + 1].endsWith('Tera Shell'), `Tera Shell should have activated on Seismic Toss`);
+
+ battle.makeChoices('auto', 'move superfang');
+ damage = terapagos.maxhp - terapagos.hp;
+ assert.equal(damage, Math.floor(terapagos.maxhp / 2), `Tera Shell should not have reduced the damage Super Fang dealt`);
+ assert(battle.log[battle.lastMoveLine + 1].endsWith('Tera Shell'), `Tera Shell should have activated on Super Fang`);
+
+ battle.makeChoices('auto', 'move counter');
+ battle.makeChoices('move seismictoss', 'move counter');
+ damage = terapagos.maxhp - terapagos.hp;
+ assert.equal(damage, 200, `Tera Shell should not have reduced the damage Counter dealt`);
+ assert(battle.log[battle.lastMoveLine + 1].endsWith('Tera Shell'), `Tera Shell should have activated on Counter`);
+
+ battle.makeChoices('auto', 'switch 2');
+ const shuckle = battle.p2.active[0];
+ battle.makeChoices('auto', 'move finalgambit');
+ damage = terapagos.maxhp - terapagos.hp;
+ assert.equal(damage, shuckle.maxhp, `Tera Shell should not have reduced the damage Final Gambit dealt`);
+ assert(battle.log[battle.lastMoveLine + 1].endsWith('Tera Shell'), `Tera Shell should have activated on Final Gambit`);
+
+ battle.choose('p2', 'switch 3');
+ battle.makeChoices('auto', 'move sheercold');
+ assert.fainted(terapagos);
+ assert(battle.log[battle.lastMoveLine + 1].endsWith('Tera Shell'), `Tera Shell should have activated on Sheer Cold`);
+ });
});
diff --git a/test/sim/decisions.js b/test/sim/decisions.js
index ce9f4d43b161..0c650e5d4cec 100644
--- a/test/sim/decisions.js
+++ b/test/sim/decisions.js
@@ -1239,7 +1239,7 @@ describe('Choice internals', function () {
p1.chooseMove(1);
p2.chooseMove(1);
p2.chooseMove(1);
- battle.commitDecisions();
+ battle.commitChoices();
assert.equal(battle.turn, 2);
assert.statStage(p2.active[0], 'atk', -1);
@@ -1248,7 +1248,7 @@ describe('Choice internals', function () {
p1.chooseMove('synthesis');
p2.chooseMove('surf');
p2.chooseMove('calmmind');
- battle.commitDecisions();
+ battle.commitChoices();
assert.equal(battle.turn, 3);
assert.fullHP(p1.active[1]);
@@ -1257,7 +1257,7 @@ describe('Choice internals', function () {
p1.chooseMove('2');
p2.chooseMove('1');
p2.chooseMove('calmmind');
- battle.commitDecisions();
+ battle.commitChoices();
assert.equal(battle.turn, 4);
assert.fullHP(p1.active[1]);
@@ -1282,13 +1282,13 @@ describe('Choice internals', function () {
p1.chooseMove('selfdestruct');
p2.chooseMove('recover');
p2.chooseMove('recover');
- battle.commitDecisions();
+ battle.commitChoices();
assert.fainted(p1.active[0]);
assert.fainted(p1.active[1]);
p1.chooseSwitch(4);
p1.chooseSwitch(3);
- battle.commitDecisions();
+ battle.commitChoices();
assert.equal(battle.turn, 2);
assert.equal(p1.active[0].name, 'Ekans');
assert.equal(p1.active[1].name, 'Koffing');
@@ -1316,7 +1316,7 @@ describe('Choice internals', function () {
`Expected switch to fail`
);
p2.choose('move recover, move recover');
- battle.commitDecisions();
+ battle.commitChoices();
assert.equal(battle.turn, 2);
assert.equal(p1.active[0].name, 'Mew');
@@ -1329,7 +1329,7 @@ describe('Choice internals', function () {
`Expected switch to fail`
);
p2.choose('move recover, move recover');
- battle.commitDecisions();
+ battle.commitChoices();
assert.equal(battle.turn, 3);
assert.equal(p1.active[0].name, 'Bulbasaur');
diff --git a/test/sim/items/metronome.js b/test/sim/items/metronome.js
index 799a17187a11..893233c43386 100644
--- a/test/sim/items/metronome.js
+++ b/test/sim/items/metronome.js
@@ -97,7 +97,7 @@ describe('Metronome (item)', function () {
assert.bounded(damage, [80, 95], `Solar Beam should be Metronome 1 boosted`);
});
- it.skip(`should use called moves to determine the Metronome multiplier`, function () {
+ it(`should use called moves to determine the Metronome multiplier`, function () {
battle = common.createBattle([[
{species: 'goomy', item: 'metronome', moves: ['copycat', 'surf']},
], [
diff --git a/test/sim/misc/endlessbattleclause.js b/test/sim/misc/endlessbattleclause.js
index 5bf58477860b..10cb747a1402 100644
--- a/test/sim/misc/endlessbattleclause.js
+++ b/test/sim/misc/endlessbattleclause.js
@@ -206,5 +206,5 @@ describe('Endless Battle Clause (slow)', () => {
// Endless Battle Clause doesn't take effect for 100 turns, so we artificially skip turns
// to get the turn counter to be in the range which could possibly trigger the clause
function skipTurns(battle, turns) {
- for (let i = 0; i < turns; i++) battle.nextTurn();
+ for (let i = 0; i < turns; i++) battle.endTurn();
}
diff --git a/test/sim/moves/psychicnoise.js b/test/sim/moves/psychicnoise.js
new file mode 100644
index 000000000000..ece6cdf204f4
--- /dev/null
+++ b/test/sim/moves/psychicnoise.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const assert = require('./../../assert');
+const common = require('./../../common');
+
+let battle;
+
+describe('Psychic Noise', function () {
+ afterEach(function () {
+ battle.destroy();
+ });
+
+ it(`should prevent the target from healing, like Heal Block`, function () {
+ battle = common.createBattle([[
+ {species: 'Wynaut', ability: 'battlearmor', moves: ['softboiled', 'sleeptalk']},
+ ], [
+ {species: 'Regieleki', moves: ['psychicnoise']},
+ ]]);
+ const wynaut = battle.p1.active[0];
+ battle.makeChoices();
+ assert.false.fullHP(wynaut);
+ assert.cantMove(() => battle.choose('p1', 'move softboiled'));
+ });
+
+ it.skip(`should prevent the target's ally from healing it with Life Dew`, function () {
+ battle = common.createBattle({gameType: 'doubles'}, [[
+ {species: 'Wynaut', ability: 'battlearmor', moves: ['sleeptalk']},
+ {species: 'Blissey', ability: 'battlearmor', moves: ['lifedew']},
+ ], [
+ {species: 'Regieleki', moves: ['psychicnoise']},
+ {species: 'Mew', moves: ['watergun']},
+ ]]);
+ const wynaut = battle.p1.active[0];
+ const blissey = battle.p1.active[1];
+ battle.makeChoices('auto', 'move psychicnoise 1, move watergun 2');
+ assert.false.fullHP(wynaut, `Wynaut should not be healed, because it is affected by Psychic Noise`);
+ assert.fullHP(blissey, `Blissey should be healed, because it is not affected by Psychic Noise`);
+ });
+});
diff --git a/test/sim/team-validator/breeding.js b/test/sim/team-validator/breeding.js
index a39178c7d324..ae1f02dba01e 100644
--- a/test/sim/team-validator/breeding.js
+++ b/test/sim/team-validator/breeding.js
@@ -222,17 +222,25 @@ describe('Team Validator', function () {
assert.legalTeam(team, 'gen4ou');
});
- it.skip('should reject Volbeat with both Lunge and Dizzy Punch in Gen 7', function () {
+ it('should reject Volbeat with both Lunge and Dizzy Punch in Gen 7', function () {
team = [
{species: 'volbeat', ability: 'swarm', moves: ['lunge', 'dizzypunch'], evs: {hp: 1}},
];
assert.false.legalTeam(team, 'gen7anythinggoes');
});
+ it('should allow level 5 Indeedee-M with Disarming Voice', function () {
+ team = [
+ {species: 'indeedee', level: 5, ability: 'innerfocus', moves: ['disarmingvoice'], evs: {hp: 1}},
+ ];
+ assert.legalTeam(team, 'gen8ou');
+ });
+
it('should allow egg moves on event formes in Gen 9', function () {
team = [
{species: 'ursalunabloodmoon', ability: 'mindseye', moves: ['yawn', 'bellydrum'], evs: {hp: 1}},
{species: 'greninjabond', ability: 'battlebond', moves: ['counter', 'switcheroo'], evs: {hp: 1}},
+ {species: 'pikachualola', ability: 'static', moves: ['wish', 'fakeout'], evs: {hp: 1}},
];
assert.legalTeam(team, 'gen9anythinggoes');
});