import DataChannel from "./networking/DataChannel.js"; import Signaler from "./networking/Signaler.js"; /** * @typedef {import("./SkribblServer.js").MessageToServer} MessageToServer */ /** * @typedef {import("./SkribblServer.js").MessageToClient} MessageToClient */ /** * Client/endpoint that manages a connection to a SkribblServer */ export default class SkribblClient { /** * Constructs a new client for a given game ID or DataChannel. * @param {string|DataChannel<MessageToServer,MessageToClient>} idOrDataChannel */ constructor(idOrDataChannel){ /** @type {string[]} */ this._players = []; /** @type {((host:boolean)=>void)[]} */ this._onHostChangeCallbacks = []; /** @type {((players:string[])=>void)[]} */ this._onPlayersListChangeCallbacks = []; this._readyPromise = (async()=>{ if (idOrDataChannel instanceof DataChannel){ this._dataChannel = idOrDataChannel; }else{ /** @type {DataChannel<MessageToServer,MessageToClient>} */ this._dataChannel = DataChannel.from(await Signaler.join(idOrDataChannel),JSON.stringify,JSON.parse); } await this._dataChannel.waitUntilReady(); this._dataChannel.onMessage(message=>{ console.log("message from server:",message); if (message!=="yup"&&message!=="nope"&&message.action=="stateUpdate"){ let hostChanged = this._host!==message.newState.host; let playersChanged = JSON.stringify(this._players)!==JSON.stringify(message.newState.players); this._hasGameStarted = message.newState.hasGameStarted; this._host = message.newState.host; this._players = message.newState.players.slice(); if (hostChanged){ this._onHostChangeCallbacks.forEach(callback=>callback(this._host)); } if (playersChanged){ this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players)); } } }); })(); } /** * Waits until the connection to the server is ready. */ async waitUntilReady(){ return this._readyPromise; } /** * Joins the game with the given name. * @param {string} name */ async join(name){ this._dataChannel.send({action:"join",name}); let message = await this._dataChannel.next(); if (message!=="yup"){ throw new Error(`Failed to join! Server response: "${message}"`); } } /** * Tells the server to start the game and then immediately returns, without waiting for a response. */ startGame(){ this._dataChannel.send({action:"startGame"}); } /** * Whether the game has started yet. */ get hasGameStarted(){ return this._hasGameStarted; } /** * If the game hasn't started yet, waits until the host of the game has clicked the start button in the lobby, otherwise immediately returns. */ async waitUntilGameStarted(){ if (!this._hasGameStarted){ while(true){ let message = await this._dataChannel.next(); if (message!=="yup"&&message!=="nope"&&message.action=="stateUpdate"&&message.newState.hasGameStarted){ break; } } } } /** * Whether this client is currently "host" in the sense that they have permissions to alter settings and start the game. * Doesn't actually always correlate with who is hosting the game. */ get host(){ return this._host; } /** * Registers a callback to be called whenever the host of the game changes, and once when it is registered. * @param {(host:boolean)=>void} callback */ onHostChange(callback){ this._onHostChangeCallbacks.push(callback); callback(this._host); } /** * Removes a callback registered using `onHostChange(callback)`. * @param {(host:boolean)=>void} callback */ removeOnHostChangeCallback(callback){ let index = this._onHostChangeCallbacks.indexOf(callback); if (index!==-1){ this._onHostChangeCallbacks.splice(index,1); } } /** * Registers a callback whenever the list of players in the game changes, and once when it is registered. * @param {(players:string[])=>void} callback */ onPlayersListChange(callback){ this._onPlayersListChangeCallbacks.push(callback); callback(this._players); } }