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._rounds = 3; this._drawTime = 180; /** @type {((settings:{rounds:number,drawTime:number})=>void)[]} */ this._onSettingsChangeCallbacks = []; this._readyPromise = (async()=>{ /** @type {DataChannel<MessageToServer,MessageToClient>} */ this._dataChannel = idOrDataChannel instanceof DataChannel?idOrDataChannel: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"){ if (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.slice().forEach(callback=>callback(this._host)); } if (playersChanged){ this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players)); } }else if(message.action=="settingsUpdate"){ this._rounds = message.rounds; this._drawTime = message.drawTime; this._onSettingsChangeCallbacks.slice().forEach(callback=>callback(this.settings)); } } }); })(); } /** * 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); } } /** * The settings of the current game. * * This property will always refer to the same object - when assigning a new one, it's relevant properties will simply be copied to the old one. * Changes to those properties will be send to the server, and be reflected in their values only once the server has confirmed them. * @readonly */ get settings(){ if (!this._settings){ let _this = this; this._settings = { get rounds(){ return _this._rounds; }, set rounds(rounds){ _this._dataChannel.send({action:"changeSettings",rounds,drawTime:_this._drawTime}); }, get drawTime(){ return _this._drawTime; }, set drawTime(drawTime){ _this._dataChannel.send({action:"changeSettings",rounds:_this._rounds,drawTime}); } }; } return this._settings; } set settings(settings){ this.settings.rounds = settings.rounds; this.settings.drawTime = settings.drawTime; } /** * Registers a callback whenever the settings of the game change, and once when it is registered. * @param {(settings:{rounds:number,drawTime:number})=>void} callback */ onSettingsChange(callback){ this._onSettingsChangeCallbacks.push(callback); callback(this.settings); } /** * Removes a callback registered using `onSettingsChange(callback)`. * @param {(settings:{rounds:number,drawTime:number})=>void} callback */ removeOnSettingsChangeCallback(callback){ let index = this._onSettingsChangeCallbacks.indexOf(callback); if (index!==-1){ this._onSettingsChangeCallbacks.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); } }