mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
262 lines
8.5 KiB
TypeScript
262 lines
8.5 KiB
TypeScript
import type { ChallengeType } from './room-battle';
|
|
|
|
/**
|
|
* A bundle of:
|
|
- a ID
|
|
* - a battle format
|
|
* - a valid team for that format
|
|
* - misc other preferences for the battle
|
|
*
|
|
* To start a battle, you need one of these for every player.
|
|
*/
|
|
export class BattleReady {
|
|
readonly userid: ID;
|
|
readonly formatid: string;
|
|
readonly settings: User['battleSettings'];
|
|
readonly rating: number;
|
|
readonly challengeType: ChallengeType;
|
|
readonly time: number;
|
|
constructor(
|
|
userid: ID,
|
|
formatid: string,
|
|
settings: User['battleSettings'],
|
|
rating = 0,
|
|
challengeType: ChallengeType = 'challenge'
|
|
) {
|
|
this.userid = userid;
|
|
this.formatid = formatid;
|
|
this.settings = settings;
|
|
this.rating = rating;
|
|
this.challengeType = challengeType;
|
|
this.time = Date.now();
|
|
}
|
|
}
|
|
|
|
export abstract class AbstractChallenge {
|
|
from: ID;
|
|
to: ID;
|
|
ready: BattleReady | null;
|
|
format: string;
|
|
acceptCommand: string | null;
|
|
message: string;
|
|
acceptButton: string;
|
|
rejectButton: string;
|
|
roomid: RoomID;
|
|
constructor(from: ID, to: ID, ready: BattleReady | string, options: {
|
|
acceptCommand?: string, rejectCommand?: string, roomid?: RoomID,
|
|
message?: string, acceptButton?: string, rejectButton?: string,
|
|
} = {}) {
|
|
this.from = from;
|
|
this.to = to;
|
|
this.ready = typeof ready === 'string' ? null : ready;
|
|
this.format = typeof ready === 'string' ? ready : ready.formatid;
|
|
this.acceptCommand = options.acceptCommand || null;
|
|
this.message = options.message || '';
|
|
this.roomid = options.roomid || '';
|
|
this.acceptButton = options.acceptButton || '';
|
|
this.rejectButton = options.rejectButton || '';
|
|
}
|
|
destroy(accepted?: boolean) {}
|
|
}
|
|
/**
|
|
* As a regular battle challenge, acceptCommand will be null, but you
|
|
* can set acceptCommand to use this for custom requests wanting a
|
|
* team for something.
|
|
*/
|
|
export class BattleChallenge extends AbstractChallenge {
|
|
declare ready: BattleReady;
|
|
declare acceptCommand: string | null;
|
|
}
|
|
export class GameChallenge extends AbstractChallenge {
|
|
declare ready: null;
|
|
declare acceptCommand: string;
|
|
}
|
|
/**
|
|
* Invites for `/importinputlog` (`ready: null`) or 4-player battles
|
|
* (`ready: BattleReady`)
|
|
*/
|
|
export class BattleInvite extends AbstractChallenge {
|
|
declare acceptCommand: string;
|
|
override destroy(accepted?: boolean) {
|
|
if (accepted) return;
|
|
|
|
const room = Rooms.get(this.roomid);
|
|
if (!room) return; // room expired?
|
|
const battle = room.battle!;
|
|
let invitesFull = true;
|
|
for (const player of battle.players) {
|
|
if (!player.invite && !player.id) invitesFull = false;
|
|
if (player.invite === this.to) player.invite = '';
|
|
}
|
|
if (invitesFull) battle.sendInviteForm(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The defining difference between a BattleChallenge and a GameChallenge is
|
|
* that a BattleChallenge has a Ready (and is for a RoomBattle format) and
|
|
* a GameChallenge doesn't (and is for a RoomGame).
|
|
*
|
|
* But remember that both can have a custom acceptCommand.
|
|
*/
|
|
export type Challenge = BattleChallenge | GameChallenge;
|
|
|
|
/**
|
|
* Lists outgoing and incoming challenges for each user ID.
|
|
*/
|
|
export class Challenges extends Map<ID, Challenge[]> {
|
|
getOrCreate(userid: ID): Challenge[] {
|
|
let challenges = this.get(userid);
|
|
if (challenges) return challenges;
|
|
challenges = [];
|
|
this.set(userid, challenges);
|
|
return challenges;
|
|
}
|
|
/** Throws Chat.ErrorMessage if a challenge between these users is already in the table */
|
|
add(challenge: Challenge): true {
|
|
const oldChallenge = this.search(challenge.to, challenge.from);
|
|
if (oldChallenge) {
|
|
throw new Chat.ErrorMessage(`There is already a challenge (${challenge.format}) between ${challenge.to} and ${challenge.from}!`);
|
|
}
|
|
const to = this.getOrCreate(challenge.to);
|
|
const from = this.getOrCreate(challenge.from);
|
|
to.push(challenge);
|
|
from.push(challenge);
|
|
this.update(challenge.to, challenge.from);
|
|
return true;
|
|
}
|
|
/** Returns false if the challenge isn't in the table */
|
|
remove(challenge: Challenge, accepted?: boolean): boolean {
|
|
const to = this.getOrCreate(challenge.to);
|
|
const from = this.getOrCreate(challenge.from);
|
|
|
|
const toIndex = to.indexOf(challenge);
|
|
let success = false;
|
|
if (toIndex >= 0) {
|
|
to.splice(toIndex, 1);
|
|
if (!to.length) this.delete(challenge.to);
|
|
success = true;
|
|
}
|
|
|
|
const fromIndex = from.indexOf(challenge);
|
|
if (fromIndex >= 0) {
|
|
from.splice(fromIndex, 1);
|
|
if (!from.length) this.delete(challenge.from);
|
|
}
|
|
if (success) {
|
|
this.update(challenge.to, challenge.from);
|
|
challenge.destroy(accepted);
|
|
}
|
|
return success;
|
|
}
|
|
search(userid1: ID, userid2: ID): Challenge | null {
|
|
const challenges = this.get(userid1);
|
|
if (!challenges) return null;
|
|
for (const challenge of challenges) {
|
|
if (
|
|
(challenge.to === userid1 && challenge.from === userid2) ||
|
|
(challenge.to === userid2 && challenge.from === userid1)
|
|
) {
|
|
return challenge;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
searchByRoom(userid: ID, roomid: RoomID) {
|
|
const challenges = this.get(userid);
|
|
if (!challenges) return null;
|
|
for (const challenge of challenges) {
|
|
if (challenge.roomid === roomid) return challenge;
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Try to accept a custom challenge, throwing `Chat.ErrorMessage` on failure,
|
|
* and returning the user the challenge was from on a success.
|
|
*/
|
|
resolveAcceptCommand(context: Chat.CommandContext) {
|
|
const targetid = context.target as ID;
|
|
const chall = this.search(context.user.id, targetid);
|
|
if (!chall || chall.to !== context.user.id || chall.acceptCommand !== context.message) {
|
|
throw new Chat.ErrorMessage(`Challenge not found. You are using the wrong command. Challenges should be accepted with /accept`);
|
|
}
|
|
return chall;
|
|
}
|
|
accept(context: Chat.CommandContext) {
|
|
const chall = this.resolveAcceptCommand(context);
|
|
this.remove(chall, true);
|
|
const fromUser = Users.get(chall.from);
|
|
if (!fromUser) throw new Chat.ErrorMessage(`User "${chall.from}" is not available right now.`);
|
|
return fromUser;
|
|
}
|
|
clearFor(userid: ID, reason?: string): number {
|
|
const user = Users.get(userid);
|
|
const userIdentity = user ? user.getIdentity() : ` ${userid}`;
|
|
const challenges = this.get(userid);
|
|
if (!challenges) return 0;
|
|
for (const challenge of challenges) {
|
|
const otherid = challenge.to === userid ? challenge.from : challenge.to;
|
|
const otherUser = Users.get(otherid);
|
|
const otherIdentity = otherUser ? otherUser.getIdentity() : ` ${otherid}`;
|
|
|
|
const otherChallenges = this.get(otherid)!;
|
|
const otherIndex = otherChallenges.indexOf(challenge);
|
|
if (otherIndex >= 0) otherChallenges.splice(otherIndex, 1);
|
|
if (otherChallenges.length === 0) this.delete(otherid);
|
|
|
|
if (!user && !otherUser) continue;
|
|
const header = `|pm|${userIdentity}|${otherIdentity}|`;
|
|
let message = `${header}/challenge`;
|
|
if (reason) message = `${header}/text Challenge cancelled because ${reason}.\n${message}`;
|
|
user?.send(message);
|
|
otherUser?.send(message);
|
|
}
|
|
this.delete(userid);
|
|
return challenges.length;
|
|
}
|
|
getUpdate(challenge: Challenge | null) {
|
|
if (!challenge) return `/challenge`;
|
|
const teambuilderFormat = challenge.ready ? challenge.ready.formatid : '';
|
|
return `/challenge ${challenge.format}|${teambuilderFormat}|${challenge.message}|${challenge.acceptButton}|${challenge.rejectButton}`;
|
|
}
|
|
update(userid1: ID, userid2: ID) {
|
|
const challenge = this.search(userid1, userid2);
|
|
userid1 = challenge ? challenge.from : userid1;
|
|
userid2 = challenge ? challenge.to : userid2;
|
|
this.send(userid1, userid2, this.getUpdate(challenge));
|
|
}
|
|
send(userid1: ID, userid2: ID, message: string) {
|
|
const user1 = Users.get(userid1);
|
|
const user2 = Users.get(userid2);
|
|
const user1Identity = user1 ? user1.getIdentity() : ` ${userid1}`;
|
|
const user2Identity = user2 ? user2.getIdentity() : ` ${userid2}`;
|
|
const fullMessage = `|pm|${user1Identity}|${user2Identity}|${message}`;
|
|
user1?.send(fullMessage);
|
|
user2?.send(fullMessage);
|
|
}
|
|
updateFor(connection: Connection | User) {
|
|
const user = connection.user;
|
|
const challenges = this.get(user.id);
|
|
if (!challenges) return;
|
|
|
|
const userIdentity = user.getIdentity();
|
|
let messages = '';
|
|
for (const challenge of challenges) {
|
|
let fromIdentity, toIdentity;
|
|
if (challenge.from === user.id) {
|
|
fromIdentity = userIdentity;
|
|
const toUser = Users.get(challenge.to);
|
|
toIdentity = toUser ? toUser.getIdentity() : ` ${challenge.to}`;
|
|
} else {
|
|
const fromUser = Users.get(challenge.from);
|
|
fromIdentity = fromUser ? fromUser.getIdentity() : ` ${challenge.from}`;
|
|
toIdentity = userIdentity;
|
|
}
|
|
messages += `|pm|${fromIdentity}|${toIdentity}|${this.getUpdate(challenge)}\n`;
|
|
}
|
|
connection.send(messages);
|
|
}
|
|
}
|
|
|
|
export const challenges = new Challenges();
|