Skip to content
Snippets Groups Projects
SkribblClient.js 3.96 KiB
Newer Older
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 {((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);
	}