diff --git a/client/logic/SkribblClient.js b/client/logic/SkribblClient.js index 7faad7fe8896c2aa29aa4f1f43dab568eaa7b5cc..20ea0e8036fbedaeefafb9a681dc008f10418e36 100644 --- a/client/logic/SkribblClient.js +++ b/client/logic/SkribblClient.js @@ -10,6 +10,7 @@ import Util, {CallbackHandler,Value,ReadOnlyValue} from "../util/Util.js"; * @typedef {import("./SkribblServer.js").ClearOrder} ClearOrder * @typedef {import("./SkribblServer.js").DrawOrder} DrawOrder * @typedef {import("./SkribblServer.js").WordReveal} WordReveal + * @typedef {import("./SkribblServer.js").BackgroundOrder} BackgroundOrder */ /** * @typedef {import("../util/Util.js").DeepReadOnly<T>} DeepReadOnly @@ -32,7 +33,7 @@ export default class SkribblClient { this._state = new Value(null); /** @type {CallbackHandler<[guessData:GuessResponse]>} */ this._onGuessCallbacks = new CallbackHandler(); - /** @type {CallbackHandler<[order:ClearOrder|DrawOrder]>} */ + /** @type {CallbackHandler<[order:ClearOrder|BackgroundOrder|DrawOrder]>} */ this._onDrawCallbacks = new CallbackHandler(); /** @type {CallbackHandler<[message:WordReveal]>} */ this._onWordRevealCallbacks = new CallbackHandler(); @@ -55,7 +56,7 @@ export default class SkribblClient { }else if(message.action=="guessedWord"){ let data = (({action,...data})=>data)(message); this._onGuessCallbacks.callAll(data); - }else if(message.action=="clearCanvas"||message.action=="draw"||message.action=="erase"){ + }else if(message.action=="clearCanvas"||message.action=="changeBackgroundColor"||message.action=="draw"||message.action=="erase"){ this._onDrawCallbacks.callAll(message); }else if(message.action=="revealWord"){ this._onWordRevealCallbacks.callAll(message); @@ -197,7 +198,7 @@ export default class SkribblClient { /** * Sends the given drawing order to the server and the immediately returns, without waiting for a response. - * @param {ClearOrder|DrawOrder} order + * @param {ClearOrder|BackgroundOrder|DrawOrder} order */ draw(order){ this._dataChannel.send(order); @@ -205,7 +206,7 @@ export default class SkribblClient { /** * Registers a callback to be called whenever someone draws something on the canvas. - * @param {(order:ClearOrder|DrawOrder)=>void} callback + * @param {(order:ClearOrder|BackgroundOrder|DrawOrder)=>void} callback * @param {object} [options] * @param {Value<boolean>|ReadOnlyValue<boolean>} [options.onlyWhen] when specified, calls the callback only when this is value is true. */ diff --git a/client/logic/SkribblServer.js b/client/logic/SkribblServer.js index dac9882bf3307b38269d5cd1df43a7e89f3d15ab..63392c74c258436e5eeb0271ad41c2a567be1512 100644 --- a/client/logic/SkribblServer.js +++ b/client/logic/SkribblServer.js @@ -1,16 +1,16 @@ import { SkribblWord } from "../docs/CustomElements.js"; -import Util, {ConditionFulfilledPromise} from "../util/Util.js"; +import Util, { ConditionFulfilledPromise } from "../util/Util.js"; import DataChannel from "./networking/DataChannel.js"; import Signaler from "./networking/Signaler.js"; import SkribblWords from "./SkribblWords.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}|{action:"chooseWord",word:number}|{action:"guess",word:string}|ClearOrder|DrawOrder} MessageToServer + * @typedef {{action:"join",name:string}|{action:"startGame"}|{action:"changeSettings",rounds:number,drawTime:number}|{action:"chooseWord",word:number}|{action:"guess",word:string}|ClearOrder|BackgroundOrder|DrawOrder} 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}|({action:"guessedWord"}&GuessResponse)|ClearOrder|DrawOrder|WordReveal} MessageToClient + * @typedef {"yup"|"nope"|{action:"stateUpdate",newState:GameState}|{action:"settingsUpdate",rounds:number,drawTime:number}|({action:"guessedWord"}&GuessResponse)|ClearOrder|DrawOrder|BackgroundOrder|WordReveal} MessageToClient */ /** * Format in which the game state is send to the clients by the server. @@ -52,6 +52,12 @@ import SkribblWords from "./SkribblWords.js"; * @property {number} radius radius in pixels * @property {{x:number,y:number}[]} points array of points between which lines should be drawn */ +/** + * Format to change the Background-Color of the Canvas + * @typedef {object} BackgroundOrder + * @property {"changeBackgroundColor"} action + * @property {string} color + */ /** * @typedef {object} WordReveal * @property {"revealWord"} action @@ -64,341 +70,343 @@ import SkribblWords from "./SkribblWords.js"; * 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,guessedWord:boolean,guessedIndex:number}[]} */ - this._clients = []; - this._hasGameStarted = false; - /** current round, starts counting at 1, only 0 when the game hasn't started yet */ - this._round = 0; - this._rounds = 3; - this._drawTime = 180; - /** index of the currently drawing player in the player list */ - this._drawingPlayer = 0; - /** - * word that is currently being drawn, or list of words a word is currently getting choosen from - * @type {string|string[]} - */ - this._word = null; - 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); - })(); - } + /** + * Starts a new SkribblServer. + */ + constructor() { + this._readyPromise = (async() => { + /** @type {{name:string,dataChannel:DataChannel<MessageToClient,MessageToServer>,points:number,guessedWord:boolean,guessedIndex:number}[]} */ + this._clients = []; + this._hasGameStarted = false; + /** current round, starts counting at 1, only 0 when the game hasn't started yet */ + this._round = 0; + this._rounds = 3; + this._drawTime = 180; + /** index of the currently drawing player in the player list */ + this._drawingPlayer = 0; + /** + * word that is currently being drawn, or list of words a word is currently getting choosen from + * @type {string|string[]} + */ + this._word = null; + 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, guessedWord: false, guessedIndex: 0 }); + this._sendStateUpdate(); + if (!this._hasGameStarted) { + dataChannel.send({ action: "settingsUpdate", rounds: this._rounds, drawTime: this._drawTime }); + } + dataChannel.onMessage(message => { + let playerIndex = this._clients.map(({ dataChannel }) => dataChannel).indexOf(dataChannel); + let isHost = (playerIndex == 0); + 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._sendToAll({ action: "settingsUpdate", rounds: this._rounds, drawTime: this._drawTime }); + } + } else if (message.action == "guess") { + this._handleGuess(playerIndex, message.word); + } else if (message.action == "clearCanvas" || message.action == "changeBackgroundColor" || message.action == "draw" || message.action == "erase") { + // sends the drawing order to all connected clients iff the player is currently drawing and has already chosen a word + if (this._hasGameStarted && this._drawingPlayer == playerIndex && typeof this._word == "string") { + this._sendToAll(message); + } + } + }); + } else { + dataChannel.send("nope"); + dataChannel.close(); + } + } else { + dataChannel.send("nope"); + dataChannel.close(); + } + })(); + } - /** - * Waits until the server is ready. - */ - async waitUntilReady(){ - return this._readyPromise; - } + /** + * Sends a message to all currently connected clients. + * @param {MessageToClient} message + */ + _sendToAll(message) { + this._clients.forEach(({ dataChannel }) => { + dataChannel.send(message); + }) + } - /** - * A dataChannel talking to this server like any other client. - * @readonly - */ - get dataChannel(){ - return this._dataChannel; - } + /** + * 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; + this._clients.forEach(client => { + client.points = 0; + client.guessedWord = false; + client.guessedIndex = 0; + }); + (async() => { + for (this._round = 1; this._round <= this._rounds; this._round++) { + for (this._drawingPlayer = 0; this._drawingPlayer < this._clients.length; this._drawingPlayer++) { + let words = await Promise.all([SkribblWords.get(), SkribblWords.get(), SkribblWords.get()]); + this._word = words.map(({ word }) => word); + this._sendStateUpdate(); + /** @type {number} */ + let wordIndex = await new Promise(async resolve => { + let cancelled = false; + setTimeout(() => { + cancelled = true; + resolve(Math.floor(Math.random() * 3)); + }, 10000); + let dataChannel = this._clients[this._drawingPlayer].dataChannel; + while (!cancelled) { + let message = await dataChannel.next(); + if (message.action == "chooseWord") { + resolve(message.word); + } + } + }); + this._word = words[wordIndex].word; + this._sendToAll({ action: "clearCanvas" }); + this._sendStateUpdate(); + this._allGuessedPromise = new ConditionFulfilledPromise(() => { + return this._clients.every((client, index) => client.guessedWord || index == this._drawingPlayer); + }); + let timeOverPromise = Util.wait(this._drawTime); + await Promise.race([this._allGuessedPromise, timeOverPromise]); + let points = this._awardPoints(); + this._sendStateUpdate(); + this._sendToAll({ action: "revealWord", word: this._word, description: words[wordIndex].description, macros: await SkribblWords.macros, points }); + await Util.wait(8); + } + } + this._hasGameStarted = false; + this._sendStateUpdate(); + })(); + } + } - /** - * The ID others can use to connect to this server. - * @readonly - */ - get id(){ - return this._id; - } + /** + * Sends a state update to all connected clients (that is, clients that have already joined the game). + */ + _sendStateUpdate() { + console.log("sending state update! players:", this._clients); + if (this._hasGameStarted) { + let players = this._clients.map(({ name, points, guessedWord }) => ({ name, points, guessedWord })); + this._clients.forEach(({ dataChannel }, index) => { + let host = index == 0; + let word = this._drawingPlayer == index ? this._word : (typeof this._word == "string" ? this._word.replace(/[^ -]/g, "_") : null); + /** @type {ActiveGameState} */ + let state = { players, playerIndex: index, host, hasGameStarted: true, rounds: this._rounds, round: this._round, drawingPlayer: this._drawingPlayer, word }; + dataChannel.send({ action: "stateUpdate", newState: state }); + }); + } else { + let players = this._clients.map(({ name }) => ({ name })); + this._clients.forEach(({ dataChannel }, index) => { + dataChannel.send({ action: "stateUpdate", newState: { players, playerIndex: index, host: index == 0, hasGameStarted: false } }); + }); + } + } - /** - * Returns the full url others can use to connect to this server. - * @readonly - */ - get url(){ - return document.location.host+document.location.pathname+"#"+this._id; - } + /** + * Handles a guess send from a given dataChannel. + * @param {number} playerIndex + * @param {string} word + */ + _handleGuess(playerIndex, word) { + let player = this._clients[playerIndex]; + if (this._hasGameStarted) { + if (player.guessedWord || playerIndex == this._drawingPlayer) { + // TODO let people who already know the word send ghost messages to others who already know it too + } else if (typeof this._word == "string" && SkribblServer._isCorrect(word, this._word)) { + player.guessedWord = true; + player.guessedIndex = Math.max(...this._clients.map(client => client.guessedIndex)) + 1; + this._clients.forEach((client, index) => { + if (index == playerIndex) { + client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: true, word }); + } else { + client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: true }); + } + }); + this._allGuessedPromise.checkCondition(); + } else { + this._clients.forEach((client, index) => { + let close = typeof this._word == "string" ? SkribblServer._isClose(word, this._word) : false; + if (index == playerIndex) { + client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: false, word, close }); + } else { + client.dataChannel.send({ action: "guessedWord", player: playerIndex, correct: false, word }); + } + }); + } + } + } - /** - * 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,guessedWord:false,guessedIndex:0}); - this._sendStateUpdate(); - if (!this._hasGameStarted){ - dataChannel.send({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime}); - } - dataChannel.onMessage(message=>{ - let playerIndex = this._clients.map(({dataChannel})=>dataChannel).indexOf(dataChannel); - let isHost = (playerIndex==0); - 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._sendToAll({action:"settingsUpdate",rounds:this._rounds,drawTime:this._drawTime}); - } - }else if (message.action=="guess"){ - this._handleGuess(playerIndex,message.word); - }else if (message.action=="clearCanvas"||message.action=="draw"||message.action=="erase"){ - // sends the drawing order to all connected clients iff the player is currently drawing and has already chosen a word - if (this._hasGameStarted&&this._drawingPlayer==playerIndex&&typeof this._word=="string"){ - this._sendToAll(message); - } - } - }); - }else{ - dataChannel.send("nope"); - dataChannel.close(); - } - }else{ - dataChannel.send("nope"); - dataChannel.close(); - } - })(); - } + /** + * Awards points to each player based on if and when they guessed the word, resets the corresponding properties and then returns the list of points each player got. + * @todo find better formula for the number of points each player should get + */ + _awardPoints() { + let guessingPlayers = this._clients.length - 1; + let successfullPlayers = Math.max(...this._clients.map(client => client.guessedIndex)); + let points = this._clients.map((client, index) => { + let points; + if (this._drawingPlayer == index) { + points = successfullPlayers * 50; + } else if (client.guessedIndex != 0) { + points = ((guessingPlayers - client.guessedIndex + 1) ** 2) / guessingPlayers * 100; + } else { + points = 0; + } + points = Math.round(points / 5) * 5; + client.points += points; + client.guessedWord = false; + client.guessedIndex = 0; + return points; + }); + return points; + } - /** - * Sends a message to all currently connected clients. - * @param {MessageToClient} message - */ - _sendToAll(message){ - this._clients.forEach(({dataChannel})=>{ - dataChannel.send(message); - }) - } + /** + * Checks whether a given guess is close to the given word. A guess counts as close, if one letter is wrong, + * two letters are swapped or the guess has one letter too much or too little. + * @param {string} guess + * @param {string} word + */ + static _isClose(guess, word) { + guess = guess.toLowerCase().replace(/-/g, " "); + word = word.toLowerCase().replace(/-/g, " "); + const wordArray = Array.from(word); + const guessArray = Array.from(guess); - /** - * 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; - this._clients.forEach(client=>{ - client.points = 0; - client.guessedWord = false; - client.guessedIndex = 0; - }); - (async()=>{ - for (this._round=1;this._round<=this._rounds;this._round++){ - for (this._drawingPlayer=0;this._drawingPlayer<this._clients.length;this._drawingPlayer++){ - let words = await Promise.all([SkribblWords.get(),SkribblWords.get(),SkribblWords.get()]); - this._word = words.map(({word})=>word); - this._sendStateUpdate(); - /** @type {number} */ - let wordIndex = await new Promise(async resolve=>{ - let cancelled = false; - setTimeout(()=>{ - cancelled = true; - resolve(Math.floor(Math.random()*3)); - },10000); - let dataChannel = this._clients[this._drawingPlayer].dataChannel; - while (!cancelled){ - let message = await dataChannel.next(); - if (message.action=="chooseWord"){ - resolve(message.word); - } - } - }); - this._word = words[wordIndex].word; - this._sendToAll({action:"clearCanvas"}); - this._sendStateUpdate(); - this._allGuessedPromise = new ConditionFulfilledPromise(()=>{ - return this._clients.every((client,index)=>client.guessedWord||index==this._drawingPlayer); - }); - let timeOverPromise = Util.wait(this._drawTime); - await Promise.race([this._allGuessedPromise,timeOverPromise]); - let points = this._awardPoints(); - this._sendStateUpdate(); - this._sendToAll({action:"revealWord",word:this._word,description:words[wordIndex].description,macros:await SkribblWords.macros,points}); - await Util.wait(8); - } - } - this._hasGameStarted = false; - this._sendStateUpdate(); - })(); - } - } + //if equal + if (guess == word) { + return true; + } - /** - * Sends a state update to all connected clients (that is, clients that have already joined the game). - */ - _sendStateUpdate(){ - console.log("sending state update! players:",this._clients); - if (this._hasGameStarted){ - let players = this._clients.map(({name,points,guessedWord})=>({name,points,guessedWord})); - this._clients.forEach(({dataChannel},index)=>{ - let host = index==0; - let word = this._drawingPlayer==index?this._word:(typeof this._word=="string"?this._word.replace(/[^ -]/g,"_"):null); - /** @type {ActiveGameState} */ - let state = {players,playerIndex:index,host,hasGameStarted:true,rounds:this._rounds,round:this._round,drawingPlayer:this._drawingPlayer,word}; - dataChannel.send({action:"stateUpdate",newState:state}); - }); - }else{ - let players = this._clients.map(({name})=>({name})); - this._clients.forEach(({dataChannel},index)=>{ - dataChannel.send({action:"stateUpdate",newState:{players,playerIndex:index,host:index==0,hasGameStarted:false}}); - }); - } - } + //either one letter wrong or two letters swapped + if (guess.length == word.length) { + //Counts the mistakes and their position + let errorCounter = 0; + let errorPos = 0; + for (var i = 0; i < wordArray.length; i++) { + if (wordArray[i] != guessArray[i]) { + if (errorCounter == 0) { + errorPos = i; + } + if (errorCounter == 1) { //if a second mistake occurs, either the letters are swapped or it is not correct + //but there could be a third mistake, so in the case of a swap, true is not directly returned. + if ((wordArray[i] != guessArray[errorPos]) || (wordArray[errorPos] != guessArray[i])) { + return false; + } + } + if (errorCounter >= 2) { //with two or more mistakes, the word is not close + return false; + } + errorCounter++; + } + } + //if it hasnt returned false by now, the guess is close + return true; + } - /** - * Handles a guess send from a given dataChannel. - * @param {number} playerIndex - * @param {string} word - */ - _handleGuess(playerIndex,word){ - let player = this._clients[playerIndex]; - if (this._hasGameStarted){ - if (player.guessedWord||playerIndex==this._drawingPlayer){ - // TODO let people who already know the word send ghost messages to others who already know it too - }else if (typeof this._word=="string"&&SkribblServer._isCorrect(word,this._word)){ - player.guessedWord = true; - player.guessedIndex = Math.max(...this._clients.map(client=>client.guessedIndex))+1; - this._clients.forEach((client,index)=>{ - if (index==playerIndex){ - client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:true,word}); - }else{ - client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:true}); - } - }); - this._allGuessedPromise.checkCondition(); - }else{ - this._clients.forEach((client,index)=>{ - let close = typeof this._word=="string"?SkribblServer._isClose(word,this._word):false; - if (index==playerIndex){ - client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:false,word,close}); - }else{ - client.dataChannel.send({action:"guessedWord",player:playerIndex,correct:false,word}); - } - }); - } - } - } + //if one letter too much + if (guess.length - 1 == word.length) { + let errorCounter = 0; //also the offset + for (var i = 0; i < wordArray.length; i++) { + if (wordArray[i] != guessArray[i + errorCounter]) { + errorCounter++; + if (errorCounter >= 2) { + return false; + } + } + } + return true; + } - /** - * Awards points to each player based on if and when they guessed the word, resets the corresponding properties and then returns the list of points each player got. - * @todo find better formula for the number of points each player should get - */ - _awardPoints(){ - let guessingPlayers = this._clients.length-1; - let successfullPlayers = Math.max(...this._clients.map(client=>client.guessedIndex)); - let points = this._clients.map((client,index)=>{ - let points; - if (this._drawingPlayer==index){ - points = successfullPlayers*50; - }else if(client.guessedIndex!=0){ - points = ((guessingPlayers-client.guessedIndex+1)**2)/guessingPlayers*100; - }else{ - points = 0; - } - points = Math.round(points/5)*5; - client.points += points; - client.guessedWord = false; - client.guessedIndex = 0; - return points; - }); - return points; - } + //if one letter too little + if (guess.length + 1 == word.length) { + let errorCounter = 0; //also the offset + for (var i = 0; i < guessArray.length; i++) { + if (wordArray[i + errorCounter] != guessArray[i]) { + errorCounter++; + if (errorCounter >= 2) { + return false; + } + } + } + return true; + } - /** - * Checks whether a given guess is close to the given word. A guess counts as close, if one letter is wrong, - * two letters are swapped or the guess has one letter too much or too little. - * @param {string} guess - * @param {string} word - */ - static _isClose(guess,word) { - guess = guess.toLowerCase().replace(/-/g," "); - word = word.toLowerCase().replace(/-/g," "); - const wordArray = Array.from(word); - const guessArray = Array.from(guess); - - //if equal - if (guess == word) { - return true; - } - - //either one letter wrong or two letters swapped - if (guess.length == word.length) { - //Counts the mistakes and their position - let errorCounter = 0; - let errorPos = 0; - for (var i = 0; i < wordArray.length; i++) { - if (wordArray[i] != guessArray[i]) { - if (errorCounter == 0) { - errorPos = i; - } if (errorCounter == 1) { //if a second mistake occurs, either the letters are swapped or it is not correct - //but there could be a third mistake, so in the case of a swap, true is not directly returned. - if ((wordArray[i] != guessArray[errorPos]) || (wordArray[errorPos] != guessArray[i])) { - return false; - } - } if (errorCounter >= 2) { //with two or more mistakes, the word is not close - return false; - } - errorCounter++; - } - } - //if it hasnt returned false by now, the guess is close - return true; - } - - //if one letter too much - if (guess.length - 1 == word.length) { - let errorCounter = 0; //also the offset - for (var i = 0; i < wordArray.length; i++) { - if (wordArray[i] != guessArray[i + errorCounter]) { - errorCounter++; - if (errorCounter >= 2) { - return false; - } - } - } - return true; - } - - //if one letter too little - if (guess.length + 1 == word.length) { - let errorCounter = 0; //also the offset - for (var i = 0; i < guessArray.length; i++) { - if (wordArray[i + errorCounter] != guessArray[i]) { - errorCounter++; - if (errorCounter >= 2) { - return false; - } - } - } - return true; - } - - return false; - } + return false; + } - /** - * Checks if the guess is correct. It is not case-sensitive. - * @param {string} guess - * @param {string} word - */ - static _isCorrect(guess, word) { - guess = guess.toLowerCase().replace(/-/g," "); - word = word.toLowerCase().replace(/-/g," "); - return guess==word; - } + /** + * Checks if the guess is correct. It is not case-sensitive. + * @param {string} guess + * @param {string} word + */ + static _isCorrect(guess, word) { + guess = guess.toLowerCase().replace(/-/g, " "); + word = word.toLowerCase().replace(/-/g, " "); + return guess == word; + } } \ No newline at end of file diff --git a/client/ui/SkribblCanvas.js b/client/ui/SkribblCanvas.js index 4b7645e2a8c08556bdb8c2b4ce175f1387abead5..0fd71f826c8b1451b5022430ca904997125067ad 100644 --- a/client/ui/SkribblCanvas.js +++ b/client/ui/SkribblCanvas.js @@ -1,5 +1,10 @@ import SkribblClient from "../logic/SkribblClient.js"; import { CustomElement } from "../util/Util.js"; +/** + * @typedef {import("../logic/SkribblServer.js").DrawOrder} DrawOrder + */ + + /** * Custom element `<skribbl-canvas>` for the canvas that can be drawn on by whoever is currently drawing. @@ -27,27 +32,122 @@ export default class SkribblCanvas extends CustomElement { <canvas width="960" height="720"></canvas> `; this._canvas = this.shadowRoot.querySelector("canvas"); + this._radius = 10; + this._color = "red"; + this._client = client; + /** + * looks what kind of order is given, then either draws, erases(=drawing with background color), changes the backgroundcolor or clears the canvas + * Todo: test functionality + */ client.onDraw(order => { + // runs whenever the server tells all clients to draw something on the canvas + + this._canvas = this.shadowRoot.querySelector("canvas"); + var ctx = this._canvas.getContext("2d"); + ctx.lineCap = "round"; + if (order.action == "draw") { + let x = 960 * order.points[0].x / 1000; + let y = 720 * order.points[0].y / 1000; + ctx.beginPath(); + ctx.moveTo(x, y); + let xx = 960 * order.points[1].x / 1000; + let yy = 720 * order.points[1].y / 1000; + ctx.lineTo(xx, yy); + ctx.lineWidth = order.radius; + ctx.strokeStyle = order.color; + ctx.stroke(); + } else if (order.action == "erase") { + let x = 960 * order.points[0].x / 1000; + let y = 720 * order.points[0].y / 1000; + ctx.beginPath(); + ctx.moveTo(x, y); + let xx = 960 * order.points[1].x / 1000; + let yy = 720 * order.points[1].y / 1000; + ctx.lineTo(xx, yy); + ctx.lineWidth = order.radius; + ctx.strokeStyle = this._backgroundColor; + ctx.stroke(); + } else if (order.action == "changeBackgroundColor") { + this._backgroundColor = order.color; + ctx.fillStyle = order.color; + ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); + } else if (order.action == "clearCanvas") { + ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } - // runs whenever the server tells all clients to draw something on the canvas - // TODO draw stuff to the canvas here console.log("incoming draw order:", order); // just for testing, can be removed once this has been implemented }, { onlyWhen: this.connected }); - // TODO whenever the player draws stuff, send it to the server using `client.draw(order)` here + + /** + * is allowed to Draw and last known mouse position + */ + this._isDrawing = false; + this._relativeMousePositionX = 0; + this._relativeMousePositionY = 0; + + + /** + * Sends on MouseMove event if you are allowed to draw a draw event to the server with color, radius and erase/draw + */ + this._canvas.addEventListener("mousemove", e => { + if (this._drawable && this._isDrawing) { + let oldMousePositionX = this._relativeMousePositionX; + let oldMousePositionY = this._relativeMousePositionY; + this.updateRelativeMousePosition(e); // Size not in Percent but in perMille (per Thousand) + + /** @type {"draw"|"erase"} */ + let actionToDo = "draw"; + if (this._erasorActive) { + actionToDo = "erase" + } + /** @type {DrawOrder} */ + let coolOrder = { action: actionToDo, color: this._color, radius: this._radius, points: [{ x: oldMousePositionX, y: oldMousePositionY }, { x: this._relativeMousePositionX, y: this._relativeMousePositionY }] }; + client.draw(coolOrder); + } + }); + + /** + * look if you can draw on the canvas + */ this._canvas.addEventListener("mousedown", e => { - let cAction="draw"; - let coolOrder={action:cAction,color:"#abcdef",radius:10,points:[{x:1,y:1},{x:100,y:100}]}; - client.draw(coolOrder); - let uncoolOrder={action:"clearCanvas"}; - this._canvas = this.shadowRoot.querySelector("canvas"); - var ctx = this._canvas.getContext("2d"); - ctx.moveTo(0, 0); - ctx.lineTo(960, 720); - ctx.stroke(); - // for example, this code runs whenever the user clicks on the canvas. "mousemove", "mouseup", "mouseenter" and "mouseleave" events can be monitored similarly. + this._isDrawing = true; + this.updateRelativeMousePosition(e); + }); + + this._canvas.addEventListener("mouseup", e => { + this._isDrawing = false; + }); + + this._canvas.addEventListener("mouseleave", e => { + this._isDrawing = false; }); + + } + + /** + * Sets the backgroundColor of the canvas + */ + /** @param {string} backgroundColor */ + set backgroundColor(backgroundColor) { + /** @type {import("../logic/SkribblServer.js").BackgroundOrder} */ + let coolOrder = { action: "changeBackgroundColor", color: backgroundColor }; + this._client.draw(coolOrder); + } + + /** + * Tells the relative mouse position on the canvas in per Mille (min0, max1000) + */ + /** @param {MouseEvent} e */ + updateRelativeMousePosition(e) { + this._canvas = this.shadowRoot.querySelector("canvas"); + let canvasSizeWidth = this._canvas.getBoundingClientRect().width; + let canvasSizeHeight = this._canvas.getBoundingClientRect().height; + + + this._relativeMousePositionX = Math.round(1000 * e.offsetX / canvasSizeWidth); // Size not in Percent but in perMille (per Thousand) + this._relativeMousePositionY = Math.round(1000 * e.offsetY / canvasSizeHeight); // Size not in Percent but in perMille (per Thousand) } /** @@ -64,6 +164,38 @@ export default class SkribblCanvas extends CustomElement { return this._drawable; } - // TODO add getters and setters for properties like `color` and `radius` here, so they can then be set in `SkribblCanvasContainer.js` whenever the user clicks on the corresponding controls. + /** + * which radius the pen should have + */ + /** @param {number} radius */ + set radius(radius) { + this._radius = radius; + } + + /** + * Which color the pen should have + */ + /** @param {string} color */ + set color(color) { + this._color = color; + } + + /** + * Whether the erasor is active + */ + /** @param {boolean} erasorActive */ + set erasorActive(erasorActive) { + this._penActive = false; + this._erasorActive = erasorActive; + } + + /** + * Whether the pen is active + */ + /** @param {boolean} penActive */ + set penActive(penActive) { + this._penActive = false; + this._penActive = penActive; + } } customElements.define("skribbl-canvas", SkribblCanvas); \ No newline at end of file