Commit 426bc160 authored by LoveEevee's avatar LoveEevee

P2: Add multiplayer session

parent b85f879b
...@@ -214,6 +214,7 @@ kbd{ ...@@ -214,6 +214,7 @@ kbd{
margin-bottom: 1em; margin-bottom: 1em;
background: #fff; background: #fff;
border: 1px solid #a9a9a9; border: 1px solid #a9a9a9;
user-select: all;
} }
.text-warn{ .text-warn{
color: #d00; color: #d00;
...@@ -226,6 +227,21 @@ kbd{ ...@@ -226,6 +227,21 @@ kbd{
.nowrap{ .nowrap{
white-space: 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{ @keyframes bgscroll{
from{ from{
background-position: 0 top; background-position: 0 top;
......
...@@ -117,7 +117,8 @@ var assets = { ...@@ -117,7 +117,8 @@ var assets = {
"titlescreen.html", "titlescreen.html",
"tutorial.html", "tutorial.html",
"about.html", "about.html",
"debug.html" "debug.html",
"session.html"
], ],
"songs": [], "songs": [],
......
...@@ -294,7 +294,9 @@ class Game{ ...@@ -294,7 +294,9 @@ class Game{
this.musicFadeOut++ this.musicFadeOut++
}else if(this.musicFadeOut === 1 && ms >= started + 1600){ }else if(this.musicFadeOut === 1 && ms >= started + 1600){
this.controller.gameEnded() this.controller.gameEnded()
p2.send("gameend") if(!p2.session){
p2.send("gameend")
}
this.musicFadeOut++ this.musicFadeOut++
}else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= musicDuration + 250)){ }else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= musicDuration + 250)){
this.controller.displayResults() this.controller.displayResults()
......
...@@ -4,7 +4,6 @@ class Loader{ ...@@ -4,7 +4,6 @@ class Loader{
this.loadedAssets = 0 this.loadedAssets = 0
this.assetsDiv = document.getElementById("assets") this.assetsDiv = document.getElementById("assets")
this.canvasTest = new CanvasTest() this.canvasTest = new CanvasTest()
p2 = new P2Connection()
this.startTime = +new Date this.startTime = +new Date
this.ajax("src/views/loader.html").then(this.run.bind(this)) this.ajax("src/views/loader.html").then(this.run.bind(this))
...@@ -97,6 +96,24 @@ class Loader{ ...@@ -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 => { this.promises.forEach(promise => {
promise.then(this.assetLoaded.bind(this)) promise.then(this.assetLoaded.bind(this))
}) })
......
...@@ -92,6 +92,8 @@ class loadSong{ ...@@ -92,6 +92,8 @@ class loadSong{
} }
}else if(event.type === "gamestart"){ }else if(event.type === "gamestart"){
this.clean() this.clean()
p2.clearMessage("scorenext")
p2.clearMessage("songsel")
loader.changePage("game") loader.changePage("game")
var taikoGame1 = new Controller(this.selectedSong, this.songData, false, 1, this.touchEnabled) var taikoGame1 = new Controller(this.selectedSong, this.songData, false, 1, this.touchEnabled)
var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled) var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled)
......
...@@ -58,7 +58,7 @@ var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscree ...@@ -58,7 +58,7 @@ var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscree
var pageEvents = new PageEvents() var pageEvents = new PageEvents()
var snd = {} var snd = {}
var p2 var p2 = new P2Connection()
var disableBlur = false var disableBlur = false
var cancelTouch = true var cancelTouch = true
var lastHeight var lastHeight
...@@ -98,6 +98,11 @@ pageEvents.keyAdd(debugObj, "all", "down", event => { ...@@ -98,6 +98,11 @@ pageEvents.keyAdd(debugObj, "all", "down", event => {
debugObj.controller.restartSong() debugObj.controller.restartSong()
} }
}) })
if(location.hash.length === 6){
p2.hashLock = true
}else{
p2.hash("")
}
var loader = new Loader(() => { var loader = new Loader(() => {
new Titlescreen() new Titlescreen()
......
...@@ -5,6 +5,8 @@ class P2Connection{ ...@@ -5,6 +5,8 @@ class P2Connection{
this.otherConnected = false this.otherConnected = false
this.allEvents = new Map() this.allEvents = new Map()
this.addEventListener("message", this.message.bind(this)) this.addEventListener("message", this.message.bind(this))
this.currentHash = ""
pageEvents.add(window, "hashchange", this.onhashchange.bind(this))
} }
addEventListener(type, callback){ addEventListener(type, callback){
var addedType = this.allEvents.get(type) var addedType = this.allEvents.get(type)
...@@ -24,8 +26,8 @@ class P2Connection{ ...@@ -24,8 +26,8 @@ class P2Connection{
this.closed = false this.closed = false
var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:" var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:"
this.socket = new WebSocket(wsProtocol + "//" + location.host + "/p2") this.socket = new WebSocket(wsProtocol + "//" + location.host + "/p2")
pageEvents.race(this.socket, "open", "close", listener =>{ pageEvents.race(this.socket, "open", "close").then(response => {
if(listener === "open"){ if(response.type === "open"){
return this.openEvent() return this.openEvent()
} }
return this.closeEvent() return this.closeEvent()
...@@ -76,17 +78,22 @@ class P2Connection{ ...@@ -76,17 +78,22 @@ class P2Connection{
}catch(e){ }catch(e){
var response = {} var response = {}
} }
this.lastMessages[response.type] = response.value this.lastMessages[response.type] = response
var addedType = this.allEvents.get("message") var addedType = this.allEvents.get("message")
if(addedType){ if(addedType){
addedType.forEach(callback => callback(response)) addedType.forEach(callback => callback(response))
} }
} }
getMessage(type, callback){ getMessage(type){
if(type in this.lastMessages){ if(type in this.lastMessages){
return this.lastMessages[type] return this.lastMessages[type]
} }
} }
clearMessage(type){
if(type in this.lastMessages){
this.lastMessages[type] = null
}
}
message(response){ message(response){
switch(response.type){ switch(response.type){
case "gamestart": case "gamestart":
...@@ -98,6 +105,11 @@ class P2Connection{ ...@@ -98,6 +105,11 @@ class P2Connection{
break break
case "gameend": case "gameend":
this.otherConnected = false this.otherConnected = false
this.session = false
if(this.hashLock){
this.hash("")
this.hashLock = false
}
break break
case "gameresults": case "gameresults":
this.results = {} this.results = {}
...@@ -114,8 +126,24 @@ class P2Connection{ ...@@ -114,8 +126,24 @@ class P2Connection{
case "drumroll": case "drumroll":
this.drumrollPace = response.value.pace this.drumrollPace = response.value.pace
break 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){ play(circle, mekadon){
if(this.otherConnected || this.notes.length > 0){ if(this.otherConnected || this.notes.length > 0){
var type = circle.getType() var type = circle.getType()
......
...@@ -33,6 +33,23 @@ class Scoresheet{ ...@@ -33,6 +33,23 @@ class Scoresheet{
assets.sounds["results"].play() assets.sounds["results"].play()
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689) assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
if(p2.session){
if(p2.getMessage("scorenext")){
this.toScorenext(true)
}
if(p2.getMessage("songsel")){
this.toSongsel(true)
}
pageEvents.add(p2, "message", response => {
if(response.type === "scorenext"){
this.toScorenext(true)
}else if(response.type === "songsel"){
this.state.pointerLocked = true
this.toSongsel(true)
}
})
}
} }
keyDown(event, code){ keyDown(event, code){
if(!code){ if(!code){
...@@ -68,16 +85,29 @@ class Scoresheet{ ...@@ -68,16 +85,29 @@ class Scoresheet{
this.toNext() this.toNext()
} }
toNext(){ toNext(){
var ms = this.getMS() var elapsed = this.getMS() - this.state.screenMS
var elapsed = ms - this.state.screenMS
if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){ if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){
this.state.screen = "scoresShown" this.toScorenext()
this.state.screenMS = ms
assets.sounds["note_don"].play()
}else if(this.state.screen === "scoresShown" && elapsed >= 1000){ }else if(this.state.screen === "scoresShown" && elapsed >= 1000){
this.toSongsel()
}
}
toScorenext(fromP2){
if(p2.session && !fromP2){
p2.send("scorenext")
}
this.state.screen = "scoresShown"
this.state.screenMS = this.getMS()
assets.sounds["note_don"].play()
}
toSongsel(fromP2){
if(p2.session && !fromP2){
this.state.pointerLocked = true
p2.send("songsel")
}else{
snd.musicGain.fadeOut(0.5) snd.musicGain.fadeOut(0.5)
this.state.screen = "fadeOut" this.state.screen = "fadeOut"
this.state.screenMS = ms this.state.screenMS = this.getMS()
assets.sounds["note_don"].play() assets.sounds["note_don"].play()
} }
} }
......
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
}
}
This diff is collapsed.
...@@ -12,6 +12,13 @@ class Titlescreen{ ...@@ -12,6 +12,13 @@ class Titlescreen{
this.onPressed() this.onPressed()
} }
}) })
if(p2.session){
pageEvents.add(p2, "message", response => {
if(response.type === "songsel"){
this.goNext(true)
}
})
}
} }
keyDown(event, code){ keyDown(event, code){
if(!code){ if(!code){
...@@ -34,13 +41,20 @@ class Titlescreen{ ...@@ -34,13 +41,20 @@ class Titlescreen{
this.titleScreen.style.cursor = "auto" this.titleScreen.style.cursor = "auto"
this.clean() this.clean()
assets.sounds["don"].play() assets.sounds["don"].play()
setTimeout(this.goNext.bind(this), 500) this.goNext()
} }
goNext(){ goNext(fromP2){
if(this.touched || localStorage.getItem("tutorial") === "true"){ if(p2.session && !fromP2){
new SongSelect(false, false, this.touched) 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{ }else{
new Tutorial() setTimeout(() => {
new Tutorial()
}, 500)
} }
} }
clean(){ clean(){
......
<div id="tutorial-outer">
<div id="tutorial">
<div id="tutorial-title" class="stroke-sub" alt="Multiplayer Session">Multiplayer Session</div>
<div id="tutorial-content">
Share this link with your friend to start playing together! Do not leave this screen while they join.
<div id="session-invite"></div>
</div>
<div id="tutorial-end-button" class="taibtn stroke-sub" alt="Cancel">Cancel</div>
</div>
</div>
...@@ -3,11 +3,14 @@ ...@@ -3,11 +3,14 @@
import asyncio import asyncio
import websockets import websockets
import json import json
import random
server_status = { server_status = {
"waiting": {}, "waiting": {},
"users": [] "users": [],
"invites": {}
} }
consonants = "bcdfghjklmnpqrstvwxyz"
def msgobj(type, value=None): def msgobj(type, value=None):
if value == None: if value == None:
...@@ -24,6 +27,9 @@ def status_event(): ...@@ -24,6 +27,9 @@ def status_event():
}) })
return msgobj("users", value) return msgobj("users", value)
def get_invite():
return "".join([random.choice(consonants) for x in range(5)])
async def notify_status(): async def notify_status():
ready_users = [user for user in server_status["users"] if "ws" in user and user["action"] == "ready"] ready_users = [user for user in server_status["users"] if "ws" in user and user["action"] == "ready"]
if ready_users: if ready_users:
...@@ -34,7 +40,8 @@ async def connection(ws, path): ...@@ -34,7 +40,8 @@ async def connection(ws, path):
# User connected # User connected
user = { user = {
"ws": ws, "ws": ws,
"action": "ready" "action": "ready",
"session": False
} }
server_status["users"].append(user) server_status["users"].append(user)
try: try:
...@@ -66,6 +73,8 @@ async def connection(ws, path): ...@@ -66,6 +73,8 @@ async def connection(ws, path):
if action == "ready": if action == "ready":
# Not playing or waiting # Not playing or waiting
if type == "join": if type == "join":
if value == None:
continue
waiting = server_status["waiting"] waiting = server_status["waiting"]
id = value["id"] if "id" in value else None id = value["id"] if "id" in value else None
diff = value["diff"] if "diff" in value else None diff = value["diff"] if "diff" in value else None
...@@ -95,6 +104,7 @@ async def connection(ws, path): ...@@ -95,6 +104,7 @@ async def connection(ws, path):
]) ])
else: else:
# Wait for another user # Wait for another user
del user["other_user"]
user["action"] = "waiting" user["action"] = "waiting"
user["gameid"] = id user["gameid"] = id
waiting[id] = { waiting[id] = {
...@@ -104,9 +114,37 @@ async def connection(ws, path): ...@@ -104,9 +114,37 @@ async def connection(ws, path):
await ws.send(msgobj("waiting")) await ws.send(msgobj("waiting"))
# Update others on waiting players # Update others on waiting players
await notify_status() 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": elif action == "waiting" or action == "loading" or action == "loaded":
# Waiting for another user # Waiting for another user
if type == "leave": if type == "leave" and not user["session"]:
# Stop waiting # Stop waiting
del server_status["waiting"][user["gameid"]] del server_status["waiting"][user["gameid"]]
del user["gameid"] del user["gameid"]
...@@ -129,12 +167,120 @@ async def connection(ws, path): ...@@ -129,12 +167,120 @@ async def connection(ws, path):
elif action == "playing": elif action == "playing":
# Playing with another user # Playing with another user
if "other_user" in user and "ws" in user["other_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"\
or type == "scorenext" and user["session"]:
await user["other_user"]["ws"].send(msgobj(type, value)) 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(type)
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"
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())
])
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 wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["session"] = False
user["other_user"]["action"] = "ready" user["other_user"]["action"] = "ready"
user["other_user"]["session"] = False
sent_msg1 = msgobj("gameend") sent_msg1 = msgobj("gameend")
sent_msg2 = status_event() sent_msg2 = status_event()
await asyncio.wait([ await asyncio.wait([
...@@ -147,6 +293,7 @@ async def connection(ws, path): ...@@ -147,6 +293,7 @@ async def connection(ws, path):
else: else:
# Other user disconnected # Other user disconnected
user["action"] = "ready" user["action"] = "ready"
user["session"] = False
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameend")), ws.send(msgobj("gameend")),
ws.send(status_event()) ws.send(status_event())
...@@ -157,6 +304,7 @@ async def connection(ws, path): ...@@ -157,6 +304,7 @@ async def connection(ws, path):
del server_status["users"][server_status["users"].index(user)] del server_status["users"][server_status["users"].index(user)]
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
user["other_user"]["action"] = "ready" user["other_user"]["action"] = "ready"
user["other_user"]["session"] = False
await asyncio.wait([ await asyncio.wait([
user["other_user"]["ws"].send(msgobj("gameend")), user["other_user"]["ws"].send(msgobj("gameend")),
user["other_user"]["ws"].send(status_event()) user["other_user"]["ws"].send(status_event())
...@@ -164,6 +312,8 @@ async def connection(ws, path): ...@@ -164,6 +312,8 @@ async def connection(ws, path):
if user["action"] == "waiting": if user["action"] == "waiting":
del server_status["waiting"][user["gameid"]] del server_status["waiting"][user["gameid"]]
await notify_status() 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( asyncio.get_event_loop().run_until_complete(
websockets.serve(connection, "localhost", 34802) websockets.serve(connection, "localhost", 34802)
......
...@@ -54,6 +54,7 @@ ...@@ -54,6 +54,7 @@
<script src="/src/js/parsetja.js?{{version.commit_short}}"></script> <script src="/src/js/parsetja.js?{{version.commit_short}}"></script>
<script src="/src/js/about.js?{{version.commit_short}}"></script> <script src="/src/js/about.js?{{version.commit_short}}"></script>
<script src="/src/js/debug.js?{{version.commit_short}}"></script> <script src="/src/js/debug.js?{{version.commit_short}}"></script>
<script src="/src/js/session.js?{{version.commit_short}}"></script>
</head> </head>
<body> <body>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment