Commit 426bc160 authored by LoveEevee's avatar LoveEevee

P2: Add multiplayer session

parent b85f879b
......@@ -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;
......
......@@ -117,7 +117,8 @@ var assets = {
"titlescreen.html",
"tutorial.html",
"about.html",
"debug.html"
"debug.html",
"session.html"
],
"songs": [],
......
......@@ -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()
......
......@@ -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))
})
......
......@@ -92,6 +92,8 @@ class loadSong{
}
}else if(event.type === "gamestart"){
this.clean()
p2.clearMessage("scorenext")
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)
......
......@@ -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()
......
......@@ -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()
......@@ -76,17 +78,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 +105,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 +126,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()
......
......@@ -33,6 +33,23 @@ class Scoresheet{
assets.sounds["results"].play()
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){
if(!code){
......@@ -68,16 +85,29 @@ 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.toScorenext()
}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)
this.state.screen = "fadeOut"
this.state.screenMS = ms
this.state.screenMS = this.getMS()
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{
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(){
......
<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 @@
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,12 +167,120 @@ 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"\
or type == "scorenext" and user["session"]:
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["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([
......@@ -147,6 +293,7 @@ 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())
......@@ -157,6 +304,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 +312,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)
......
......@@ -54,6 +54,7 @@
<script src="/src/js/parsetja.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/session.js?{{version.commit_short}}"></script>
</head>
<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