Merge pull request #65 from LoveEevee/p2-add-multiplayer-session

P2: Add multiplayer session
This commit is contained in:
Bui 2018-11-02 11:16:41 +00:00 committed by GitHub
commit 29176e896e
16 changed files with 678 additions and 73 deletions

View File

@ -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{
.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;

View File

@ -31,6 +31,7 @@ var assets = {
"bg_genre_7.png", "bg_genre_7.png",
"bg_score_p1.png", "bg_score_p1.png",
"bg_score_p2.png", "bg_score_p2.png",
"bg_settings.png",
"badge_auto.png", "badge_auto.png",
"touch_drum.png", "touch_drum.png",
"touch_pause.png", "touch_pause.png",
@ -117,7 +118,8 @@ var assets = {
"titlescreen.html", "titlescreen.html",
"tutorial.html", "tutorial.html",
"about.html", "about.html",
"debug.html" "debug.html",
"session.html"
], ],
"songs": [], "songs": [],

View File

@ -188,6 +188,11 @@
ctx.restore() ctx.restore()
if(config.disabled){
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"
ctx.fillRect(x, y, w, h)
}
if(config.highlight){ if(config.highlight){
this.highlight({ this.highlight({
ctx: ctx, ctx: ctx,

View File

@ -141,7 +141,7 @@ class Controller{
} }
displayResults(){ displayResults(){
if(this.multiplayer !== 2){ 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){ displayScore(score, notPlayed, bigNote){

View File

@ -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()
if(!p2.session){
p2.send("gameend") 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()

View File

@ -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{
} }
})) }))
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))
}) })

View File

@ -92,6 +92,7 @@ class loadSong{
} }
}else if(event.type === "gamestart"){ }else if(event.type === "gamestart"){
this.clean() this.clean()
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)

View File

