UNPKG

@haelp/teto

Version:

A typescript-based controllable TETR.IO client.

427 lines (370 loc) 11.1 kB
import type { Events, Game as GameTypes, Room as RoomTypes, Utils } from "../../types"; import { Client } from "../client"; import { Game } from "../game"; import { roomConfigPresets } from "./presets"; import { ReplayManager } from "./replayManager"; export class Room { private client: Client; private listeners: Parameters<typeof this.client.on>[] = []; /** the ID of the room */ public id!: string; /** Whether or not the room is public */ public public!: boolean; /** The type of the room (public | private) */ public type!: RoomTypes.Type; /** Name of the room */ public name!: string; /** Safe Name of the room */ public name_safe!: string; /** UID of the host */ public owner!: string; /** UID of the room creator (this person can reclaim host) */ public creator!: string; /** The autostart state of the room */ public autostart!: RoomTypes.Autostart; /** The match config for the room */ public match!: RoomTypes.Match; /** The maxiumum number of players that can play in the room (override by moving as host) */ public userLimit!: number; /** The players in the room */ public players!: RoomTypes.Player[]; /** The room config */ public options!: GameTypes.Options; /** The current state of the room (ingame | lobby) */ public state!: RoomTypes.State; /** The time the last game started */ public gameStart: number | null = null; /** The replay data for the last played game */ public replay: ReplayManager | null = null; /** Room chat history */ public chats: Events.in.Room["room.chat"][] = []; /** @hideconstructor */ constructor(client: Client, data: Events.in.Room["room.update"]) { this.client = client; this.handleUpdate(data); this.init(); } private handleUpdate(data: Events.in.Room["room.update"]) { this.id = data.id; this.autostart = data.auto; [ "public", "type", "name", "name_safe", "owner", "creator", "state", "match", "players", "userLimit" ].forEach((key) => Object.assign(this, { [key]: data[key as keyof typeof data] }) ); this.options = data.options; } private listen<T extends keyof Events.in.all>( event: T, cb: (data: Events.in.all[T]) => void, once = false ) { this.listeners.push([event, cb] as any); if (once) { this.client.once(event, cb); } else { this.client.on(event, cb); } } private init() { const emitPlayers = () => this.client.emit("client.room.players", this.players); let abortTimeout: NodeJS.Timeout | null = null; this.listen("room.update.host", (data) => { this.owner = data; }); this.listen("room.update.bracket", (data) => { const idx = this.players.findIndex((p) => p._id === data.uid); if (idx >= 0) this.players[idx].bracket = data.bracket; emitPlayers(); }); this.listen("room.update.auto", (auto) => { this.autostart = auto; }); this.listen("room.update", this.handleUpdate.bind(this)); this.listen("room.player.add", (data) => { this.players.push(data); emitPlayers(); }); this.listen("room.player.remove", (data) => { this.players = this.players.filter((p) => p._id !== data); emitPlayers(); }); this.listen("game.ready", (data) => { try { this.client.game = new Game(this.client, data); } catch { return; // not in room, don't do anything } if (data.isNew) { this.gameStart = performance.now(); this.replay = new ReplayManager(data.players, this.players); this.client.emit("client.game.start", { multi: this.match.ft > 1 || this.match.wb > 1, ft: this.match.ft, wb: this.match.wb, players: data.players.map((p) => ({ id: p.userid, name: p.options.username, points: 0 as const })) }); } this.replay?.addRound(data.players); }); this.listen("game.replay", (event) => this.replay?.pipe(event)); this.listen("game.replay.end", async ({ gameid, data }) => { this.replay?.die({ gameid, data, game: this.client.game }); if (!this.client.game || this.client.game.gameid !== gameid) return; this.client.game.stop(); this.client.emit("client.game.over", { reason: "finish", data }); }); this.listen("game.advance", () => { this.replay?.endRound({ game: this.client.game }); if (this.client.game) { this.client.game = this.client.game.destroy(); this.client.emit("client.game.over", { reason: "end" }); } }); this.listen("game.score", (data) => { if (this.client.game) { this.client.game = this.client.game.destroy(); this.client.emit("client.game.over", { reason: "end" }); } this.client.emit("client.game.round.end", data.victor); }); this.listen("game.abort", () => { if (abortTimeout) return; abortTimeout = setTimeout(() => { abortTimeout = null; }, 50); this.client.emit("client.game.abort"); if (!this.client.game) return; this.client.game = this.client.game.destroy(); this.client.emit("client.game.over", { reason: "abort" }); }); this.listen("game.end", (data) => { this.client.emit("client.game.round.end", data.leaderboard[0].id); const maxWins = data.leaderboard.reduce( (max, item) => Math.max(max, item.wins), 0 ); this.client.emit("client.game.end", { duration: performance.now() - (this.gameStart ?? 0), players: data.leaderboard.map( (item) => ({ id: item.id, name: item.username, points: item.wins, won: item.wins === maxWins, raw: item }) satisfies Events.in.Client["client.game.end"]["players"][number] ) }); if (!this.client.game) return; this.client.game = this.client.game.destroy(); this.client.emit("client.game.over", { reason: "end" }); }); this.listen("client.game.end", () => this.replay?.end({ self: this.client.user.id }) ); // chat this.listen("room.chat", (item) => this.chats.push(item)); // get booted this.listen("room.kick", () => this.destroy()); this.listen("room.leave", () => this.destroy()); } /** Whether or not the client is the host */ get isHost() { return this.client.user.id === this.owner; } private destroy() { this.listeners.forEach((l) => this.client.off(l[0], l[1])); if (this.client.game) { this.client.game.destroy(); this.client.emit("client.game.over", { reason: "leave" }); } delete this.client.room; } /** * Leave the current room * @example * await client.room!.leave(); */ async leave() { await this.client.wrap("room.leave", undefined, "room.leave"); this.destroy(); } /** * Kick a user from the room for a specified duration (if host) * @param id - id of user to kick * @param duration - duration to kick the user, in seconds * @example * await client.room!.kick('646f633d276f42a80ba44304', 100); */ async kick(id: string, duration = 900) { return await this.client.wrap( "room.kick", { uid: id, duration }, "room.player.remove" ); } /** * Unban a user from the room * @example * client.room!.unban('halp'); */ unban(username: string) { return this.client.emit("room.unban", username); } /** * Send a public message to the room's chat. * The `pinned` parameter is the same as using the `/announce` command in TETR.IO * The `pinned` parameter being true will result in an error if the client is not host. * @example * await client.room!.chat('hi!'); * @example * await client.room!.chat('Important info:', true); */ async chat(message: string, pinned = false) { return await this.client.wrap( "room.chat.send", { content: message, pinned }, "room.chat" ); } /** * Clears the chat */ async clearChat() { return await this.client.wrap( "room.chat.clear", undefined, "room.chat.clear" ); } /** * Sets the room id (only works for supporter accounts) * @example * client.room!.setID('TEST'); */ async setID(id: string) { return await this.client.wrap( "room.setid", id.toUpperCase(), "room.update" ); } /** * Update the room's config, similar to using the /set command in tetr.io * await client.room!.update({ index: 'name', value: 'test room'}); * @returns */ async update<T extends Utils.DeepKeys<RoomTypes.SetConfig>>( ...options: { index: T; value: Utils.DeepKeyValue<RoomTypes.SetConfig, T>; }[] ) { return await this.client.wrap( "room.setconfig", options.map((opt) => typeof opt.value === "number" ? { index: opt.index, value: opt.value.toString() } : opt ), "room.update" ); } /** * Sets the room's preset * @example * await client.room!.usePreset('tetra league (season 1)'); */ async usePreset(preset: GameTypes.Preset) { return await this.update(...roomConfigPresets[preset]); } /** * Start the game */ async start() { return await this.client.wrap("room.start", undefined, "game.ready"); } /** * Abort the game */ async abort() { return await this.client.wrap("room.abort", undefined, "game.abort"); } /** * Give the host to someone else * @example * await client.room!.transferHost(await client.social.resolve('halp')); */ async transferHost(player: string) { return await this.client.wrap( "room.owner.transfer", player, "room.update.host" ); } /** Take host if you created the room */ async takeHost() { return await this.client.wrap( "room.owner.revoke", undefined, "room.update.host" ); } /** * Switch bracket * @example * await client.room!.switch('player'); */ async switch(bracket: "player" | "spectator") { if ( this.players.some( (p) => p._id === this.client.user.id && p.bracket === bracket ) ) return; return await this.client.wrap( "room.bracket.switch", bracket, "room.update.bracket" ); } /** * Move someone's bracket * @example * await client.room!.move('646f633d276f42a80ba44304', 'spectator'); */ async move(uid: string, bracket: "player" | "spectator") { const player = this.players.find((p) => p._id === uid); if (!player) { throw new Error(`Player with UID ${uid} not found in room.`); } if (player.bracket === bracket) return; return await this.client.wrap( "room.bracket.move", { uid, bracket }, "room.update.bracket" ); } } export { ReplayManager } from "./replayManager";