diff --git a/config/formats.ts b/config/formats.ts index c0a847f39c84..ae7472bbb8e9 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -68,7 +68,6 @@ export const Formats: FormatList = [ mod: 'gen9', team: 'random', gameType: 'multi', - searchShow: false, tournamentShow: false, rated: false, ruleset: [ diff --git a/server/chat-commands/core.ts b/server/chat-commands/core.ts index ee6a9b46cbe0..f1d927f4d7df 100644 --- a/server/chat-commands/core.ts +++ b/server/chat-commands/core.ts @@ -1420,6 +1420,54 @@ export const commands: Chat.ChatCommands = { `If no format is given, cancels searches for all formats.`, ], + requestpartner(target, room, user) { + const {targetUser, rest} = this.requireUser(target); + if (targetUser.locked && !user.locked) { + return this.popupReply(`That user is locked and cannot be invited to battles.`); + } + if (targetUser.id === user.id) { + return this.popupReply(`You cannot be your own partner.`); + } + if (user.locked && !targetUser.locked) { + return this.errorReply(`You are locked and cannot invite others to battles.`); + } + const format = Dex.formats.get(rest); + if (!format.exists) return this.popupReply(`Invalid format: ${rest}`); + if (format.gameType !== 'multi') { + return this.popupReply(`You cannot invite people to non-multibattle formats. Challenge them instead.`); + } + Ladders.challenges.add(new Ladders.GameChallenge(user.id, targetUser.id, format.id, { + acceptCommand: `/acceptpartner ${user.id}`, + rejectCommand: `/denypartner ${user.id}`, + message: `${user.name} wants you to play ${format.name} with them!`, + })); + }, + async acceptpartner(target, room, user, connection) { + const challenge = Ladders.challenges.resolveAcceptCommand(this); + Ladders.challenges.remove(challenge, true); + const format = Dex.formats.get(challenge.format); + const targetUser = Users.get(challenge.from); + if (!targetUser) return this.popupReply(`${challenge.from} is not available right now.`); + const search = await Ladders(format.id).prepBattle(connection, 'rated', user.battleSettings.team, !!format.rated, true); + if (search === null) return null; + targetUser.battleSettings.teammate = search; + const latestConn = Utils.sortBy(targetUser.connections.slice(), b => -b.lastActiveTime)[0]; + targetUser.chat(`/search ${format.id}`, null, latestConn); + targetUser.popup(`Your teammate has accepted, and a battle search has been started.`); + this.pmTarget = targetUser; + this.sendReply(`You accepted ${targetUser.name}'s partnership request!`); + user.send(`|updatesearch|${JSON.stringify({searching: [format.id], games: null})}`); + }, + denypartner(target, room, user) { + const {targetUser} = this.splitUser(target); + if (!targetUser) return this.popupReply(`User not found.`); + const chall = Ladders.challenges.search(user.id, targetUser.id); + if (!chall) return this.popupReply(`Challenge not found between you and ${targetUser.name}`); + Ladders.challenges.remove(chall, false); + this.popupReply(`Request denied.`); + targetUser.popup(`${user.id} denied your teammate request.`); + }, + chall: 'challenge', challenge(target, room, user, connection) { const {targetUser, targetUsername, rest: formatName} = this.splitUser(target); diff --git a/server/ladders-challenges.ts b/server/ladders-challenges.ts index b075a76524a2..9aaea89e96bd 100644 --- a/server/ladders-challenges.ts +++ b/server/ladders-challenges.ts @@ -30,6 +30,12 @@ export class BattleReady { this.challengeType = challengeType; this.time = Date.now(); } + static averageRatings(readies: BattleReady[]) { + const average = Math.round(readies.map(r => r.rating).reduce((a, b) => a + b) / readies.length); + for (const ready of readies) { + (ready as any).rating = average; + } + } } export abstract class AbstractChallenge { diff --git a/server/ladders.ts b/server/ladders.ts index 2c7cb33207d5..38ac8620a94a 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -37,7 +37,13 @@ class Ladder extends LadderStore { super(formatid); } - async prepBattle(connection: Connection, challengeType: ChallengeType, team: string | null = null, isRated = false) { + async prepBattle( + connection: Connection, + challengeType: ChallengeType, + team: string | null = null, + isRated = false, + noPartner = false + ) { // all validation for a battle goes through here const user = connection.user; const userid = user.id; @@ -137,6 +143,15 @@ class Ladder extends LadderStore { return null; } + if (Dex.formats.get(this.formatid).gameType === 'multi' && !noPartner) { + if (!user.battleSettings.teammate) { + connection.popup( + `You must have a teammate consent to play with you before playing this tier. Just fill out their name in the box and hit enter.` + ); + return null; + } + } + const settings = {...user.battleSettings, team: valResult.slice(1)}; user.battleSettings.inviteOnly = false; user.battleSettings.hidden = false; @@ -250,6 +265,10 @@ class Ladder extends LadderStore { formatTable.searches.delete(user.id); cancelCount++; } + if (user.battleSettings.teammate) { + const partner = Users.get(user.battleSettings.teammate.userid); + if (partner) Ladder.updateSearch(partner); + } Ladder.updateSearch(user); return cancelCount; @@ -329,6 +348,9 @@ class Ladder extends LadderStore { if (oldUserid !== user.id) return; if (!search) return; + if (user.battleSettings.teammate) { + BattleReady.averageRatings([search, user.battleSettings.teammate]); + } this.addSearch(search, user); } @@ -422,7 +444,6 @@ class Ladder extends LadderStore { static periodicMatch() { // In order from longest waiting to shortest waiting for (const [formatid, formatTable] of Ladders.searches) { - if (formatTable.playerCount > 2) continue; // TODO: implement const matchmaker = Ladders(formatid); let longest: [BattleReady, User] | null = null; for (const search of formatTable.searches.values()) { @@ -459,6 +480,10 @@ class Ladder extends LadderStore { missingUser = ready.userid; break; } + if (user.battleSettings.teammate) { + readies.push(user.battleSettings.teammate); + delete user.battleSettings.teammate; + } players.push({ user, team: ready.settings.team, diff --git a/server/rooms.ts b/server/rooms.ts index 39f8429a339d..1a30c6ee62c1 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1474,6 +1474,9 @@ export class GlobalRoomState { if (level === 50) displayCode |= 16; // 32 was previously used for Multi Battles if (format.bestOfDefault) displayCode |= 64; + if (format.gameType === 'multi') { + displayCode |= 32; + } this.formatList += ',' + displayCode.toString(16); } return this.formatList; diff --git a/server/users.ts b/server/users.ts index abc54fa164bf..401086b8dfb5 100644 --- a/server/users.ts +++ b/server/users.ts @@ -49,6 +49,7 @@ import {FS, Utils, ProcessManager} from '../lib'; import { Auth, GlobalAuth, SECTIONLEADER_SYMBOL, PLAYER_SYMBOL, HOST_SYMBOL, RoomPermission, GlobalPermission, } from './user-groups'; +import {BattleReady} from './ladders-challenges'; const MINUTES = 60 * 1000; const IDLE_TIMER = 60 * MINUTES; @@ -385,6 +386,7 @@ export class User extends Chat.MessageContext { hidden: boolean, inviteOnly: boolean, special?: string, + teammate?: BattleReady, }; isSysop: boolean;