@ -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 => {
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()

View File

@ -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{
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()
@ -45,6 +47,11 @@ class P2Connection{
closeEvent(){ closeEvent(){
this.removeEventListener(onmessage) this.removeEventListener(onmessage)
this.otherConnected = false this.otherConnected = false
this.session = false
if(this.hashLock){
this.hash("")
this.hashLock = false
}
if(!this.closed){ if(!this.closed){
setTimeout(() => { setTimeout(() => {
if(this.socket.readyState !== this.socket.OPEN){ if(this.socket.readyState !== this.socket.OPEN){
@ -76,17 +83,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 +110,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 +131,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()

View File

@ -1,11 +1,12 @@
class Scoresheet{ class Scoresheet{
constructor(controller, results, multiplayer){ constructor(controller, results, multiplayer, touchEnabled){
this.controller = controller this.controller = controller
this.results = {} this.results = {}
for(var i in results){ for(var i in results){
this.results[i] = results[i].toString() this.results[i] = results[i].toString()
} }
this.multiplayer = multiplayer this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
this.canvas = document.getElementById("canvas") this.canvas = document.getElementById("canvas")
this.ctx = this.canvas.getContext("2d") this.ctx = this.canvas.getContext("2d")
@ -15,7 +16,8 @@ class Scoresheet{
screen: "fadeIn", screen: "fadeIn",
screenMS: this.getMS(), screenMS: this.getMS(),
startDelay: 3300, startDelay: 3300,
hasPointer: 0 hasPointer: 0,
scoreNext: false
} }
this.frame = 1000 / 60 this.frame = 1000 / 60
this.numbers = "001122334455667788900112233445".split("") this.numbers = "001122334455667788900112233445".split("")
@ -33,6 +35,17 @@ 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("songsel")){
this.toSongsel(true)
}
pageEvents.add(p2, "message", response => {
if(response.type === "songsel"){
this.toSongsel(true)
}
})
}
} }
keyDown(event, code){ keyDown(event, code){
if(!code){ if(!code){
@ -68,19 +81,30 @@ 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.toScoresShown()
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()
}
}
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) snd.musicGain.fadeOut(0.5)
this.state.screen = "fadeOut" this.state.screen = "fadeOut"
this.state.screenMS = ms this.state.screenMS = this.getMS()
if(!fromP2){
assets.sounds["note_don"].play() assets.sounds["note_don"].play()
} }
} }
}
startRedraw(){ startRedraw(){
this.redrawing = true this.redrawing = true
@ -232,7 +256,7 @@ class Scoresheet{
if(elapsed >= 0){ if(elapsed >= 0){
if(this.state.hasPointer === 0){ if(this.state.hasPointer === 0){
this.state.hasPointer = 1 this.state.hasPointer = 1
if(!this.state.pointerLocked){ if(!this.state.pointerLocked && !p2.session){
this.canvas.style.cursor = "pointer" this.canvas.style.cursor = "pointer"
} }
} }
@ -615,6 +639,11 @@ class Scoresheet{
ctx.restore() 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"){ if(this.state.screen === "fadeOut"){
ctx.save() ctx.save()
if(this.state.hasPointer === 1){ if(this.state.hasPointer === 1){
@ -631,7 +660,7 @@ class Scoresheet{
if(elapsed >= 1000){ if(elapsed >= 1000){
this.clean() this.clean()
this.controller.songSelection(true, false, this.state.pointerLocked) this.controller.songSelection(true, false, p2.session ? this.touchEnabled : this.state.pointerLocked)
} }
} }

65
public/src/js/session.js Normal file
View File

@ -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
}
}

View File

@ -176,10 +176,16 @@ class SongSelect{
this.selectTextCache = new CanvasCache() this.selectTextCache = new CanvasCache()
this.categoryCache = new CanvasCache() this.categoryCache = new CanvasCache()
this.difficultyCache = new CanvasCache() this.difficultyCache = new CanvasCache()
this.sessionCache = new CanvasCache()
this.difficulty = ["かんたん", "ふつう", "むずかしい", "おに"] this.difficulty = ["かんたん", "ふつう", "むずかしい", "おに"]
this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"]
this.sessionText = {
"sessionstart": "オンラインセッションを開始する!",
"sessionend": "オンラインセッションを終了する"
}
this.selectedSong = 0 this.selectedSong = 0
this.selectedDiff = 0 this.selectedDiff = 0
assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506)
@ -187,12 +193,15 @@ class SongSelect{
if(!fromTutorial && !("selectedSong" in localStorage)){ if(!fromTutorial && !("selectedSong" in localStorage)){
fromTutorial = touchEnabled ? "about" : "tutorial" fromTutorial = touchEnabled ? "about" : "tutorial"
} }
if(p2.session){
fromTutorial = false
}
if(fromTutorial){ if(fromTutorial){
this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial) this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial)
this.playBgm(true) this.playBgm(true)
}else{ }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) this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length)
} }
assets.sounds["song-select"].play() assets.sounds["song-select"].play()
@ -209,8 +218,9 @@ class SongSelect{
this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')" this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')"
this.previewId = 0 this.previewId = 0
var skipStart = fromTutorial || p2.session
this.state = { this.state = {
screen: fromTutorial ? "song" : (fadeIn ? "titleFadeIn" : "title"), screen: fadeIn ? "titleFadeIn" : (skipStart ? "song" : "title"),
screenMS: this.getMS(), screenMS: this.getMS(),
move: 0, move: 0,
moveMS: 0, moveMS: 0,
@ -218,7 +228,8 @@ class SongSelect{
moveHover: null, moveHover: null,
locked: true, locked: true,
hasPointer: false, hasPointer: false,
options: 0 options: 0,
selLock: false
} }
this.songSelecting = { this.songSelecting = {
speed: 800, speed: 800,
@ -231,11 +242,12 @@ class SongSelect{
this.pressedKeys = {} this.pressedKeys = {}
this.gamepad = new Gamepad({ this.gamepad = new Gamepad({
"13": ["b", "start", "ls", "rs"], "13": ["b", "start", "ls", "rs"],
"8": ["a"], "27": ["a"],
"37": ["l", "lb", "lt", "lsl"], "37": ["l", "lb", "lt", "lsl"],
"39": ["r", "rb", "rt", "lsr"], "39": ["r", "rb", "rt", "lsr"],
"38": ["u", "lsu"], "38": ["u", "lsu"],
"40": ["d", "lsd"], "40": ["d", "lsd"],
"8": ["back"],
"ctrl": ["y"], "ctrl": ["y"],
"shift": ["x"] "shift": ["x"]
}) })
@ -244,6 +256,9 @@ class SongSelect{
pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this)) pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this))
pageEvents.add(loader.screen, "mousemove", this.mouseMove.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)) pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this))
if(touchEnabled && fullScreenSupported){ if(touchEnabled && fullScreenSupported){
this.touchFullBtn = document.getElementById("touch-full-btn") this.touchFullBtn = document.getElementById("touch-full-btn")
@ -279,8 +294,10 @@ class SongSelect{
var key = { var key = {
confirm: code == 13 || code == 32 || code == 70 || code == 74, confirm: code == 13 || code == 32 || code == 70 || code == 74,
// Enter, Space, F, J // Enter, Space, F, J
cancel: code == 27 || code == 8, cancel: code == 27,
// Esc, Backspace // Esc
session: code == 8,
// Backspace
left: code == 37 || code == 68, left: code == 37 || code == 68,
// Left, D // Left, D
right: code == 39 || code == 75, right: code == 39 || code == 75,
@ -298,6 +315,8 @@ class SongSelect{
this.toSelectDifficulty() this.toSelectDifficulty()
}else if(key.cancel){ }else if(key.cancel){
this.toTitleScreen() this.toTitleScreen()
}else if(key.session){
this.toSession()
}else if(key.left){ }else if(key.left){
this.moveToSong(-1) this.moveToSong(-1)
}else if(key.right){ }else if(key.right){
@ -312,7 +331,7 @@ class SongSelect{
}else{ }else{
this.toLoadSong(this.selectedDiff - this.diffOptions.length, modifiers.shift, modifiers.ctrl) this.toLoadSong(this.selectedDiff - this.diffOptions.length, modifiers.shift, modifiers.ctrl)
} }
}else if(key.cancel){ }else if(key.cancel || key.session){
this.toSongSelect() this.toSongSelect()
}else if(key.left){ }else if(key.left){
this.moveToDiff(-1) this.moveToDiff(-1)
@ -350,12 +369,16 @@ class SongSelect{
var touch = true var touch = true
} }
if(this.state.screen === "song"){ if(this.state.screen === "song"){
if(mouse.x > 513 && mouse.y > 603){
this.toSession()
}else{
var moveBy = this.songSelMouse(mouse.x, mouse.y) var moveBy = this.songSelMouse(mouse.x, mouse.y)
if(moveBy === 0){ if(moveBy === 0){
this.toSelectDifficulty() this.toSelectDifficulty()
}else if(moveBy !== null){ }else if(moveBy !== null){
this.moveToSong(moveBy) this.moveToSong(moveBy)
} }
}
}else if(this.state.screen === "difficulty"){ }else if(this.state.screen === "difficulty"){
var moveBy = this.diffSelMouse(mouse.x, mouse.y) var moveBy = this.diffSelMouse(mouse.x, mouse.y)
if(mouse.x < 55 || mouse.x > 967 || mouse.y < 40 || mouse.y > 540){ if(mouse.x < 55 || mouse.x > 967 || mouse.y < 40 || mouse.y > 540){
@ -380,10 +403,14 @@ class SongSelect{
var mouse = this.mouseOffset(event.offsetX, event.offsetY) var mouse = this.mouseOffset(event.offsetX, event.offsetY)
var moveTo = null var moveTo = null
if(this.state.screen === "song"){ if(this.state.screen === "song"){
if(mouse.x > 513 && mouse.y > 603){
moveTo = "session"
}else{
var moveTo = this.songSelMouse(mouse.x, mouse.y) var moveTo = this.songSelMouse(mouse.x, mouse.y)
if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].stars){ if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].stars){
this.state.moveMS = this.getMS() - this.songSelecting.speed this.state.moveMS = this.getMS() - this.songSelecting.speed
} }
}
this.state.moveHover = moveTo this.state.moveHover = moveTo
}else if(this.state.screen === "difficulty"){ }else if(this.state.screen === "difficulty"){
var moveTo = this.diffSelMouse(mouse.x, mouse.y) var moveTo = this.diffSelMouse(mouse.x, mouse.y)
@ -446,10 +473,17 @@ class SongSelect{
return null return null
} }
moveToSong(moveBy){ moveToSong(moveBy, fromP2){
if(this.state.locked !== 1){
var ms = this.getMS() var ms = this.getMS()
if(this.songs[this.selectedSong].stars && this.state.locked === 0){ 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 this.state.moveMS = ms
}else{ }else{
this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize
@ -482,9 +516,19 @@ class SongSelect{
assets.sounds["ka"].play() assets.sounds["ka"].play()
} }
} }
toSelectDifficulty(){ toSelectDifficulty(fromP2){
if(this.state.locked === 0){
var currentSong = this.songs[this.selectedSong] 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){ if(currentSong.stars){
this.state.screen = "difficulty" this.state.screen = "difficulty"
this.state.screenMS = this.getMS() this.state.screenMS = this.getMS()
@ -516,11 +560,18 @@ class SongSelect{
}else if(currentSong.action === "about"){ }else if(currentSong.action === "about"){
this.toAbout() this.toAbout()
} }
}
this.pointer(false) this.pointer(false)
} }
toSongSelect(fromP2){
if(p2.session && !fromP2){
if(!this.state.selLock){
this.state.selLock = true
p2.send("songsel", {
song: this.selectedSong
})
} }
toSongSelect(){ }else if(fromP2 || this.state.locked !== 1){
if(this.state.locked !== 1){
this.state.screen = "song" this.state.screen = "song"
this.state.screenMS = this.getMS() this.state.screenMS = this.getMS()
this.state.locked = true this.state.locked = true
@ -544,10 +595,10 @@ class SongSelect{
} }
var autoplay = false var autoplay = false
var multiplayer = false var multiplayer = false
if(this.state.options === 1){ if(p2.session || this.state.options === 2){
autoplay = true
}else if(this.state.options === 2){
multiplayer = true multiplayer = true
}else if(this.state.options === 1){
autoplay = true
}else if(shift){ }else if(shift){
autoplay = shift autoplay = shift
}else{ }else{
@ -564,17 +615,21 @@ class SongSelect{
}, autoplay, multiplayer, touch) }, autoplay, multiplayer, touch)
} }
toOptions(moveBy){ toOptions(moveBy){
if(!p2.session){
assets.sounds["ka"].play() assets.sounds["ka"].play()
this.selectedDiff = 1 this.selectedDiff = 1
this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy)
} }
}
toTitleScreen(){ toTitleScreen(){
if(!p2.session){
assets.sounds["cancel"].play() assets.sounds["cancel"].play()
this.clean() this.clean()
setTimeout(() => { setTimeout(() => {
new Titlescreen() new Titlescreen()
}, 500) }, 500)
} }
}
toTutorial(){ toTutorial(){
assets.sounds["don"].play() assets.sounds["don"].play()
this.clean() this.clean()
@ -589,6 +644,17 @@ class SongSelect{
new About(this.touchEnabled) new About(this.touchEnabled)
}, 500) }, 500)
} }
toSession(){
if(p2.session){
p2.send("gameend")
}else{
assets.sounds["don"].play()
this.clean()
setTimeout(() => {
new Session(this.touchEnabled)
}, 500)
}
}
redraw(){ redraw(){
if(!this.redrawRunning){ if(!this.redrawRunning){
@ -657,6 +723,28 @@ class SongSelect{
this.difficultyCache.resize((44 + 56 + 2) * 5, 135 + 10, ratio + 0.5) 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 = "" this.selectableText = ""
}else if(!document.hasFocus()){ }else if(!document.hasFocus()){
this.pointer(false) this.pointer(false)
@ -858,7 +946,8 @@ class SongSelect{
x: _x, x: _x,
y: songTop, y: songTop,
song: this.songs[index], 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++){ for(var i = this.selectedSong + 1; ; i++){
@ -877,7 +966,8 @@ class SongSelect{
x: _x, x: _x,
y: songTop, y: songTop,
song: this.songs[index], 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, animateMS: this.state.moveMS,
cached: selectedWidth === this.songAsset.fullWidth ? 3 : (selectedWidth === this.songAsset.selectedWidth ? 2 : (selectedWidth === this.songAsset.width ? 1 : 0)), cached: selectedWidth === this.songAsset.fullWidth ? 3 : (selectedWidth === this.songAsset.selectedWidth ? 2 : (selectedWidth === this.songAsset.width ? 1 : 0)),
frameCache: this.songFrameCache, frameCache: this.songFrameCache,
disabled: p2.session && currentSong.action && currentSong.action !== "random",
innerContent: (x, y, w, h) => { innerContent: (x, y, w, h) => {
ctx.strokeStyle = "#000" ctx.strokeStyle = "#000"
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ 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"){ if(screen === "titleFadeIn"){
ctx.save() ctx.save()
@ -1408,8 +1606,8 @@ class SongSelect{
this.songs.forEach(song => { this.songs.forEach(song => {
song.p2Cursor = null song.p2Cursor = null
}) })
if(response){ if(response && response.value){
response.forEach(idDiff => { response.value.forEach(idDiff => {
var id = idDiff.id |0 var id = idDiff.id |0
var diff = idDiff.diff var diff = idDiff.diff
var diffId = this.difficultyId.indexOf(diff) var diffId = this.difficultyId.indexOf(diff)
@ -1417,17 +1615,72 @@ class SongSelect{
diffId = 3 diffId = 3
} }
if(diffId >= 0){ 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 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(){ startP2(){
this.onusers(p2.getMessage("users")) this.onusers(p2.getMessage("users"))
if(p2.session){
this.onsongsel(p2.getMessage("songsel"))
}
pageEvents.add(p2, "message", response => { pageEvents.add(p2, "message", response => {
if(response.type == "users"){ 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){ if(p2.closed){
@ -1449,6 +1702,7 @@ class SongSelect{
this.selectTextCache.clean() this.selectTextCache.clean()
this.categoryCache.clean() this.categoryCache.clean()
this.difficultyCache.clean() this.difficultyCache.clean()
this.sessionCache.clean()
assets.sounds["bgm_songsel"].stop() assets.sounds["bgm_songsel"].stop()
if(!this.bgmEnabled){ if(!this.bgmEnabled){
snd.musicGain.fadeIn() snd.musicGain.fadeIn()
@ -1459,7 +1713,8 @@ class SongSelect{
this.redrawRunning = false this.redrawRunning = false
this.endPreview() this.endPreview()
pageEvents.keyRemove(this, "all") 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){ if(this.touchEnabled && fullScreenSupported){
pageEvents.remove(this.touchFullBtn, "click") pageEvents.remove(this.touchFullBtn, "click")
delete this.touchFullBtn delete this.touchFullBtn

View File

@ -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{
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){
p2.send("songsel")
}else if(fromP2 || this.touched || localStorage.getItem("tutorial") === "true"){
pageEvents.remove(p2, "message")
setTimeout(() => {
new SongSelect(false, false, this.touched) new SongSelect(false, false, this.touched)
}, 500)
}else{ }else{
setTimeout(() => {
new Tutorial() new Tutorial()
}, 500)
} }
} }
clean(){ clean(){

View File

@ -0,0 +1,11 @@
<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>

159
server.py
View File

@ -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():
}) })
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):
# 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):
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):
]) ])
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):
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,9 +167,22 @@ 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":
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("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 wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["other_user"]["action"] = "ready" user["other_user"]["action"] = "ready"
@ -147,6 +198,101 @@ 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([
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([ await asyncio.wait([
ws.send(msgobj("gameend")), ws.send(msgobj("gameend")),
ws.send(status_event()) ws.send(status_event())
@ -157,6 +303,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 +311,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)

View File

@ -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>