Newer
Older
import DataChannel from "./networking/DataChannel.js";
import Signaler from "./networking/Signaler.js";
import {CallbackHandler,Value,ReadOnlyValue,deepFreeze} from "../util/Util.js";

Ben Eltschig
committed
/**
* @typedef {import("./SkribblServer.js").MessageToServer} MessageToServer
* @typedef {import("./SkribblServer.js").MessageToClient} MessageToClient
* @typedef {import("./SkribblServer.js").GameState} GameState
* @typedef {import("./SkribblServer.js").GuessResponse} GuessResponse
*/

Ben Eltschig
committed
/**
* @typedef {import("../util/Util.js").DeepReadOnly<T>} DeepReadOnly

Ben Eltschig
committed
* @template {unknown} T
*/

Ben Eltschig
committed
/**
* Client/endpoint that manages a connection to a SkribblServer
* @todo find a less redundant way to manage callbacks to avoid clutter

Ben Eltschig
committed
*/
export default class SkribblClient {
/**
* Constructs a new client for a given game ID or DataChannel.

Ben Eltschig
committed
* @param {string|DataChannel<MessageToServer,MessageToClient>} idOrDataChannel
*/
constructor(idOrDataChannel){

Ben Eltschig
committed
/** @type {Value<DeepReadOnly<{name:string,points?:number}[]>>} */
this._players = new Value([]);
this._isHost = new Value(false);
/** @type {Value<DeepReadOnly<GameState>>} */
this._state = new Value(null);
/** @type {CallbackHandler<[guessData:GuessResponse]>} */
this._onGuessCallbacks = new CallbackHandler();
this._settings = new Value(deepFreeze({rounds:3,drawTime:180}));
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"){

Ben Eltschig
committed
let state = deepFreeze(message.newState);
this._state.value = state;
this._hasGameStarted = state.hasGameStarted;
this._players.value = state.players;
this._isHost.value = state.host;
}else if(message.action=="settingsUpdate"){

Ben Eltschig
committed
this._settings.value = deepFreeze({rounds:message.rounds,drawTime:message.drawTime});
}else if(message.action=="guessedWord"){
let data = (({action,...data})=>data)(message);

Ben Eltschig
committed
this._onGuessCallbacks.callAll(data);

Ben Eltschig
committed
}

Ben Eltschig
committed
}
});
})();
}
/**
* Waits until the connection to the server is ready.
*/
async waitUntilReady(){
return this._readyPromise;
}

Ben Eltschig
committed
/**
* Joins the game with the given name.
* @param {string} name
*/
async join(name){

Ben Eltschig
committed
this._dataChannel.send({action:"join",name});

Ben Eltschig
committed
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;
}
}
}
}
/**
* If a game has already started, waits until it ends again.
*/
async waitUntilGameEnded(){
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.

Ben Eltschig
committed
* @readonly

Ben Eltschig
committed
get isHost(){
return this._isHost.readOnly;
/**
* The settings of the current game.
* @readonly
*/
get settings(){

Ben Eltschig
committed
return this._settings.readOnly;
/**

Ben Eltschig
committed
* Asks the server to change the given settings to the given values.
* @param {{rounds?:number,drawTime?:number}} settings
*/

Ben Eltschig
committed
setSettings({rounds=this._settings.value.rounds,drawTime=this._settings.value.drawTime}){
this._dataChannel.send({action:"changeSettings",rounds,drawTime});
}
/**

Ben Eltschig
committed
* A readonly version of the current state that can be monitored for changes.
* @readonly
*/

Ben Eltschig
committed
get state(){
return this._state.readOnly;
}
/**
* Sends the index of the choosen word to the server.
*
* Also temporary, should be replaced by something cleaner later on.
* @param {number} index
*/
chooseWord(index){
this._dataChannel.send({action:"chooseWord",word:index});
}
/**
* Sends a guess to the server and then immediately returns, without waiting for a response.
* @param {string} word
*/
sendGuess(word){
this._dataChannel.send({action:"guess",word});
}
/**

Ben Eltschig
committed
* Registers a callback to be called whenever someone guesses a word (not necessarily correctly - just whenever anyone writes anything in the chat, basically).
* @param {(guessData:GuessResponse)=>void} callback

Ben Eltschig
committed
* @param {object} [options]
* @param {Value<boolean>|ReadOnlyValue<boolean>} [options.onlyWhen] when specified, calls the callback only when this is value is true.

Ben Eltschig
committed
onGuess(callback,{onlyWhen=null}={}){
this._onGuessCallbacks.addCallback(callback,{onlyWhen});
}
/**
* All currently connected players.

Ben Eltschig
committed
* @readonly

Ben Eltschig
committed
return this._players.readOnly;

Ben Eltschig
committed
}