diff --git a/public/src/css/main.css b/public/src/css/main.css index 97efcda..4814eb7 100644 --- a/public/src/css/main.css +++ b/public/src/css/main.css @@ -214,6 +214,7 @@ kbd{ margin-bottom: 1em; background: #fff; border: 1px solid #a9a9a9; + user-select: all; } .text-warn{ color: #d00; @@ -226,6 +227,21 @@ kbd{ .nowrap{ white-space: nowrap; } +#session-invite{ + width: 100%; + height: 1.9em; + font-family: sans-serif; + font-size: 2em; + background: #fff; + border: 1px solid #a9a9a9; + padding: 0.3em; + margin: 0.3em 0; + box-sizing: border-box; + text-align: center; + user-select: all; + cursor: text; + overflow: hidden; +} @keyframes bgscroll{ from{ background-position: 0 top; diff --git a/public/src/js/assets.js b/public/src/js/assets.js index 3c903f6..c475f26 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -31,6 +31,7 @@ var assets = { "bg_genre_7.png", "bg_score_p1.png", "bg_score_p2.png", + "bg_settings.png", "badge_auto.png", "touch_drum.png", "touch_pause.png", @@ -117,7 +118,8 @@ var assets = { "titlescreen.html", "tutorial.html", "about.html", - "debug.html" + "debug.html", + "session.html" ], "songs": [], diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index 4ea8c5a..50cbbb9 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -188,6 +188,11 @@ ctx.restore() + if(config.disabled){ + ctx.fillStyle = "rgba(0, 0, 0, 0.5)" + ctx.fillRect(x, y, w, h) + } + if(config.highlight){ this.highlight({ ctx: ctx, diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 680afdb..4dc08bd 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -141,7 +141,7 @@ class Controller{ } displayResults(){ if(this.multiplayer !== 2){ - this.scoresheet = new Scoresheet(this, this.getGlobalScore(), this.multiplayer) + this.scoresheet = new Scoresheet(this, this.getGlobalScore(), this.multiplayer, this.touchEnabled) } } displayScore(score, notPlayed, bigNote){ diff --git a/public/src/js/game.js b/public/src/js/game.js index abde5a3..ce0aa55 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -294,7 +294,9 @@ class Game{ this.musicFadeOut++ }else if(this.musicFadeOut === 1 && ms >= started + 1600){ this.controller.gameEnded() - p2.send("gameend") + if(!p2.session){ + p2.send("gameend") + } this.musicFadeOut++ }else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= musicDuration + 250)){ this.controller.displayResults() diff --git a/public/src/js/loader.js b/public/src/js/loader.js index 673997e..af05129 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -4,7 +4,6 @@ class Loader{ this.loadedAssets = 0 this.assetsDiv = document.getElementById("assets") this.canvasTest = new CanvasTest() - p2 = new P2Connection() this.startTime = +new Date this.ajax("src/views/loader.html").then(this.run.bind(this)) @@ -97,6 +96,24 @@ class Loader{ } })) + if(location.hash.length === 6){ + this.promises.push(new Promise(resolve => { + p2.open() + pageEvents.add(p2, "message", response => { + if(response.type === "session"){ + resolve() + }else if(response.type === "gameend"){ + p2.hash("") + p2.hashLock = false + resolve() + } + }) + p2.send("invite", location.hash.slice(1).toLowerCase()) + }).then(() => { + pageEvents.remove(p2, "message") + })) + } + this.promises.forEach(promise => { promise.then(this.assetLoaded.bind(this)) }) diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index dc6c2d6..1321e81 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -92,6 +92,7 @@ class loadSong{ } }else if(event.type === "gamestart"){ this.clean() + p2.clearMessage("songsel") loader.changePage("game") var taikoGame1 = new Controller(this.selectedSong, this.songData, false, 1, this.touchEnabled) var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled) diff --git a/public/src/js/main.js b/public/src/js/main.js index 268f06c..87055af 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -58,7 +58,7 @@ var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscree var pageEvents = new PageEvents() var snd = {} -var p2 +var p2 = new P2Connection() var disableBlur = false var cancelTouch = true var lastHeight @@ -98,6 +98,11 @@ pageEvents.keyAdd(debugObj, "all", "down", event => { debugObj.controller.restartSong() } }) +if(location.hash.length === 6){ + p2.hashLock = true +}else{ + p2.hash("") +} var loader = new Loader(() => { new Titlescreen() diff --git a/public/src/js/p2.js b/public/src/js/p2.js index 1874da8..046c542 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -5,6 +5,8 @@ class P2Connection{ this.otherConnected = false this.allEvents = new Map() this.addEventListener("message", this.message.bind(this)) + this.currentHash = "" + pageEvents.add(window, "hashchange", this.onhashchange.bind(this)) } addEventListener(type, callback){ var addedType = this.allEvents.get(type) @@ -24,8 +26,8 @@ class P2Connection{ this.closed = false var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:" this.socket = new WebSocket(wsProtocol + "//" + location.host + "/p2") - pageEvents.race(this.socket, "open", "close", listener =>{ - if(listener === "open"){ + pageEvents.race(this.socket, "open", "close").then(response => { + if(response.type === "open"){ return this.openEvent() } return this.closeEvent() @@ -45,6 +47,11 @@ class P2Connection{ closeEvent(){ this.removeEventListener(onmessage) this.otherConnected = false + this.session = false + if(this.hashLock){ + this.hash("") + this.hashLock = false + } if(!this.closed){ setTimeout(() => { if(this.socket.readyState !== this.socket.OPEN){ @@ -76,17 +83,22 @@ class P2Connection{ }catch(e){ var response = {} } - this.lastMessages[response.type] = response.value + this.lastMessages[response.type] = response var addedType = this.allEvents.get("message") if(addedType){ addedType.forEach(callback => callback(response)) } } - getMessage(type, callback){ + getMessage(type){ if(type in this.lastMessages){ return this.lastMessages[type] } } + clearMessage(type){ + if(type in this.lastMessages){ + this.lastMessages[type] = null + } + } message(response){ switch(response.type){ case "gamestart": @@ -98,6 +110,11 @@ class P2Connection{ break case "gameend": this.otherConnected = false + this.session = false + if(this.hashLock){ + this.hash("") + this.hashLock = false + } break case "gameresults": this.results = {} @@ -114,8 +131,24 @@ class P2Connection{ case "drumroll": this.drumrollPace = response.value.pace break + case "session": + this.clearMessage("users") + this.otherConnected = true + this.session = true + break } } + onhashchange(){ + if(this.hashLock){ + this.hash(this.currentHash) + }else{ + location.reload() + } + } + hash(string){ + this.currentHash = string + history.replaceState("", "", location.pathname + (string ? "#" + string : "")) + } play(circle, mekadon){ if(this.otherConnected || this.notes.length > 0){ var type = circle.getType() diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index af27f1a..fe283db 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -1,11 +1,12 @@ class Scoresheet{ - constructor(controller, results, multiplayer){ + constructor(controller, results, multiplayer, touchEnabled){ this.controller = controller this.results = {} for(var i in results){ this.results[i] = results[i].toString() } this.multiplayer = multiplayer + this.touchEnabled = touchEnabled this.canvas = document.getElementById("canvas") this.ctx = this.canvas.getContext("2d") @@ -15,7 +16,8 @@ class Scoresheet{ screen: "fadeIn", screenMS: this.getMS(), startDelay: 3300, - hasPointer: 0 + hasPointer: 0, + scoreNext: false } this.frame = 1000 / 60 this.numbers = "001122334455667788900112233445".split("") @@ -33,6 +35,17 @@ class Scoresheet{ assets.sounds["results"].play() assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689) + + if(p2.session){ + if(p2.getMessage("songsel")){ + this.toSongsel(true) + } + pageEvents.add(p2, "message", response => { + if(response.type === "songsel"){ + this.toSongsel(true) + } + }) + } } keyDown(event, code){ if(!code){ @@ -68,17 +81,28 @@ class Scoresheet{ this.toNext() } toNext(){ - var ms = this.getMS() - var elapsed = ms - this.state.screenMS + var elapsed = this.getMS() - this.state.screenMS if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){ - this.state.screen = "scoresShown" - this.state.screenMS = ms - assets.sounds["note_don"].play() + this.toScoresShown() }else if(this.state.screen === "scoresShown" && elapsed >= 1000){ + this.toSongsel() + } + } + toScoresShown(){ + if(!p2.session){ + this.state.screen = "scoresShown" + this.state.screenMS = this.getMS() + assets.sounds["note_don"].play() + } + } + toSongsel(fromP2){ + if(!p2.session || fromP2){ snd.musicGain.fadeOut(0.5) this.state.screen = "fadeOut" - this.state.screenMS = ms - assets.sounds["note_don"].play() + this.state.screenMS = this.getMS() + if(!fromP2){ + assets.sounds["note_don"].play() + } } } @@ -232,7 +256,7 @@ class Scoresheet{ if(elapsed >= 0){ if(this.state.hasPointer === 0){ this.state.hasPointer = 1 - if(!this.state.pointerLocked){ + if(!this.state.pointerLocked && !p2.session){ this.canvas.style.cursor = "pointer" } } @@ -615,6 +639,11 @@ class Scoresheet{ ctx.restore() } + if(p2.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){ + this.state.scoreNext = true + p2.send("songsel") + } + if(this.state.screen === "fadeOut"){ ctx.save() if(this.state.hasPointer === 1){ @@ -631,7 +660,7 @@ class Scoresheet{ if(elapsed >= 1000){ this.clean() - this.controller.songSelection(true, false, this.state.pointerLocked) + this.controller.songSelection(true, false, p2.session ? this.touchEnabled : this.state.pointerLocked) } } diff --git a/public/src/js/session.js b/public/src/js/session.js new file mode 100644 index 0000000..baf2010 --- /dev/null +++ b/public/src/js/session.js @@ -0,0 +1,65 @@ +class Session{ + constructor(touchEnabled){ + this.touchEnabled = touchEnabled + loader.changePage("session") + this.endButton = document.getElementById("tutorial-end-button") + if(touchEnabled){ + document.getElementById("tutorial-outer").classList.add("touch-enabled") + } + this.sessionInvite = document.getElementById("session-invite") + + pageEvents.add(window, ["mousedown", "touchstart"], this.mouseDown.bind(this)) + pageEvents.keyOnce(this, 27, "down").then(this.onEnd.bind(this)) + + this.gamepad = new Gamepad({ + "confirm": ["start", "b", "ls", "rs"] + }, this.onEnd.bind(this)) + + p2.hashLock = true + pageEvents.add(p2, "message", response => { + if(response.type === "invite"){ + this.sessionInvite.innerText = location.origin + location.pathname + "#" + response.value + p2.hash(response.value) + }else if(response.type === "songsel"){ + p2.clearMessage("users") + this.onEnd(false, true) + } + }) + p2.send("invite") + } + mouseDown(event){ + if(event.target === this.sessionInvite){ + this.sessionInvite.focus() + }else{ + getSelection().removeAllRanges() + this.sessionInvite.blur() + } + if(event.target === this.endButton){ + this.onEnd() + } + } + onEnd(event, fromP2){ + if(!p2.session){ + p2.send("leave") + p2.hash("") + p2.hashLock = false + }else if(!fromP2){ + return p2.send("songsel") + } + if(event && event.type === "keydown"){ + event.preventDefault() + } + this.clean() + assets.sounds["don"].play() + setTimeout(() => { + new SongSelect(false, false, this.touchEnabled) + }, 500) + } + clean(){ + this.gamepad.clean() + pageEvents.remove(window, ["mousedown", "touchstart"]) + pageEvents.keyRemove(this, 27) + delete this.endButton + delete this.sessionInvite + } +} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 1d428ac..4697391 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -176,10 +176,16 @@ class SongSelect{ this.selectTextCache = new CanvasCache() this.categoryCache = new CanvasCache() this.difficultyCache = new CanvasCache() + this.sessionCache = new CanvasCache() this.difficulty = ["かんたん", "ふつう", "むずかしい", "おに"] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] + this.sessionText = { + "sessionstart": "オンラインセッションを開始する!", + "sessionend": "オンラインセッションを終了する" + } + this.selectedSong = 0 this.selectedDiff = 0 assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) @@ -187,12 +193,15 @@ class SongSelect{ if(!fromTutorial && !("selectedSong" in localStorage)){ fromTutorial = touchEnabled ? "about" : "tutorial" } + if(p2.session){ + fromTutorial = false + } if(fromTutorial){ this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial) this.playBgm(true) }else{ - if("selectedSong" in localStorage){ + if((!p2.session || fadeIn) && "selectedSong" in localStorage){ this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length) } assets.sounds["song-select"].play() @@ -209,8 +218,9 @@ class SongSelect{ this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')" this.previewId = 0 + var skipStart = fromTutorial || p2.session this.state = { - screen: fromTutorial ? "song" : (fadeIn ? "titleFadeIn" : "title"), + screen: fadeIn ? "titleFadeIn" : (skipStart ? "song" : "title"), screenMS: this.getMS(), move: 0, moveMS: 0, @@ -218,7 +228,8 @@ class SongSelect{ moveHover: null, locked: true, hasPointer: false, - options: 0 + options: 0, + selLock: false } this.songSelecting = { speed: 800, @@ -231,11 +242,12 @@ class SongSelect{ this.pressedKeys = {} this.gamepad = new Gamepad({ "13": ["b", "start", "ls", "rs"], - "8": ["a"], + "27": ["a"], "37": ["l", "lb", "lt", "lsl"], "39": ["r", "rb", "rt", "lsr"], "38": ["u", "lsu"], "40": ["d", "lsd"], + "8": ["back"], "ctrl": ["y"], "shift": ["x"] }) @@ -244,6 +256,9 @@ class SongSelect{ pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this)) pageEvents.add(loader.screen, "mousemove", this.mouseMove.bind(this)) + pageEvents.add(loader.screen, "mouseleave", () => { + this.state.moveHover = null + }) pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this)) if(touchEnabled && fullScreenSupported){ this.touchFullBtn = document.getElementById("touch-full-btn") @@ -279,8 +294,10 @@ class SongSelect{ var key = { confirm: code == 13 || code == 32 || code == 70 || code == 74, // Enter, Space, F, J - cancel: code == 27 || code == 8, - // Esc, Backspace + cancel: code == 27, + // Esc + session: code == 8, + // Backspace left: code == 37 || code == 68, // Left, D right: code == 39 || code == 75, @@ -298,6 +315,8 @@ class SongSelect{ this.toSelectDifficulty() }else if(key.cancel){ this.toTitleScreen() + }else if(key.session){ + this.toSession() }else if(key.left){ this.moveToSong(-1) }else if(key.right){ @@ -312,7 +331,7 @@ class SongSelect{ }else{ this.toLoadSong(this.selectedDiff - this.diffOptions.length, modifiers.shift, modifiers.ctrl) } - }else if(key.cancel){ + }else if(key.cancel || key.session){ this.toSongSelect() }else if(key.left){ this.moveToDiff(-1) @@ -350,11 +369,15 @@ class SongSelect{ var touch = true } if(this.state.screen === "song"){ - var moveBy = this.songSelMouse(mouse.x, mouse.y) - if(moveBy === 0){ - this.toSelectDifficulty() - }else if(moveBy !== null){ - this.moveToSong(moveBy) + if(mouse.x > 513 && mouse.y > 603){ + this.toSession() + }else{ + var moveBy = this.songSelMouse(mouse.x, mouse.y) + if(moveBy === 0){ + this.toSelectDifficulty() + }else if(moveBy !== null){ + this.moveToSong(moveBy) + } } }else if(this.state.screen === "difficulty"){ var moveBy = this.diffSelMouse(mouse.x, mouse.y) @@ -380,9 +403,13 @@ class SongSelect{ var mouse = this.mouseOffset(event.offsetX, event.offsetY) var moveTo = null if(this.state.screen === "song"){ - var moveTo = this.songSelMouse(mouse.x, mouse.y) - if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].stars){ - this.state.moveMS = this.getMS() - this.songSelecting.speed + if(mouse.x > 513 && mouse.y > 603){ + moveTo = "session" + }else{ + var moveTo = this.songSelMouse(mouse.x, mouse.y) + if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].stars){ + this.state.moveMS = this.getMS() - this.songSelecting.speed + } } this.state.moveHover = moveTo }else if(this.state.screen === "difficulty"){ @@ -446,10 +473,17 @@ class SongSelect{ return null } - moveToSong(moveBy){ - if(this.state.locked !== 1){ - var ms = this.getMS() - if(this.songs[this.selectedSong].stars && this.state.locked === 0){ + moveToSong(moveBy, fromP2){ + var ms = this.getMS() + if(p2.session && !fromP2){ + if(!this.state.selLock && ms > this.state.moveMS + 800){ + this.state.selLock = true + p2.send("songsel", { + song: this.mod(this.songs.length, this.selectedSong + moveBy) + }) + } + }else if(this.state.locked !== 1 || fromP2){ + if(this.songs[this.selectedSong].stars && (this.state.locked === 0 || fromP2)){ this.state.moveMS = ms }else{ this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize @@ -482,9 +516,19 @@ class SongSelect{ assets.sounds["ka"].play() } } - toSelectDifficulty(){ - if(this.state.locked === 0){ - var currentSong = this.songs[this.selectedSong] + toSelectDifficulty(fromP2){ + var currentSong = this.songs[this.selectedSong] + if(p2.session && !fromP2 && currentSong.action !== "random"){ + if(this.songs[this.selectedSong].stars){ + if(!this.state.selLock){ + this.state.selLock = true + p2.send("songsel", { + song: this.selectedSong, + selected: true + }) + } + } + }else if(this.state.locked === 0 || fromP2){ if(currentSong.stars){ this.state.screen = "difficulty" this.state.screenMS = this.getMS() @@ -516,11 +560,18 @@ class SongSelect{ }else if(currentSong.action === "about"){ this.toAbout() } - this.pointer(false) } + this.pointer(false) } - toSongSelect(){ - if(this.state.locked !== 1){ + toSongSelect(fromP2){ + if(p2.session && !fromP2){ + if(!this.state.selLock){ + this.state.selLock = true + p2.send("songsel", { + song: this.selectedSong + }) + } + }else if(fromP2 || this.state.locked !== 1){ this.state.screen = "song" this.state.screenMS = this.getMS() this.state.locked = true @@ -544,10 +595,10 @@ class SongSelect{ } var autoplay = false var multiplayer = false - if(this.state.options === 1){ - autoplay = true - }else if(this.state.options === 2){ + if(p2.session || this.state.options === 2){ multiplayer = true + }else if(this.state.options === 1){ + autoplay = true }else if(shift){ autoplay = shift }else{ @@ -564,16 +615,20 @@ class SongSelect{ }, autoplay, multiplayer, touch) } toOptions(moveBy){ - assets.sounds["ka"].play() - this.selectedDiff = 1 - this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) + if(!p2.session){ + assets.sounds["ka"].play() + this.selectedDiff = 1 + this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) + } } toTitleScreen(){ - assets.sounds["cancel"].play() - this.clean() - setTimeout(() => { - new Titlescreen() - }, 500) + if(!p2.session){ + assets.sounds["cancel"].play() + this.clean() + setTimeout(() => { + new Titlescreen() + }, 500) + } } toTutorial(){ assets.sounds["don"].play() @@ -589,6 +644,17 @@ class SongSelect{ new About(this.touchEnabled) }, 500) } + toSession(){ + if(p2.session){ + p2.send("gameend") + }else{ + assets.sounds["don"].play() + this.clean() + setTimeout(() => { + new Session(this.touchEnabled) + }, 500) + } + } redraw(){ if(!this.redrawRunning){ @@ -657,6 +723,28 @@ class SongSelect{ this.difficultyCache.resize((44 + 56 + 2) * 5, 135 + 10, ratio + 0.5) + var w = winW / ratio / 2 + this.sessionCache.resize(w, 39 * 2, ratio + 0.5) + for(var id in this.sessionText){ + this.sessionCache.set({ + w: w, + h: 38, + id: id + }, ctx => { + var text = this.sessionText[id] + ctx.font = "28px " + this.font + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.strokeStyle = "#000" + ctx.lineWidth = 8 + ctx.lineJoin = "round" + ctx.miterLimit = 1 + ctx.strokeText(text, w / 2, 38 / 2) + ctx.fillStyle = "#fff" + ctx.fillText(text, w / 2, 38 / 2) + }) + } + this.selectableText = "" }else if(!document.hasFocus()){ this.pointer(false) @@ -858,7 +946,8 @@ class SongSelect{ x: _x, y: songTop, song: this.songs[index], - highlight: highlight + highlight: highlight, + disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random" }) } for(var i = this.selectedSong + 1; ; i++){ @@ -877,7 +966,8 @@ class SongSelect{ x: _x, y: songTop, song: this.songs[index], - highlight: highlight + highlight: highlight, + disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random" }) } } @@ -920,6 +1010,7 @@ class SongSelect{ animateMS: this.state.moveMS, cached: selectedWidth === this.songAsset.fullWidth ? 3 : (selectedWidth === this.songAsset.selectedWidth ? 2 : (selectedWidth === this.songAsset.width ? 1 : 0)), frameCache: this.songFrameCache, + disabled: p2.session && currentSong.action && currentSong.action !== "random", innerContent: (x, y, w, h) => { ctx.strokeStyle = "#000" if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ @@ -1273,6 +1364,113 @@ class SongSelect{ }) } + ctx.fillStyle = "#000" + ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop) + var x = 0 + var y = frameTop + 603 + var w = frameLeft + 638 + var h = 117 + frameTop + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_score_p1"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 10, + dy: frameTop + 15, + scale: 1.55 + }) + ctx.fillStyle = "rgba(249, 163, 149, 0.5)" + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w - 4, y + 4) + ctx.lineTo(x, y + 4) + ctx.fill() + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.beginPath() + ctx.moveTo(x + w, y) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - 4, y + h) + ctx.lineTo(x + w - 4, y + 4) + ctx.fill() + x = frameLeft + 642 + if(p2.session){ + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_score_p2"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 15, + dy: frameTop - 20, + scale: 1.55 + }) + ctx.fillStyle = "rgba(138, 245, 247, 0.5)" + }else{ + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_settings"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 11, + dy: frameTop + 45, + scale: 3.1 + }) + ctx.fillStyle = "rgba(255, 255, 255, 0.5)" + } + ctx.beginPath() + ctx.moveTo(x, y + h) + ctx.lineTo(x, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + 4) + ctx.lineTo(x + 4, y + 4) + ctx.lineTo(x + 4, y + h) + ctx.fill() + if(screen !== "difficulty"){ + var elapsed = (ms - this.state.screenMS) % 3100 + var fade = 1 + if(!p2.session && screen === "song"){ + if(elapsed > 2800){ + fade = (elapsed - 2800) / 300 + }else if(2000 < elapsed){ + if(elapsed < 2300){ + fade = 1 - (elapsed - 2000) / 300 + }else{ + fade = 0 + } + } + } + if(fade > 0){ + if(fade < 1){ + ctx.globalAlpha = this.draw.easeIn(fade) + } + this.sessionCache.get({ + ctx: ctx, + x: winW / 2, + y: y + (h - 32) / 2, + w: winW / 2, + h: 38, + id: p2.session ? "sessionend" : "sessionstart" + }) + ctx.globalAlpha = 1 + } + if(this.state.moveHover === "session"){ + this.draw.highlight({ + ctx: ctx, + x: x, + y: y, + w: w, + h: h, + opacity: 0.8 + }) + } + } + if(screen === "titleFadeIn"){ ctx.save() @@ -1408,8 +1606,8 @@ class SongSelect{ this.songs.forEach(song => { song.p2Cursor = null }) - if(response){ - response.forEach(idDiff => { + if(response && response.value){ + response.value.forEach(idDiff => { var id = idDiff.id |0 var diff = idDiff.diff var diffId = this.difficultyId.indexOf(diff) @@ -1417,17 +1615,72 @@ class SongSelect{ diffId = 3 } if(diffId >= 0){ - var currentSong = this.songs.find(song => song.id === id) + var index = 0 + var currentSong = this.songs.find((song, i) => { + index = i + return song.id === id + }) currentSong.p2Cursor = diffId + if(currentSong.stars){ + this.selectedSong = index + this.state.move = 0 + if(this.state.screen !== "difficulty"){ + this.toSelectDifficulty(true) + } + } } }) } } + onsongsel(response){ + if(response && response.value){ + var selected = false + if("selected" in response.value){ + selected = response.value.selected + } + if("song" in response.value){ + var song = +response.value.song + if(song >= 0 && song < this.songs.length){ + if(!selected){ + this.state.locked = true + if(this.state.screen === "difficulty"){ + this.toSongSelect(true) + } + var moveBy = song - this.selectedSong + if(moveBy){ + if(this.selectedSong < song){ + var altMoveBy = -this.mod(this.songs.length, this.selectedSong - song) + }else{ + var altMoveBy = this.mod(this.songs.length, moveBy) + } + if(Math.abs(altMoveBy) < Math.abs(moveBy)){ + moveBy = altMoveBy + } + this.moveToSong(moveBy, true) + } + }else if(this.songs[song].stars){ + this.selectedSong = song + this.state.move = 0 + if(this.state.screen !== "difficulty"){ + this.toSelectDifficulty(true) + } + } + } + } + } + } startP2(){ this.onusers(p2.getMessage("users")) + if(p2.session){ + this.onsongsel(p2.getMessage("songsel")) + } pageEvents.add(p2, "message", response => { if(response.type == "users"){ - this.onusers(response.value) + this.onusers(response) + } + if(p2.session && response.type == "songsel"){ + this.onsongsel(response) + this.state.selLock = false } }) if(p2.closed){ @@ -1449,6 +1702,7 @@ class SongSelect{ this.selectTextCache.clean() this.categoryCache.clean() this.difficultyCache.clean() + this.sessionCache.clean() assets.sounds["bgm_songsel"].stop() if(!this.bgmEnabled){ snd.musicGain.fadeIn() @@ -1459,7 +1713,8 @@ class SongSelect{ this.redrawRunning = false this.endPreview() pageEvents.keyRemove(this, "all") - pageEvents.remove(loader.screen, ["mousemove", "mousedown", "touchstart"]) + pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"]) + pageEvents.remove(p2, "message") if(this.touchEnabled && fullScreenSupported){ pageEvents.remove(this.touchFullBtn, "click") delete this.touchFullBtn diff --git a/public/src/js/titlescreen.js b/public/src/js/titlescreen.js index fecea36..71e8c7e 100644 --- a/public/src/js/titlescreen.js +++ b/public/src/js/titlescreen.js @@ -12,6 +12,13 @@ class Titlescreen{ this.onPressed() } }) + if(p2.session){ + pageEvents.add(p2, "message", response => { + if(response.type === "songsel"){ + this.goNext(true) + } + }) + } } keyDown(event, code){ if(!code){ @@ -34,13 +41,20 @@ class Titlescreen{ this.titleScreen.style.cursor = "auto" this.clean() assets.sounds["don"].play() - setTimeout(this.goNext.bind(this), 500) + this.goNext() } - goNext(){ - if(this.touched || localStorage.getItem("tutorial") === "true"){ - new SongSelect(false, false, this.touched) + goNext(fromP2){ + if(p2.session && !fromP2){ + p2.send("songsel") + }else if(fromP2 || this.touched || localStorage.getItem("tutorial") === "true"){ + pageEvents.remove(p2, "message") + setTimeout(() => { + new SongSelect(false, false, this.touched) + }, 500) }else{ - new Tutorial() + setTimeout(() => { + new Tutorial() + }, 500) } } clean(){ diff --git a/public/src/views/session.html b/public/src/views/session.html new file mode 100644 index 0000000..2a6854b --- /dev/null +++ b/public/src/views/session.html @@ -0,0 +1,11 @@ +
+
+
Multiplayer Session
+
+ Share this link with your friend to start playing together! Do not leave this screen while they join. +
+
+ +
Cancel
+
+
diff --git a/server.py b/server.py index e38705f..9270c8f 100644 --- a/server.py +++ b/server.py @@ -3,11 +3,14 @@ import asyncio import websockets import json +import random server_status = { "waiting": {}, - "users": [] + "users": [], + "invites": {} } +consonants = "bcdfghjklmnpqrstvwxyz" def msgobj(type, value=None): if value == None: @@ -24,6 +27,9 @@ def status_event(): }) return msgobj("users", value) +def get_invite(): + return "".join([random.choice(consonants) for x in range(5)]) + async def notify_status(): ready_users = [user for user in server_status["users"] if "ws" in user and user["action"] == "ready"] if ready_users: @@ -34,7 +40,8 @@ async def connection(ws, path): # User connected user = { "ws": ws, - "action": "ready" + "action": "ready", + "session": False } server_status["users"].append(user) try: @@ -66,6 +73,8 @@ async def connection(ws, path): if action == "ready": # Not playing or waiting if type == "join": + if value == None: + continue waiting = server_status["waiting"] id = value["id"] if "id" in value else None diff = value["diff"] if "diff" in value else None @@ -95,6 +104,7 @@ async def connection(ws, path): ]) else: # Wait for another user + del user["other_user"] user["action"] = "waiting" user["gameid"] = id waiting[id] = { @@ -104,9 +114,37 @@ async def connection(ws, path): await ws.send(msgobj("waiting")) # Update others on waiting players await notify_status() + elif type == "invite": + if value == None: + # Session invite link requested + invite = get_invite() + server_status["invites"][invite] = user + user["action"] = "invite" + user["session"] = invite + await ws.send(msgobj("invite", invite)) + elif value in server_status["invites"]: + # Join a session with the other user + user["other_user"] = server_status["invites"][value] + del server_status["invites"][value] + if "ws" in user["other_user"]: + user["other_user"]["other_user"] = user + user["action"] = "invite" + user["session"] = value + sent_msg = msgobj("session") + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + await ws.send(msgobj("invite")) + else: + del user["other_user"] + await ws.send(msgobj("gameend")) + else: + # Session code is invalid + await ws.send(msgobj("gameend")) elif action == "waiting" or action == "loading" or action == "loaded": # Waiting for another user - if type == "leave": + if type == "leave" and not user["session"]: # Stop waiting del server_status["waiting"][user["gameid"]] del user["gameid"] @@ -129,9 +167,22 @@ async def connection(ws, path): elif action == "playing": # Playing with another user if "other_user" in user and "ws" in user["other_user"]: - if type == "note" or type == "drumroll" or type == "gameresults": + if type == "note"\ + or type == "drumroll"\ + or type == "gameresults": await user["other_user"]["ws"].send(msgobj(type, value)) - if type == "gameend": + elif type == "songsel" and user["session"]: + user["action"] = "songsel" + user["other_user"]["action"] = "songsel" + sent_msg1 = msgobj("songsel") + sent_msg2 = msgobj("users", []) + 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) + ]) + elif type == "gameend": # User wants to disconnect user["action"] = "ready" user["other_user"]["action"] = "ready" @@ -147,6 +198,101 @@ async def connection(ws, path): else: # Other user disconnected user["action"] = "ready" + user["session"] = False + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + elif action == "invite": + if type == "leave": + # Cancel session invite + if user["session"] in server_status["invites"]: + del server_status["invites"][user["session"]] + user["action"] = "ready" + user["session"] = False + if "other_user" in user and "ws" in user["other_user"]: + user["other_user"]["action"] = "ready" + user["other_user"]["session"] = False + sent_msg = status_event() + await asyncio.wait([ + ws.send(msgobj("left")), + ws.send(sent_msg), + user["other_user"]["ws"].send(msgobj("gameend")), + user["other_user"]["ws"].send(sent_msg) + ]) + else: + await asyncio.wait([ + ws.send(msgobj("left")), + ws.send(status_event()) + ]) + elif type == "songsel" and "other_user" in user: + if "ws" in user["other_user"]: + user["action"] = "songsel" + user["other_user"]["action"] = "songsel" + sent_msg = msgobj(type) + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + else: + user["action"] = "ready" + user["session"] = False + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + elif action == "songsel": + # Session song selection + if "other_user" in user and "ws" in user["other_user"]: + if type == "songsel": + # Change song select position + if user["other_user"]["action"] == "songsel": + sent_msg = msgobj(type, value) + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + elif type == "join": + # Start game + if value == None: + continue + 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 user["other_user"]["action"] == "waiting": + user["action"] = "loading" + user["other_user"]["action"] = "loading" + await asyncio.wait([ + ws.send(msgobj("gameload", user["other_user"]["gamediff"])), + user["other_user"]["ws"].send(msgobj("gameload", diff)) + ]) + else: + user["action"] = "waiting" + user["gamediff"] = diff + await user["other_user"]["ws"].send(msgobj("users", [{ + "id": id, + "diff": diff + }])) + elif type == "gameend": + # User wants to disconnect + user["action"] = "ready" + user["session"] = False + user["other_user"]["action"] = "ready" + user["other_user"]["session"] = False + 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" + user["session"] = False await asyncio.wait([ ws.send(msgobj("gameend")), ws.send(status_event()) @@ -157,6 +303,7 @@ async def connection(ws, path): del server_status["users"][server_status["users"].index(user)] if "other_user" in user and "ws" in user["other_user"]: user["other_user"]["action"] = "ready" + user["other_user"]["session"] = False await asyncio.wait([ user["other_user"]["ws"].send(msgobj("gameend")), user["other_user"]["ws"].send(status_event()) @@ -164,6 +311,8 @@ async def connection(ws, path): if user["action"] == "waiting": del server_status["waiting"][user["gameid"]] await notify_status() + elif user["action"] == "invite" and user["session"] in server_status["invites"]: + del server_status["invites"][user["session"]] asyncio.get_event_loop().run_until_complete( websockets.serve(connection, "localhost", 34802) diff --git a/templates/index.html b/templates/index.html index d21621a..58bad43 100644 --- a/templates/index.html +++ b/templates/index.html @@ -54,6 +54,7 @@ +