From 8e99da6aa2db3dfae4c2beac6c893a1e2e81cb32 Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Wed, 12 Sep 2018 20:10:00 +0300 Subject: [PATCH] Add 2-player mode --- public/index.html | 1 + public/src/css/songselect.css | 10 + public/src/js/controller.js | 440 +++++++++++++++++----------------- public/src/js/game.js | 220 +++++++++-------- public/src/js/keyboard.js | 25 +- public/src/js/loader.js | 3 + public/src/js/loadsong.js | 47 +++- public/src/js/mekadon.js | 67 ++++-- public/src/js/p2.js | 132 ++++++++++ public/src/js/songselect.js | 38 ++- public/src/js/view.js | 32 ++- server.py | 168 +++++++++++++ 12 files changed, 813 insertions(+), 370 deletions(-) create mode 100644 public/src/js/p2.js create mode 100644 server.py diff --git a/public/index.html b/public/index.html index f88b462..8fdeb7f 100644 --- a/public/index.html +++ b/public/index.html @@ -45,6 +45,7 @@ + diff --git a/public/src/css/songselect.css b/public/src/css/songselect.css index 1147e0d..81a8298 100644 --- a/public/src/css/songselect.css +++ b/public/src/css/songselect.css @@ -75,6 +75,7 @@ ul li{ box-shadow: 2px 2px 10px black; overflow: hidden; cursor: pointer; + position: relative; } .opened{ @@ -127,3 +128,12 @@ ul li{ .difficulty:hover .stars{ color:white; } + +.song.p2:not(.opened)::after, +.difficulty.p2::after{ + content:"P2"; + display:block; + position:absolute; + bottom:0; + width:100%; +} diff --git a/public/src/js/controller.js b/public/src/js/controller.js index c2029b6..491ecea 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -1,226 +1,220 @@ -function Controller(selectedSong, songData, autoPlayEnabled){ - - var _this = this; - var _backgroundURL = "/songs/"+selectedSong.folder+"/bg.png"; - - var _songParser = new ParseSong(songData); //get file content - var _songData = _songParser.getData(); - - var _game = new Game(this, selectedSong, _songData); - var _view = new View(this, _backgroundURL, selectedSong.title, selectedSong.difficulty); - var _mekadon = new Mekadon(this, _game); - var _keyboard = new Keyboard(this); - var _mainLoop; - var _pauseMenu = false; - var _mainAsset - assets.songs.forEach(song => { - if(song.id == selectedSong.folder){ - _mainAsset = song.sound - } - }) - - this.autoPlayEnabled = autoPlayEnabled - - this.run = function(){ +class Controller{ + constructor(selectedSong, songData, autoPlayEnabled, multiplayer){ + this.selectedSong = selectedSong + this.songData = songData + this.autoPlayEnabled = autoPlayEnabled + this.multiplayer = multiplayer + this.pauseMenu = false - _this.loadUIEvents(); - _game.run(); - _view.run(); - _this.startMainLoop(); - } - - this.loadUIEvents = function(){ - $("#song-selection-butt").click(function(){ - assets.sounds["don"].play(); - _this.songSelection(); - }); - $("#restart-butt").click(function(){ - assets.sounds["don"].play(); - _this.restartSong(); - }); - $("#continue-butt").click(function(){ - _this.togglePauseMenu(); - }); + var backgroundURL = "/songs/" + this.selectedSong.folder + "/bg.png" + var songParser = new ParseSong(songData) + this.parsedSongData = songParser.getData() + + assets.songs.forEach(song => { + if(song.id == this.selectedSong.folder){ + this.mainAsset = song.sound + } + }) + + this.game = new Game(this, this.selectedSong, this.parsedSongData) + this.view = new View(this, backgroundURL, this.selectedSong.title, this.selectedSong.difficulty) + this.mekadon = new Mekadon(this, this.game) + this.keyboard = new Keyboard(this) } - - this.startMainLoop = function(){ - - var started=false; - _mainLoop = setInterval(function(){ - - var ms = _game.getEllapsedTime().ms; - if(ms<0){ //before starting game, offseting the circles - _game.updateTime(); - _view.refresh(); - } - else if(ms>=0 && !started){ //when music shall starts - _mainAsset.play(_songData.generalInfo.audioWait) - started=true; - } - - if(started){ //Game start here - if(!_game.isPaused()){ - _game.update(); - _view.refresh(); - _keyboard.checkGameKeys(); - } - _keyboard.checkMenuKeys(); - } - - }, 20); - - } - - this.getDistanceForCircle = function(){ - return _view.getDistanceForCircle(); + run(syncWith){ + this.loadUIEvents() + this.game.run() + this.view.run() + this.startMainLoop() + if(syncWith){ + syncWith.game.getElapsedTime = () => { + return this.game.elapsedTime + } + this.game.setElapsedTime = + syncWith.game.setElapsedTime = time => { + this.game.elapsedTime.ms = time + syncWith.game.elapsedTime.ms = time + } + syncWith.run() + this.syncWith = syncWith + } } - - this.togglePauseMenu = function(){ - _this.togglePause(); - _view.togglePauseMenu(); - } - - this.displayResults = function(){ - clearInterval(_mainLoop); - - - var score = _this.getGlobalScore(); - - if (score.fail == 0) { - vp = 'fullcombo'; - _this.playSoundMeka('fullcombo', 1.350); - } else if (score.hp >= 50) { - vp = 'clear'; - } else { - vp = 'fail'; - } - - assets.sounds['game' + vp].play(); - - setTimeout(function(){ - new Scoresheet(_this, _this.getGlobalScore()); - }, 7000); - } - - this.displayScore = function(score, notPlayed){ - _view.displayScore(score, notPlayed); - } - - this.fadeOutOver = function(){ - _game.fadeOutOver(); - _this.displayResults(); - } - - this.getCurrentTimingPoint = function(){ - return _game.getCurrentTimingPoint(); - } - - this.songSelection = function(){ - $("#main-music").remove(); - $("#music-bg").remove(); - clearInterval(_mainLoop); - new SongSelect(); - } - - this.restartSong = function(){ - _mainAsset.stop() - clearInterval(_mainLoop); - $("#screen").load("/src/views/game.html", function(){ - var taikoGame = new Controller(selectedSong, songData, autoPlayEnabled); - taikoGame.run(); - }); - } - - this.playSoundMeka = function(soundID, time){ - assets.sounds[soundID + (autoPlayEnabled ? '-meka' : '')].play(time) - } - - this.initTiming = function(){ - _game.initTiming(); - } - - this.setHitcircleSpeed = function(speed){ - _view.setHitcircleSpeed(speed); - } - - this.getHitcircleSpeed = function(){ - return _game.getHitcircleSpeed(); - } - - this.toggleMainMusic = function(){ - _game.toggleMainMusic(); - } - - this.togglePause = function(){ - _game.togglePause(); - } - - this.isPaused = function(){ - return _game.isPaused(); - } - - this.getKeys = function(){ - return _keyboard.getKeys(); - } - - this.setKey = function(keyCode, down){ - return _keyboard.setKey(keyCode, down); - } - - this.getBindings = function(){ - return _keyboard.getBindings(); - } - - this.getSongData = function(){ - return _game.getSongData(); - } - - this.getEllapsedTime = function(){ - return _game.getEllapsedTime(); - } - - this.getCircles = function(){ - return _game.getCircles(); - } - - this.getCurrentCircle = function(){ - return _game.getCurrentCircle(); - } - - this.updateCurrentCircle = function(){ - _game.updateCurrentCircle(); - } - - this.isWaitingForKeyup = function(key, type){ - return _keyboard.isWaitingForKeyup(key, type); - } - - this.waitForKeyup = function(key, type){ - _keyboard.waitForKeyup(key, type); - } - - this.getKeyTime = function(){ - return _keyboard.getKeyTime(); - } - - this.updateCombo = function(score){ - _game.updateCombo(score); - } - - this.getCombo = function(){ - return _game.getCombo(); - } - - this.getGlobalScore = function(){ - return _game.getGlobalScore(); - } - - this.updateGlobalScore = function(score){ - _game.updateGlobalScore(score); - } - - this.autoPlay = function(circle){ - _mekadon.play(circle) - } - -} \ No newline at end of file + loadUIEvents(){ + $("#song-selection-butt").click(() => { + assets.sounds["don"].play() + this.songSelection() + }) + $("#restart-butt").click(() => { + assets.sounds["don"].play() + this.restartSong() + }) + $("#continue-butt").click(() => { + this.togglePauseMenu() + }) + } + startMainLoop(){ + this.mainLoopStarted = false + this.mainLoopRunning = true + this.mainLoop() + } + mainLoop(){ + if(this.mainLoopRunning){ + if(this.multiplayer != 2){ + requestAnimationFrame(() => { + if(this.syncWith){ + this.syncWith.game.elapsedTime.ms = this.game.elapsedTime.ms + } + this.mainLoop() + if(this.syncWith){ + this.syncWith.mainLoop() + } + }) + } + var ms = this.game.getElapsedTime().ms + if(!this.game.isPaused()){ + if(ms >= 0 && !this.mainLoopStarted){ + this.mainLoopStarted = true + } + if(ms < 0){ + this.game.updateTime() + } + if(this.mainLoopStarted){ + this.game.update() + this.game.playMainMusic() + } + this.view.refresh() + this.keyboard.checkGameKeys() + } + this.keyboard.checkMenuKeys() + } + } + getDistanceForCircle(){ + return this.view.getDistanceForCircle() + } + togglePauseMenu(){ + this.togglePause() + this.view.togglePauseMenu() + } + displayResults(){ + var score = this.getGlobalScore() + var vp + if (score.fail == 0) { + vp = "fullcombo" + this.playSoundMeka("fullcombo", 1.350) + } else if (score.hp >= 50) { + vp = "clear" + } else { + vp = "fail" + } + assets.sounds["game" + vp].play() + setTimeout(() => { + this.mainLoopRunning = false + if(this.multiplayer != 2){ + new Scoresheet(this, this.getGlobalScore()) + } + }, 7000) + } + displayScore(score, notPlayed){ + this.view.displayScore(score, notPlayed) + } + fadeOutOver(){ + this.game.fadeOutOver() + this.displayResults() + } + getCurrentTimingPoint(){ + return this.game.getCurrentTimingPoint() + } + songSelection(){ + $("#music-bg").remove() + this.mainLoopRunning = false + new SongSelect() + } + restartSong(){ + this.mainAsset.stop() + this.mainLoopRunning = false + $("#screen").load("/src/views/game.html", () => { + var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled) + taikoGame.run() + }) + } + playSoundMeka(soundID, time){ + var meka = "" + if(this.autoPlayEnabled && !this.multiplayer){ + meka = "-meka" + } + assets.sounds[soundID + meka].play(time) + } + initTiming(){ + this.game.initTiming() + } + setHitcircleSpeed(speed){ + this.view.setHitcircleSpeed(speed) + } + getHitcircleSpeed(){ + return this.game.getHitcircleSpeed() + } + toggleMainMusic(){ + this.game.toggleMainMusic() + } + togglePause(){ + if(this.syncWith){ + this.syncWith.game.togglePause() + } + this.game.togglePause() + } + isPaused(){ + return this.game.isPaused() + } + getKeys(){ + return this.keyboard.getKeys() + } + setKey(keyCode, down){ + return this.keyboard.setKey(keyCode, down) + } + getBindings(){ + return this.keyboard.getBindings() + } + getSongData(){ + return this.game.getSongData() + } + getElapsedTime(){ + return this.game.getElapsedTime() + } + getCircles(){ + return this.game.getCircles() + } + getCurrentCircle(){ + return this.game.getCurrentCircle() + } + updateCurrentCircle(){ + this.game.updateCurrentCircle() + } + isWaitingForKeyup(key, type){ + return this.keyboard.isWaitingForKeyup(key, type) + } + waitForKeyup(key, type){ + this.keyboard.waitForKeyup(key, type) + } + getKeyTime(){ + return this.keyboard.getKeyTime() + } + updateCombo(score){ + this.game.updateCombo(score) + } + getCombo(){ + return this.game.getCombo() + } + getGlobalScore(){ + return this.game.getGlobalScore() + } + updateGlobalScore(score){ + this.game.updateGlobalScore(score) + } + autoPlay(circle){ + if(this.multiplayer){ + p2.play(circle, this.mekadon) + }else{ + this.mekadon.play(circle) + } + } +} diff --git a/public/src/js/game.js b/public/src/js/game.js index 45f4e86..24fcc68 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -2,7 +2,7 @@ function Game(controller, selectedSong, songData){ var _this = this; var _selectedSong = selectedSong; - var _ellapsedTime; //current time in ms from the beginning of the song + this.elapsedTime = {} //current time in ms from the beginning of the song var _offsetDate; //date when the chrono is started (before the game begins) var _startDate; //real start date (when the chrono will be 0) var _currentDate; // refreshed date @@ -14,60 +14,53 @@ function Game(controller, selectedSong, songData){ var _HPGain= 100/_songData.circles.length; var _paused=false; var _started=false; - var _mainMusicPlaying=true; + var _mainMusicPlaying=false; var _latestDate; - var _ellapsedTimeSincePause=0; + var _elapsedTimeSincePause=0; var _musicFadeOut=0; var _fadeOutStarted=false; var _currentTimingPoint=0; var _offsetTime=0; - var _hitcircleSpeed=_songData.difficulty.sliderMultiplier*8; - var _timeForDistanceCircle; - var _mainAsset - assets.songs.forEach(song => { - if(song.id == selectedSong.folder){ - _mainAsset = song.sound - } - }) + var _hitcircleSpeed=_songData.difficulty.sliderMultiplier*8; + var _timeForDistanceCircle; + var _mainAsset + assets.songs.forEach(song => { + if(song.id == selectedSong.folder){ + _mainAsset = song.sound + } + }) this.run = function(){ - _timeForDistanceCircle=((20*controller.getDistanceForCircle())/_hitcircleSpeed); - _this.initTiming(); + _timeForDistanceCircle=2500 + _this.initTiming(); } this.initTiming = function(){ - _offsetDate = new Date(); - - _ellapsedTime = { - ms:-parseInt(_timeForDistanceCircle), - sec:0, - min:0, - hour:0 - } - _offsetTime = parseInt(_timeForDistanceCircle); + _this.setElapsedTime(-_timeForDistanceCircle |0) + _offsetTime = _timeForDistanceCircle |0 _startDate = new Date(); - _startDate.setMilliseconds(_startDate.getMilliseconds()+_offsetTime); //The real start for the game will start when chrono will reach 0 - + // The real start for the game will start when chrono will reach 0 + _startDate.setMilliseconds(_startDate.getMilliseconds()+_offsetTime); } this.update = function(){ - /* Main operations */ + // Main operations _this.updateTime(); _this.checkTiming(); _this.updateCirclesStatus(); _this.checkPlays(); - /* Event operations */ + // Event operations _this.whenFadeoutMusic(); _this.whenLastCirclePlayed(); - + + } + + this.getCircles = function(){ + return _songData.circles; } - - this.getCircles = function(){ - return _songData.circles; - } this.updateCirclesStatus = function(){ @@ -77,13 +70,14 @@ function Game(controller, selectedSong, songData){ if(!circle.getPlayed()){ - var currentTime = _ellapsedTime.ms; + var currentTime = _this.getElapsedTime().ms; var startingTime = circle.getMS()-_timeForDistanceCircle; - var finishTime = circle.getMS(); //at circle.getMS(), the cirlce fits the slot + // At circle.getMS(), the circle fits the slot + var finishTime = circle.getMS(); if( currentTime >= startingTime && currentTime <= finishTime+200){ - if(currentTime>= finishTime-50 && currentTime < finishTime-30){ + if(currentTime>= finishTime-50 && currentTime < finishTime-30){ circle.updateStatus(0); } else if(currentTime>= finishTime-30 && currentTime < finishTime){ @@ -95,14 +89,20 @@ function Game(controller, selectedSong, songData){ } else if(currentTime>finishTime+200 && currentTime<=finishTime+300){ - - circle.updateStatus(-1); - _currentScore=0; - circle.played(_currentScore); - controller.displayScore(_currentScore, true); - _this.updateCurrentCircle(); - _this.updateCombo(_currentScore); - _this.updateGlobalScore(_currentScore); + if(controller.multiplayer != 2){ + circle.updateStatus(-1); + _currentScore=0; + circle.played(_currentScore); + controller.displayScore(_currentScore, true); + _this.updateCurrentCircle(); + _this.updateCombo(_currentScore); + _this.updateGlobalScore(_currentScore); + } + if(controller.multiplayer == 1){ + p2.send("note", { + score: -1 + }) + } } @@ -118,7 +118,7 @@ function Game(controller, selectedSong, songData){ this.checkPlays = function(){ - var circles = _songData.circles; + var circles = _songData.circles; var circle = circles[_currentCircle]; if(circle){ @@ -149,6 +149,12 @@ function Game(controller, selectedSong, songData){ circle.played(score); _this.updateCurrentCircle(); controller.waitForKeyup(keyCode, "score"); + if(controller.multiplayer == 1){ + p2.send("note", { + score: score, + ms: circle.getMS() - _this.getElapsedTime().ms + }) + } } } @@ -158,7 +164,7 @@ function Game(controller, selectedSong, songData){ var kbd = controller.getBindings() if( - ((keys[kbd["don_l"]] || keys[kbd["don_r"]]) && (circle.getType()=="don" || circle.getType()=="daiDon")) || + ((keys[kbd["don_l"]] || keys[kbd["don_r"]]) && (circle.getType()=="don" || circle.getType()=="daiDon")) || ((keys[kbd["ka_l"]] || keys[kbd["ka_r"]]) && (circle.getType()=="ka" || circle.getType()=="daiKa")) ){ @@ -173,25 +179,25 @@ function Game(controller, selectedSong, songData){ break; } - controller.displayScore(_currentScore); + controller.displayScore(_currentScore); } else{ _currentScore=0; - controller.displayScore(_currentScore, true); + controller.displayScore(_currentScore, true); } _this.updateCombo(_currentScore); _this.updateGlobalScore(_currentScore); - return _currentScore; + return _currentScore; } this.whenLastCirclePlayed = function(){ var circles = _songData.circles; var lastCircle = circles[_songData.circles.length-1]; - if(!_fadeOutStarted && _ellapsedTime.ms>=lastCircle.getMS()+2000){ - _fadeOutStarted=_ellapsedTime.ms + if(!_fadeOutStarted && _this.getElapsedTime().ms>=lastCircle.getMS()+2000){ + _fadeOutStarted=_this.getElapsedTime().ms } } @@ -199,23 +205,27 @@ function Game(controller, selectedSong, songData){ if(_fadeOutStarted){ if(_musicFadeOut==0){ snd.musicGain.fadeOut(1.6) + _musicFadeOut++ + if(controller.multiplayer == 1){ + p2.send("gameend") + } } - if(_ellapsedTime.ms>=_fadeOutStarted+1600){ + if(_musicFadeOut==1 && _this.getElapsedTime().ms>=_fadeOutStarted+1600){ controller.fadeOutOver() _mainAsset.stop() + _musicFadeOut++ setTimeout(() => { snd.musicGain.fadeIn() snd.musicGain.unmute() }, 1000) } - _musicFadeOut++; } } this.checkTiming = function(){ if(_songData.timingPoints[_currentTimingPoint+1]){ - if(_this.getEllapsedTime().ms>=_songData.timingPoints[_currentTimingPoint+1].start){ + if(_this.getElapsedTime().ms>=_songData.timingPoints[_currentTimingPoint+1].start){ _currentTimingPoint++; } } @@ -225,19 +235,18 @@ function Game(controller, selectedSong, songData){ return _songData.timingPoints[_currentTimingPoint]; } - this.toggleMainMusic = function(){ - if(_mainMusicPlaying){ - _mainAsset.stop(); - _mainMusicPlaying=false; - } - else{ - _mainAsset.play(0, false, _this.getEllapsedTime().ms / 1000); + this.playMainMusic = function(){ + var ms = _this.getElapsedTime().ms + if(!_mainMusicPlaying && (!_fadeOutStarted || ms<_fadeOutStarted+1600)){ + if(controller.multiplayer != 2){ + _mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000)); + } _mainMusicPlaying=true; } } this.fadeOutOver = function(){ - _fadeOutStarted=false; + } this.getHitcircleSpeed = function(){ @@ -249,15 +258,15 @@ function Game(controller, selectedSong, songData){ assets.sounds["pause"].play(); _paused=true; _latestDate = new Date(); - _this.toggleMainMusic(); + _mainAsset.stop(); + _mainMusicPlaying=false; } else{ assets.sounds["cancel"].play(); - _paused=false; + _paused=false; var currentDate = new Date(); - _ellapsedTimeSincePause = _ellapsedTimeSincePause + Math.abs(currentDate.getTime() - _latestDate.getTime()); - _this.toggleMainMusic(); + _elapsedTimeSincePause = _elapsedTimeSincePause + currentDate.getTime() - _latestDate.getTime(); } } @@ -265,30 +274,33 @@ function Game(controller, selectedSong, songData){ return _paused; } - this.getEllapsedTime = function(){ - return _ellapsedTime; + this.getElapsedTime = function(){ + return this.elapsedTime; + } + + this.setElapsedTime = function(time){ + this.elapsedTime.ms = time + this.elapsedTime.sec = (this.elapsedTime.ms / 1000 |0) % 60 + this.elapsedTime.min = (this.elapsedTime.ms / 1000 / 60 |0) % 60 + this.elapsedTime.hour = (this.elapsedTime.ms / 1000 / 60 / 60 |0) % 60 } this.updateTime = function(){ - _currentDate = new Date(); + var time = _this.getElapsedTime() - if(_ellapsedTime.ms<0){ - _ellapsedTime.ms = _currentDate.getTime() - _startDate.getTime(); + if(time.ms<0){ + _this.setElapsedTime(_currentDate.getTime() - _startDate.getTime() - _elapsedTimeSincePause) } - else if(_ellapsedTime.ms>=0 && !_started){ + else if(time.ms>=0 && !_started){ _startDate = new Date(); - _ellapsedTime.ms = Math.abs(_startDate.getTime() - _currentDate.getTime()); + _elapsedTimeSincePause = 0; + _this.setElapsedTime(_currentDate.getTime() - _startDate.getTime()) _started=true; } - else if(_ellapsedTime.ms>=0 && _started){ - _ellapsedTime.ms = Math.abs(_startDate.getTime() - _currentDate.getTime()) - _ellapsedTimeSincePause; + else if(time.ms>=0 && _started){ + _this.setElapsedTime(_currentDate.getTime() - _startDate.getTime() - _elapsedTimeSincePause) } - - _ellapsedTime.sec = parseInt(_ellapsedTime.ms / 1000) % 60; - _ellapsedTime.min = parseInt(_ellapsedTime.ms / (1000 * 60)) % 60; - _ellapsedTime.hour = parseInt(_ellapsedTime.ms / (1000 * 60 * 60)) % 60; - } this.getCircles = function(){ @@ -300,7 +312,7 @@ function Game(controller, selectedSong, songData){ } this.updateCurrentCircle = function(){ - _currentCircle++; + _currentCircle++; } this.getCurrentCircle = function(){ @@ -400,32 +412,32 @@ function Game(controller, selectedSong, songData){ if(_combo>=11 && _combo<=20){ score+=100; } - else if(_combo>=21 && _combo<=30){ - score+=200; - } - else if(_combo>=31 && _combo<=40){ - score+=300; - } - else if(_combo>=41 && _combo<=50){ - score+=400; - } - else if(_combo>=51 && _combo<=60){ - score+=500; - } - else if(_combo>=61 && _combo<=70){ - score+=500; - } - else if(_combo>=71 && _combo<=80){ - score+=600; - } - else if(_combo>=81 && _combo<=90){ - score+=700; - } - else if(_combo>=91 && _combo<=100){ - score+=800; - } + else if(_combo>=21 && _combo<=30){ + score+=200; + } + else if(_combo>=31 && _combo<=40){ + score+=300; + } + else if(_combo>=41 && _combo<=50){ + score+=400; + } + else if(_combo>=51 && _combo<=60){ + score+=500; + } + else if(_combo>=61 && _combo<=70){ + score+=500; + } + else if(_combo>=71 && _combo<=80){ + score+=600; + } + else if(_combo>=81 && _combo<=90){ + score+=700; + } + else if(_combo>=91 && _combo<=100){ + score+=800; + } - _globalScore.points+=score; + _globalScore.points+=score; } diff --git a/public/src/js/keyboard.js b/public/src/js/keyboard.js index e8b0842..efcbc94 100644 --- a/public/src/js/keyboard.js +++ b/public/src/js/keyboard.js @@ -65,14 +65,21 @@ function Keyboard(controller){ } this.checkMenuKeys = function(){ - _gamepad.play(1) - _this.checkKey(_kbd["back"], "menu", function(){ - controller.togglePause(); - controller.songSelection(); - }) - _this.checkKey(_kbd["pause"], "menu", function(){ - controller.togglePauseMenu(); - }) + if(!controller.multiplayer){ + _gamepad.play(1) + _this.checkKey(_kbd["pause"], "menu", function(){ + controller.togglePauseMenu(); + }) + } + if(controller.multiplayer != 2){ + _this.checkKey(_kbd["back"], "menu", function(){ + if(controller.multiplayer == 1){ + p2.send("gameend") + } + controller.togglePause(); + controller.songSelection(); + }) + } } this.checkKey = function(keyCode, keyup, callback){ @@ -85,7 +92,7 @@ function Keyboard(controller){ this.checkKeySound = function(keyCode, sound){ _this.checkKey(keyCode, "sound", function(){ assets.sounds["note_"+sound].play() - _keyTime[sound] = controller.getEllapsedTime().ms + _keyTime[sound] = controller.getElapsedTime().ms }) } diff --git a/public/src/js/loader.js b/public/src/js/loader.js index f1c8e55..deb91aa 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -51,6 +51,8 @@ class Loader{ })) }) + p2 = new P2Connection() + this.promises.push(ajax("/api/songs").then(songs => { assets.songs = JSON.parse(songs) })) @@ -100,4 +102,5 @@ function promiseLoad(asset){ } var snd = {} +var p2 new Loader() diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index f0bbc94..af8f3e3 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -1,7 +1,9 @@ class loadSong{ - constructor(selectedSong, autoPlayEnabled){ + constructor(selectedSong, autoPlayEnabled, multiplayer){ this.selectedSong = selectedSong + this.multiplayer = multiplayer this.autoPlayEnabled = autoPlayEnabled + this.diff = this.selectedSong.difficulty.slice(0, -4) this.songFilePath = "/songs/" + this.selectedSong.folder + "/" + this.selectedSong.difficulty $("#screen").load("/src/views/loadsong.html", () => { this.run() @@ -42,11 +44,46 @@ class loadSong{ Promise.all(promises).then(() => { $("#screen").load("/src/views/game.html", () => { - var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled) - taikoGame.run() + this.setupMultiplayer() }) - }, () => { + }, error => { + console.error(error) alert("An error occurred, please refresh") }) } - } \ No newline at end of file + setupMultiplayer(){ + if(this.multiplayer){ + this.song2Data = this.songData + this.selectedSong2 = this.selectedSong + p2.onmessage("gamestart", () => { + var taikoGame1 = new Controller(this.selectedSong, this.songData, false, 1) + var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2) + taikoGame1.run(taikoGame2) + }, true) + p2.onmessage("gameload", response => { + if(response == this.diff){ + p2.send("gamestart") + }else{ + this.selectedSong2 = { + title: this.selectedSong.title, + folder: this.selectedSong.folder, + difficulty: response + ".osu" + } + ajax("/songs/" + this.selectedSong2.folder + "/" + this.selectedSong2.difficulty).then(data => { + this.song2Data = data.replace(/\0/g, "").split("\n") + p2.send("gamestart") + }, () => { + p2.send("gamestart") + }) + } + }, true) + p2.send("join", { + id: this.selectedSong.folder, + diff: this.diff + }) + }else{ + var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled) + taikoGame.run() + } + } +} diff --git a/public/src/js/mekadon.js b/public/src/js/mekadon.js index b365de0..3ef38d9 100644 --- a/public/src/js/mekadon.js +++ b/public/src/js/mekadon.js @@ -7,27 +7,56 @@ class Mekadon{ } play(circle){ if(circle.getStatus() == 450){ - var kbd = this.controller.getBindings() - if(circle.getType() == "don"){ - this.setKey(this.lr ? kbd["don_l"] : kbd["don_r"]) - this.lr = !this.lr - }else if(circle.getType() == "daiDon"){ - this.setKey(kbd["don_l"]) - this.setKey(kbd["don_r"]) - this.lr = false - }else if(circle.getType() == "ka"){ - this.setKey(this.lr ? kbd["ka_l"] : kbd["ka_r"]) - this.lr = !this.lr - }else if(circle.getType() == "daiKa"){ - this.setKey(kbd["ka_l"]) - this.setKey(kbd["ka_r"]) - this.lr = false - } - var score = this.game.checkScore(circle); - circle.played(score); - this.game.updateCurrentCircle(); + return this.playNow(circle) } } + playAt(circle, ms, score){ + var currentMs = circle.getMS() - this.controller.getElapsedTime().ms + if(ms > currentMs - 10){ + return this.playNow(circle, score) + } + } + miss(circle){ + var currentMs = circle.getMS() - this.controller.getElapsedTime().ms + if(0 > currentMs - 10){ + circle.updateStatus(-1) + circle.played(0) + this.controller.displayScore(0, true) + this.game.updateCurrentCircle() + this.game.updateCombo(0) + this.game.updateGlobalScore(0) + return true + } + } + playNow(circle, score){ + var kbd = this.controller.getBindings() + if(circle.getType() == "don"){ + this.setKey(this.lr ? kbd["don_l"] : kbd["don_r"]) + this.lr = !this.lr + }else if(circle.getType() == "daiDon"){ + this.setKey(kbd["don_l"]) + this.setKey(kbd["don_r"]) + this.lr = false + }else if(circle.getType() == "ka"){ + this.setKey(this.lr ? kbd["ka_l"] : kbd["ka_r"]) + this.lr = !this.lr + }else if(circle.getType() == "daiKa"){ + this.setKey(kbd["ka_l"]) + this.setKey(kbd["ka_r"]) + this.lr = false + } + if(typeof score == "undefined"){ + score = this.game.checkScore(circle) + }else{ + this.controller.displayScore(score) + this.game.updateCombo(score) + this.game.updateGlobalScore(score) + } + circle.updateStatus(score) + circle.played(score) + this.game.updateCurrentCircle() + return true + } setKey(keyCode){ var self = this if(this.keys[keyCode]){ diff --git a/public/src/js/p2.js b/public/src/js/p2.js new file mode 100644 index 0000000..b1be188 --- /dev/null +++ b/public/src/js/p2.js @@ -0,0 +1,132 @@ +class P2Connection{ + constructor(){ + this.closed = true + this.lastMessages = {} + this.msgCallbacks = {} + this.closeCallbacks = new Set() + this.openCallbacks = new Set() + this.notes = [] + this.otherConnected = false + this.onmessage("gamestart", () => { + this.otherConnected = true + this.notes = [] + }) + this.onmessage("gameend", () => { + this.otherConnected = false + }) + this.onmessage("note", response => { + this.notes.push(response) + }) + } + open(){ + this.closed = false + var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:" + this.socket = new WebSocket(wsProtocol + "//" + window.location.hostname + "/p2") + var events = ["open", "close", "message"] + events.forEach(eventName => { + this.socket.addEventListener(eventName, event => { + this[eventName + "Event"](event) + }) + }) + } + openEvent(event){ + this.openCallbacks.forEach(obj => { + obj.callback() + if(obj.once){ + this.openCallbacks.delete(obj) + } + }) + } + onopen(callback, once){ + this.openCallbacks.add({ + callback: callback, + once: once + }) + } + close(){ + this.closed = true + this.socket.close() + } + closeEvent(event){ + if(!this.closed){ + setTimeout(() => { + if(this.socket.readyState != this.socket.OPEN){ + this.open() + } + }, 500) + } + this.closeCallbacks.forEach(obj => { + obj.callback() + if(obj.once){ + this.closeCallbacks.delete(obj) + } + }) + } + onclose(callback, once){ + this.closeCallbacks.add({ + callback: callback, + once: once + }) + } + send(type, value){ + if(this.socket.readyState == this.socket.OPEN){ + if(typeof value == "undefined"){ + this.socket.send(JSON.stringify({type: type})) + }else{ + this.socket.send(JSON.stringify({type: type, value: value})) + } + }else{ + this.onopen(() => { + this.send(type, value) + }, true) + } + } + messageEvent(event){ + try{ + var data = JSON.parse(event.data) + }catch(e){ + var data = {} + } + this.lastMessages[data.type] = data.value + if(this.msgCallbacks[data.type]){ + this.msgCallbacks[data.type].forEach(obj => { + obj.callback(data.value) + if(obj.once){ + delete this.msgCallbacks[obj] + } + }) + } + } + onmessage(type, callback, once){ + if(!(type in this.msgCallbacks)){ + this.msgCallbacks[type] = new Set() + } + this.msgCallbacks[type].add({ + callback: callback, + once: once + }) + } + getMessage(type, callback){ + if(type in this.lastMessages){ + callback(this.lastMessages[type]) + } + } + play(circle, mekadon){ + if(this.otherConnected){ + if(this.notes.length == 0){ + mekadon.play(circle) + }else{ + var note = this.notes[0] + if(note.score >= 0){ + if(mekadon.playAt(circle, note.ms, note.score)){ + this.notes.shift() + } + }else{ + if(mekadon.miss(circle)){ + this.notes.shift() + } + } + } + } + } +} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 0b41ac8..4832901 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -56,6 +56,7 @@ function SongSelect(){ this.run = function(){ _this.createCode(); + _this.startP2(); $("#song-container").show(); @@ -84,7 +85,7 @@ function SongSelect(){ _selectedSong.folder = songID; snd.musicGain.fadeIn() - new loadSong(_selectedSong, e.shiftKey); + new loadSong(_selectedSong, e.shiftKey, e.ctrlKey); }); $(".song").hover(function(){ @@ -236,6 +237,41 @@ function SongSelect(){ $('.difficulty').hide(); } + this.onusers = function(response){ + var oldP2Elements = document.getElementsByClassName("p2") + for(var i = oldP2Elements.length; i--;){ + oldP2Elements[i].classList.remove("p2") + } + if(response){ + response.forEach(idDiff => { + id = idDiff.id |0 + diff = idDiff.diff + if(diff in _diffNames){ + var idElement = document.getElementById("song-" + id) + if(idElement){ + idElement.classList.add("p2") + var diffElement = idElement.getElementsByClassName("difficulty " + diff)[0] + if(diffElement){ + diffElement.classList.add("p2") + } + } + } + }) + } + } + + this.startP2 = function(){ + p2.getMessage("users", response =>{ + this.onusers(response) + }) + p2.onmessage("users", response => { + this.onusers(response) + }) + if(p2.closed){ + p2.open() + } + } + $("#screen").load("/src/views/songselect.html", _this.run); } \ No newline at end of file diff --git a/public/src/js/view.js b/public/src/js/view.js index e603d35..02cceae 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -4,7 +4,14 @@ class View{ this.bg = bg this.diff = diff - this.canvas = new ScalableCanvas("canvas", $(window).width(), $(window).height()) + if(this.controller.multiplayer == 2){ + this.canvas = new ScalableCanvas("canvas-p2", $(window).width(), $(window).height() / 3 * 2) + this.canvas.canvas.style.position = "absolute" + this.canvas.canvas.style.top = "33%" + document.getElementById("game").appendChild(this.canvas.canvas) + }else{ + this.canvas = new ScalableCanvas("canvas", $(window).width(), $(window).height()) + } this.winW = this.canvas.scaledWidth this.winH = this.canvas.scaledHeight this.ctx = this.canvas.ctx @@ -50,9 +57,16 @@ class View{ positionning(){ this.canvas.rescale() - this.canvas.resize($(window).width(), $(window).height()) + var height = $(window).height() + if(this.controller.multiplayer == 2){ + height = height / 3 * 2 + } + this.canvas.resize($(window).width(), height) this.winW = this.canvas.scaledWidth this.winH = this.canvas.scaledHeight + if(this.controller.multiplayer == 2){ + this.winH = this.winH / 2 * 3 + } this.barY = 0.25 * this.winH this.barH = 0.23 * this.winH @@ -113,7 +127,7 @@ class View{ } updateDonFaces(){ - if(this.controller.getEllapsedTime().ms >= this.nextBeat){ + if(this.controller.getElapsedTime().ms >= this.nextBeat){ this.nextBeat += this.controller.getSongData().beatInfo.beatInterval if(this.controller.getCombo() >= 50){ this.currentBigDonFace = (this.currentBigDonFace + 1) % 2 @@ -190,7 +204,7 @@ class View{ drawMeasures(){ var measures = this.controller.getSongData().measures - var currentTime = this.controller.getEllapsedTime().ms + var currentTime = this.controller.getElapsedTime().ms measures.forEach((measure, index)=>{ var timeForDistance = 70 / this.circleSize * this.distanceForCircle / measure.speed @@ -206,7 +220,7 @@ class View{ drawMeasure(measure){ var z = this.canvas.scale - var currentTime = this.controller.getEllapsedTime().ms + var currentTime = this.controller.getElapsedTime().ms var measureX = this.slotX + measure.speed / (70 / this.circleSize) * (measure.ms - currentTime) this.ctx.strokeStyle = "#bab8b8" this.ctx.lineWidth = 2 @@ -388,7 +402,7 @@ class View{ for(var i = circles.length; i--;){ var circle = circles[i] - var currentTime = this.controller.getEllapsedTime().ms + var currentTime = this.controller.getElapsedTime().ms var timeForDistance = 70 / this.circleSize * this.distanceForCircle / circle.getSpeed() + this.bigCircleSize / 2 var startingTime = circle.getMS() - timeForDistance // At circle.getMS(), the cirlce fits the slot @@ -459,7 +473,7 @@ class View{ var fill, size, faceID if(!circlePos){ - var currentTime = this.controller.getEllapsedTime().ms + var currentTime = this.controller.getElapsedTime().ms circlePos = { x: this.slotX + circle.getSpeed() / (70 / this.circleSize) * (circle.getMS() - currentTime), y: this.circleY @@ -549,7 +563,7 @@ class View{ drawTime(){ var z = this.canvas.scale - var time = this.controller.getEllapsedTime() + var time = this.controller.getElapsedTime() this.ctx.globalAlpha = 0.7 this.ctx.fillStyle = "#000" @@ -579,7 +593,7 @@ class View{ this.ctx.closePath() this.ctx.fill() - var currentTime = this.controller.getEllapsedTime().ms + var currentTime = this.controller.getElapsedTime().ms var keyTime = this.controller.getKeyTime() var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka" if(keyTime[sound] > currentTime - 200){ diff --git a/server.py b/server.py new file mode 100644 index 0000000..740bbf1 --- /dev/null +++ b/server.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +import asyncio +import websockets +import json + +users = [] +server_status = { + "waiting": {} +} + +def msgobj(type, value=None): + if value == None: + return json.dumps({"type": type}) + else: + return json.dumps({"type": type, "value": value}) + +def status_event(): + value = [] + for id, userDiff in server_status["waiting"].items(): + value.append({ + "id": id, + "diff": userDiff["diff"] + }) + return msgobj("users", value) + +async def notify_status(): + ready_users = [user for user in users if "ws" in user and user["action"] == "ready"] + if ready_users: + sent_msg = status_event() + await asyncio.wait([user["ws"].send(sent_msg) for user in ready_users]) + +async def connection(ws, path): + # User connected + user = { + "ws": ws, + "action": "ready" + } + users.append(user) + try: + # Notify user about other users + await ws.send(status_event()) + while True: + try: + message = await asyncio.wait_for(ws.recv(), timeout=5) + except asyncio.TimeoutError: + # Keep user connected + pong_waiter = await ws.ping() + try: + await asyncio.wait_for(pong_waiter, timeout=5) + except asyncio.TimeoutError: + # Disconnect + break + else: + # Message received + try: + data = json.loads(message) + except json.decoder.JSONDecodeError: + data = {} + action = user["action"] + type = data["type"] if "type" in data else None + value = data["value"] if "value" in data else None + if action == "ready": + # Not playing or waiting + if type == "join": + waiting = server_status["waiting"] + id = value["id"] if "id" in value else None + diff = value["diff"] if "diff" in value else None + if not id or not diff: + continue + if id not in waiting: + # Wait for another user + user["action"] = "waiting" + user["gameid"] = id + waiting[id] = { + "user": user, + "diff": diff + } + await ws.send(msgobj("waiting")) + else: + # Join the other user and start game + user["other_user"] = waiting[id]["user"] + waiting_diff = waiting[id]["diff"] + del waiting[id] + if "ws" in user["other_user"]: + user["action"] = "loading" + user["other_user"]["action"] = "loading" + user["other_user"]["other_user"] = user + await asyncio.wait([ + ws.send(msgobj("gameload", waiting_diff)), + user["other_user"]["ws"].send(msgobj("gameload", diff)) + ]) + else: + # Wait for another user + user["action"] = "waiting" + user["gameid"] = id + waiting[id] = { + "user": user, + "diff": diff + } + await ws.send(msgobj("waiting")) + # Update others on waiting players + await notify_status() + elif action == "waiting" or action == "loading" or action == "loaded": + # Waiting for another user + if type == "leave": + # Stop waiting + del server_status["waiting"][user["gameid"]] + del user["gameid"] + user["action"] = "ready" + await asyncio.wait([ + ws.send(msgobj("left")), + notify_status() + ]) + if action == "loading": + if type == "gamestart": + user["action"] = "loaded" + if user["other_user"]["action"] == "loaded": + user["action"] = "playing" + user["other_user"]["action"] = "playing" + sent_msg = msgobj("gamestart") + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + elif action == "playing": + # Playing with another user + if "other_user" in user and "ws" in user["other_user"]: + if type == "note": + await user["other_user"]["ws"].send(msgobj("note", value)) + if type == "gameend": + # User wants to disconnect + user["action"] = "ready" + user["other_user"]["action"] = "ready" + sent_msg1 = msgobj("gameend") + sent_msg2 = status_event() + await asyncio.wait([ + ws.send(sent_msg1), + ws.send(sent_msg2), + user["other_user"]["ws"].send(sent_msg1), + user["other_user"]["ws"].send(sent_msg2) + ]) + del user["other_user"] + else: + # Other user disconnected + user["action"] = "ready" + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + finally: + # User disconnected + del user["ws"] + del users[users.index(user)] + if "other_user" in user and "ws" in user["other_user"]: + user["other_user"]["action"] = "ready" + await asyncio.wait([ + user["other_user"]["ws"].send(msgobj("gameend")), + user["other_user"]["ws"].send(status_event()) + ]) + if user["action"] == "waiting": + del server_status["waiting"][user["gameid"]] + await notify_status() + +asyncio.get_event_loop().run_until_complete( + websockets.serve(connection, "localhost", 34802) +) +asyncio.get_event_loop().run_forever()