diff --git a/client/SkribblClient.js b/client/SkribblClient.js index 81c5e257032224c484d59493d0e3df5cc01eba9f..636ab8872a25df1c5719d0953d4681a4457d5a93 100644 --- a/client/SkribblClient.js +++ b/client/SkribblClient.js @@ -18,6 +18,8 @@ export default class SkribblClient { constructor(idOrDataChannel){ /** @type {string[]} */ this._players = []; + /** @type {((host:boolean)=>void)[]} */ + this._onHostChangeCallbacks = []; /** @type {((players:string[])=>void)[]} */ this._onPlayersListChangeCallbacks = []; this._readyPromise = (async()=>{ @@ -29,15 +31,19 @@ export default class SkribblClient { } await this._dataChannel.waitUntilReady(); this._dataChannel.onMessage(message=>{ - console.log(`message from server: "${message}"`); - if (message!=="yup"&&message!=="nope"&&message.action=="updatePlayerList"){ - let update = message.update; - if (update.type=="add"){ - this._players.push(update.player); - }else if(update.type=="set"){ - this._players = update.players; + 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)); } - this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players)); } }); })(); @@ -62,6 +68,62 @@ export default class SkribblClient { } } + /** + * 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 diff --git a/client/SkribblContainer.js b/client/SkribblContainer.js index e9d2bcf4cbdcf23d883130e2358dc31065650809..ac16d036bed29974f043f1a4b4007fedc48d799b 100644 --- a/client/SkribblContainer.js +++ b/client/SkribblContainer.js @@ -43,8 +43,12 @@ export default class SkribblContainer extends HTMLElement { let {name} = await menu.awaitProceed(); await client.join(name); menu.remove(); - let lobby = new SkribblLobby(client); - this.shadowRoot.appendChild(lobby); + if (!client.hasGameStarted){ + let lobby = new SkribblLobby(client); + this.shadowRoot.appendChild(lobby); + await client.waitUntilGameStarted(); + lobby.remove(); + } })(); } } diff --git a/client/SkribblLobby.js b/client/SkribblLobby.js index 8909668cb5ae00a29e9cc4f65c6ed3a26d2bef3f..d1e72b87a24e01d1cec13cb3a1a4b2a53c90838a 100644 --- a/client/SkribblLobby.js +++ b/client/SkribblLobby.js @@ -25,7 +25,7 @@ export default class SkribblLobby extends HTMLElement { } </style> `; - this._settings = new SkribblSettings(); + this._settings = new SkribblSettings(client); this._playerList = new SkribblPlayerList(client); this.shadowRoot.append(this._settings,this._playerList); } diff --git a/client/SkribblServer.js b/client/SkribblServer.js index f5194f4569e17114600d0c232df49b70d6a5d931..794e8a11cfe9ae82e2deab1352c79fa3b84366db 100644 --- a/client/SkribblServer.js +++ b/client/SkribblServer.js @@ -2,10 +2,12 @@ import DataChannel from "./networking/DataChannel.js"; import Signaler from "./networking/Signaler.js"; /** - * @typedef {{action:"join",name:string}} MessageToServer + * Format of the messages send to the server by the clients. + * @typedef {{action:"join",name:string}|{action:"startGame"}} MessageToServer */ /** - * @typedef {"yup"|"nope"|{action:"updatePlayerList",update:{type:"set",players:string[]}|{type:"add",player:string}}} MessageToClient + * Format of the messages send to the clients by the server. + * @typedef {"yup"|"nope"|{action:"stateUpdate",newState:{players:string[],host:boolean,hasGameStarted:boolean}}} MessageToClient */ /** * A local server. Handles all the important game logic, and communicates with clients via DataChannels. @@ -18,6 +20,7 @@ export default class SkribblServer { this._readyPromise = (async()=>{ /** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>}[]} */ this._clients = []; + this._hasGameStarted = false; this._id = await Signaler.host(dataChannel=>{ this.connect(DataChannel.from(dataChannel,JSON.stringify,JSON.parse)); }); @@ -71,11 +74,23 @@ export default class SkribblServer { let name = message.name; if (name.length<30&&name.length>=1){ dataChannel.send("yup"); - this._clients.forEach(({dataChannel})=>{ - dataChannel.send({action:"updatePlayerList",update:{type:"add",player:name}}); - }); this._clients.push({name,dataChannel}); - dataChannel.send({action:"updatePlayerList",update:{type:"set",players:this._clients.map(({name})=>name)}}); + let players = this._clients.map(({name})=>name); + this._clients.forEach(({dataChannel},index)=>{ + dataChannel.send({action:"stateUpdate",newState:{players,host:index==0,hasGameStarted:this._hasGameStarted}}); + }); + dataChannel.onMessage(message=>{ + let isHost = this._clients[0].dataChannel = dataChannel; + if (message.action=="startGame"){ + if (isHost&&!this._hasGameStarted){ + this._hasGameStarted = true; + let players = this._clients.map(({name})=>name); + this._clients.forEach(({dataChannel},index)=>{ + dataChannel.send({action:"stateUpdate",newState:{players,host:index==0,hasGameStarted:this._hasGameStarted}}); + }); + } + } + }); }else{ dataChannel.send("nope"); dataChannel.close(); diff --git a/client/SkribblSettings.js b/client/SkribblSettings.js index 9553aba68ec435ac442f4956923c79d752034919..de707120268f76e38cb4278379377f2d03339423 100644 --- a/client/SkribblSettings.js +++ b/client/SkribblSettings.js @@ -1,8 +1,13 @@ +import SkribblClient from "./SkribblClient.js"; + /** * Custom element `<skribbl-settings>` for the settings that appear in the lobby of the game. */ export default class SkribblSettings extends HTMLElement { - constructor(){ + /** + * @param {SkribblClient} client + */ + constructor(client){ super(); this.attachShadow({mode:"open"}); this.shadowRoot.innerHTML = ` @@ -32,6 +37,10 @@ export default class SkribblSettings extends HTMLElement { border-radius: 4px; cursor: pointer; } + button:disabled { + cursor: default; + background: #dfa580; + } </style> <h2>Lobby</h2> <b>Rounds:</b> @@ -53,14 +62,34 @@ export default class SkribblSettings extends HTMLElement { <option value="60">60s</option> <option value="90">90s</option> <option value="120">120s</option> - <option value="180">180s</option> + <option value="180" selected="">180s</option> + <option value="240">240s</option> </select><br> <div style="text-align:center"> <button>Start Game</button> </div> `; - this._roundsInput = this.querySelectorAll("select")[0]; - this._drawTimeInput = this.querySelectorAll("select")[1]; + this._client = client; + this._roundsInput = this.shadowRoot.querySelectorAll("select")[0]; + this._drawTimeInput = this.shadowRoot.querySelectorAll("select")[1]; + this._startButton = this.shadowRoot.querySelector("button"); + /** @param {boolean} host whether this client currently has host permissions */ + this._onHostChangeCallback = host=>{ + this._roundsInput.disabled = !host; + this._drawTimeInput.disabled = !host; + this._startButton.disabled = !host; + } + this._startButton.addEventListener("click",e=>{ + this._client.startGame(); + }); + } + + connectedCallback(){ + this._client.onHostChange(this._onHostChangeCallback); + } + + disconnectedCallback(){ + this._client.removeOnHostChangeCallback(this._onHostChangeCallback); } get settings(){