diff --git a/client/SkribblClient.js b/client/SkribblClient.js index 6b9e8f44d7aa3129bd9c2b7d7d9111389ee65718..31644ea5030384ef7efa8f129f0ccd17e5b9b87a 100644 --- a/client/SkribblClient.js +++ b/client/SkribblClient.js @@ -1,19 +1,34 @@ import DataChannel from "./networking/DataChannel.js"; import Signaler from "./networking/Signaler.js"; +/** + * 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} idOrDataChannel */ constructor(idOrDataChannel){ + /** @type {string[]} */ + this._players = []; + /** @type {((players:string[])=>void)[]} */ + this._onPlayersListChangeCallbacks = []; this._readyPromise = (async()=>{ this._dataChannel = idOrDataChannel instanceof DataChannel?idOrDataChannel:await Signaler.join(idOrDataChannel); await this._dataChannel.waitUntilReady(); - this._dataChannel.send("Hi there :3"); this._dataChannel.onMessage(message=>{ console.log(`message from server: "${message}"`); - }) + if (message.startsWith("players add ")){ + let name = message.split(" ").slice(2).join(" "); + this._players.push(name); + this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players)); + }else if(message.startsWith("players list ")){ + let json = message.split(" ").slice(2).join(" "); + this._players = JSON.parse(json); + this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players)); + } + }); })(); } @@ -23,4 +38,25 @@ export default class SkribblClient { async waitUntilReady(){ return this._readyPromise; } + + /** + * Joins the game with the given name. + * @param {string} name + */ + async join(name){ + this._dataChannel.send(`join ${name}`); + let message = await this._dataChannel.next(); + if (message!=="yup"){ + throw new Error(`Failed to join! Server response: "${message}"`); + } + } + + /** + * 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); + } } \ No newline at end of file diff --git a/client/SkribblContainer.js b/client/SkribblContainer.js index 4675aafa9ecef69a676029a0e7926aac0e3148c4..defb93a7f4b92d620b2b4c2b7192264189c8b448 100644 --- a/client/SkribblContainer.js +++ b/client/SkribblContainer.js @@ -1,8 +1,16 @@ +import SkribblClient from "./SkribblClient.js"; +import SkribblLobby from "./SkribblLobby.js"; +import SkribblMenu from "./SkribblMenu.js"; + /** * Custom element `<skribbl-container>` that functions as a root element for the game, containing and managing both the game and all its menus, taking care of high-level state managment. */ export default class SkribblContainer extends HTMLElement { - constructor(){ + /** + * + * @param {SkribblClient} client + */ + constructor(client){ super(); this.attachShadow({mode:"open"}); this.shadowRoot.innerHTML = ` @@ -14,11 +22,26 @@ export default class SkribblContainer extends HTMLElement { right: 0; bottom: 0; left: 0; - --background-color: #efefefef; + --background-color: #bfbfefef; background-image: linear-gradient(var(--background-color),var(--background-color)), url(./res/background.png); } + skribbl-menu, skribbl-lobby { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%,-50%); + } </style> `; + (async()=>{ + let menu = new SkribblMenu(); + this.shadowRoot.appendChild(menu); + let {name} = await menu.awaitProceed(); + await client.join(name); + menu.remove(); + let lobby = new SkribblLobby(client); + this.shadowRoot.appendChild(lobby); + })(); } } customElements.define("skribbl-container",SkribblContainer); \ No newline at end of file diff --git a/client/SkribblLobby.js b/client/SkribblLobby.js new file mode 100644 index 0000000000000000000000000000000000000000..f63d5d1dfbc6b8491707bbc76a2f44a3f1a7e611 --- /dev/null +++ b/client/SkribblLobby.js @@ -0,0 +1,30 @@ +import SkribblClient from "./SkribblClient.js"; + +/** + * Custom element `<skribbl-lobby>` for the lobby of a game. + */ +export default class SkribblLobby extends HTMLElement { + /** + * @param {SkribblClient} client + */ + constructor(client){ + super(); + this.attachShadow({mode:"open"}); + this.shadowRoot.innerHTML = ` + <style> + :host { + display: block; + background: #ffffff; + border-radius: 10px; + padding: 15px; + } + </style> + Players: <span></span> + `; + this._jens = this.shadowRoot.querySelector("span"); + client.onPlayersListChange(players=>{ + this._jens.innerText = players.join(", "); + }); + } +} +customElements.define("skribbl-lobby",SkribblLobby); \ No newline at end of file diff --git a/client/SkribblMenu.js b/client/SkribblMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..200b10cfb3c2e65583eb83ad63935b95d07a1d05 --- /dev/null +++ b/client/SkribblMenu.js @@ -0,0 +1,37 @@ +/** + * Custom element `<skribbl-menu>` for the name selection menu that appears when first entering a game. + */ +export default class SkribblMenu extends HTMLElement { + constructor(){ + super(); + this.attachShadow({mode:"open"}); + this.shadowRoot.innerHTML = ` + <style> + :host { + display: block; + background: #ffffff; + border-radius: 10px; + padding: 15px; + } + </style> + <input type="text"> + <button>Play</button> + `; + this._nameInput = this.shadowRoot.querySelector("input"); + this._playButton = this.shadowRoot.querySelector("button"); + /** @type {Promise<{name:string}>} */ + this._proceedPromise = new Promise(resolve=>{ + this._playButton.addEventListener("click",e=>{ + resolve({name:this._nameInput.value}); + }); + }); + } + + /** + * Waits until the user clicks play, then returns the name and settings he has chosen. + */ + async awaitProceed(){ + return await this._proceedPromise; + } +} +customElements.define("skribbl-menu",SkribblMenu); \ No newline at end of file diff --git a/client/SkribblServer.js b/client/SkribblServer.js index 34f20eba8d6e4339bb3412aa29901ffbe409b293..54023bcf19b8c4fa87b9e0a9909e97f293571be3 100644 --- a/client/SkribblServer.js +++ b/client/SkribblServer.js @@ -10,9 +10,14 @@ export default class SkribblServer { */ constructor(){ this._readyPromise = (async()=>{ + /** @type {{name:string,dataChannel:DataChannel}[]} */ + this._clients = []; this._id = await Signaler.host(dataChannel=>{ this.connect(dataChannel); }); + let [endpointA,endpointB] = DataChannel.createPair(); + this._dataChannel = endpointA; + this.connect(endpointB); })(); } @@ -23,6 +28,14 @@ export default class SkribblServer { return this._readyPromise; } + /** + * A dataChannel talking to this server like any other client. + * @readonly + */ + get dataChannel(){ + return this._dataChannel; + } + /** * The ID others can use to connect to this server. * @readonly @@ -44,9 +57,26 @@ export default class SkribblServer { * @param {DataChannel} dataChannel */ connect(dataChannel){ - dataChannel.onMessage(message=>{ - console.log(`message from client: "${message}"`); - dataChannel.send("Got your message :D"); - }); + (async()=>{ + let message = await dataChannel.next(); + console.log("message: ",message); + if (message.startsWith("join ")){ + let name = message.split(" ").slice(1).join(" "); + if (name.length<30&&name.length>=1){ + dataChannel.send("yup"); + this._clients.forEach(({dataChannel})=>{ + dataChannel.send("players add "+name); + }); + this._clients.push({name,dataChannel}); + dataChannel.send("players list "+JSON.stringify(this._clients.map(({name})=>name))) + }else{ + dataChannel.send("nope"); + dataChannel.close(); + } + }else{ + dataChannel.send("nope"); + dataChannel.close(); + } + })(); } } \ No newline at end of file diff --git a/client/networking/DataChannel.js b/client/networking/DataChannel.js index 3a7e6a323fa862de8ff5161596c00dff21ace6fd..e705891cd446b43bbfaf537e39d55521a5df71a5 100644 --- a/client/networking/DataChannel.js +++ b/client/networking/DataChannel.js @@ -114,6 +114,30 @@ export default class DataChannel { return {send,onMessage}; }); } + + /** + * Creates a pair of DataChannel endpoints talking directly to each other. + * + * Whenever one of those sends a message, all onMessage callbacks registered on the other one get queued as a microtask. + * @returns {[DataChannel,DataChannel]} + */ + static createPair(){ + /** @type {MessageCallback[]} */ + let onMessageCallbacksA = []; + /** @type {MessageCallback[]} */ + let onMessageCallbacksB = []; + let endpointA = new DataChannel(()=>Promise.resolve({send:message=>{ + onMessageCallbacksB.forEach(callback=>queueMicrotask(()=>callback(message))); + },onMessage:callback=>{ + onMessageCallbacksA.push(callback); + }})); + let endpointB = new DataChannel(()=>Promise.resolve({send:message=>{ + onMessageCallbacksA.forEach(callback=>queueMicrotask(()=>callback(message))); + },onMessage:callback=>{ + onMessageCallbacksB.push(callback); + }})); + return [endpointA,endpointB]; + } } /** * Class representing a connection to a server via a WebSocket. diff --git a/client/script.js b/client/script.js index af81585cfe7c6b6281fa7d83388e218eb233d3f2..e4f963031d1cc2b59b18265d344e57438d922377 100644 --- a/client/script.js +++ b/client/script.js @@ -3,10 +3,12 @@ import SkribblContainer from "./SkribblContainer.js"; import SkribblServer from "./SkribblServer.js"; document.addEventListener("DOMContentLoaded",async()=>{ + /** @type {SkribblClient} */ + let client; if (document.location.hash){ document.body.innerHTML = ""; const gameID = document.location.hash.substring(1); - const client = new SkribblClient(gameID); + client = new SkribblClient(gameID); }else{ /** @type {HTMLButtonElement} *///@ts-ignore const button = document.getElementById("button"); @@ -17,8 +19,9 @@ document.addEventListener("DOMContentLoaded",async()=>{ const server = new SkribblServer(); await server.waitUntilReady(); console.log(server.url); + client = new SkribblClient(server.dataChannel); } - const game = new SkribblContainer(); + const game = new SkribblContainer(client); document.body.innerHTML = ""; document.body.appendChild(game); }); \ No newline at end of file