import DataChannel from "./networking/DataChannel.js"; import Signaler from "./networking/Signaler.js"; /** * Format of the messages send to the server by the clients. * @typedef {{action:"join",name:string}|{action:"startGame"}|{action:"changeSettings",rounds:number,drawTime:number}} MessageToServer */ /** * Format of the messages send to the clients by the server. * @typedef {"yup"|"nope"|{action:"stateUpdate",newState:GameState}|{action:"settingsUpdate",rounds:number,drawTime:number}} MessageToClient */ /** * Format in which the game state is send to the clients by the server. * @typedef {{hasGameStarted:false,host:boolean,players:{name:string}[]}|{hasGameStarted:true,round:number,rounds:number,host:boolean,players:{name:string,points:number}[]}} GameState */ /** * A local server. Handles all the important game logic, and communicates with clients via DataChannels. */ export default class SkribblServer { /** * Starts a new SkribblServer. */ constructor(){ this._readyPromise = (async()=>{ /** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>,points:number}[]} */ this._clients = []; this._hasGameStarted = false; this._round = 0; this._rounds = 3; this._drawTime = 180; this._id = await Signaler.host(dataChannel=>{ this.connect(DataChannel.from(dataChannel,JSON.stringify,JSON.parse)); }); /** @type {[DataChannel<MessageToServer,MessageToClient>,DataChannel<MessageToClient,MessageToServer>]} */ let [endpointA,endpointB] = DataChannel.createPair(); this._dataChannel = endpointA; this.connect(endpointB); })(); } /** * Waits until the server is ready. */ async waitUntilReady(){ 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 */ get id(){ return this._id; } /** * Returns the full url others can use to connect to this server. * @readonly */ get url(){ return document.location.host+document.location.pathname+"#"+this._id; } /** * Adds an incoming connection as a client. * @param {DataChannel<MessageToClient,MessageToServer>} dataChannel */ connect(dataChannel){ (async()=>{ let message = await dataChannel.next(); console.log("message: ",message); if (message.action==="join"){ let name = message.name; if (name.length<30&&name.length>=1){ dataChannel.send("yup"); this._clients.push({name,dataChannel,points:0}); this._sendStateUpdate(); if (!this._hasGameStarted){ dataChannel.send({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime}); } dataChannel.onMessage(message=>{ let isHost = this._clients[0].dataChannel = dataChannel; if (message.action=="startGame"){ if (isHost&&!this._hasGameStarted){ this._startGame(); } }else if(message.action=="changeSettings"){ if (isHost&&!this._hasGameStarted){ this._rounds = message.rounds; this._drawTime = message.drawTime; this._clients.forEach(({dataChannel})=>{ dataChannel.send({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime}); }); } } }); }else{ dataChannel.send("nope"); dataChannel.close(); } }else{ dataChannel.send("nope"); dataChannel.close(); } })(); } /** * Starts the game if it hasn't already started. */ _startGame(){ if (this._hasGameStarted){ console.warn("Tried to start a game that has already started."); }else{ this._hasGameStarted = true; (async()=>{ for (this._round=1;this._round<=this._rounds;this._round++){ this._sendStateUpdate(); await new Promise(resolve=>setTimeout(resolve,2000)); } this._hasGameStarted = false; this._sendStateUpdate(); })(); } } /** * Sends a state update to all connected clients (that is, clients that have already joined the game). */ _sendStateUpdate(){ if (this._hasGameStarted){ let players = this._clients.map(({name,points})=>({name,points})); this._clients.forEach(({dataChannel},index)=>{ dataChannel.send({action:"stateUpdate",newState:{players,host:index==0,hasGameStarted:true,rounds:this._rounds,round:this._round}}); }); }else{ let players = this._clients.map(({name})=>({name})); this._clients.forEach(({dataChannel},index)=>{ dataChannel.send({action:"stateUpdate",newState:{players,host:index==0,hasGameStarted:false}}); }); } } }