Commit 8e99da6a authored by LoveEevee's avatar LoveEevee

Add 2-player mode

parent 7744fdac
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
<script src="/src/js/scalablecanvas.js"></script> <script src="/src/js/scalablecanvas.js"></script>
<script src="/src/js/element.js"></script> <script src="/src/js/element.js"></script>
<script src="/src/js/soundbuffer.js"></script> <script src="/src/js/soundbuffer.js"></script>
<script src="/src/js/p2.js"></script>
</head> </head>
<body> <body>
......
...@@ -75,6 +75,7 @@ ul li{ ...@@ -75,6 +75,7 @@ ul li{
box-shadow: 2px 2px 10px black; box-shadow: 2px 2px 10px black;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
position: relative;
} }
.opened{ .opened{
...@@ -127,3 +128,12 @@ ul li{ ...@@ -127,3 +128,12 @@ ul li{
.difficulty:hover .stars{ .difficulty:hover .stars{
color:white; color:white;
} }
.song.p2:not(.opened)::after,
.difficulty.p2::after{
content:"P2";
display:block;
position:absolute;
bottom:0;
width:100%;
}
This diff is collapsed.
This diff is collapsed.
...@@ -65,14 +65,21 @@ function Keyboard(controller){ ...@@ -65,14 +65,21 @@ function Keyboard(controller){
} }
this.checkMenuKeys = function(){ this.checkMenuKeys = function(){
_gamepad.play(1) if(!controller.multiplayer){
_this.checkKey(_kbd["back"], "menu", function(){ _gamepad.play(1)
controller.togglePause(); _this.checkKey(_kbd["pause"], "menu", function(){
controller.songSelection(); controller.togglePauseMenu();
}) })
_this.checkKey(_kbd["pause"], "menu", function(){ }
controller.togglePauseMenu(); if(controller.multiplayer != 2){
}) _this.checkKey(_kbd["back"], "menu", function(){
if(controller.multiplayer == 1){
p2.send("gameend")
}
controller.togglePause();
controller.songSelection();
})
}
} }
this.checkKey = function(keyCode, keyup, callback){ this.checkKey = function(keyCode, keyup, callback){
...@@ -85,7 +92,7 @@ function Keyboard(controller){ ...@@ -85,7 +92,7 @@ function Keyboard(controller){
this.checkKeySound = function(keyCode, sound){ this.checkKeySound = function(keyCode, sound){
_this.checkKey(keyCode, "sound", function(){ _this.checkKey(keyCode, "sound", function(){
assets.sounds["note_"+sound].play() assets.sounds["note_"+sound].play()
_keyTime[sound] = controller.getEllapsedTime().ms _keyTime[sound] = controller.getElapsedTime().ms
}) })
} }
......
...@@ -51,6 +51,8 @@ class Loader{ ...@@ -51,6 +51,8 @@ class Loader{
})) }))
}) })
p2 = new P2Connection()
this.promises.push(ajax("/api/songs").then(songs => { this.promises.push(ajax("/api/songs").then(songs => {
assets.songs = JSON.parse(songs) assets.songs = JSON.parse(songs)
})) }))
...@@ -100,4 +102,5 @@ function promiseLoad(asset){ ...@@ -100,4 +102,5 @@ function promiseLoad(asset){
} }
var snd = {} var snd = {}
var p2
new Loader() new Loader()
class loadSong{ class loadSong{
constructor(selectedSong, autoPlayEnabled){ constructor(selectedSong, autoPlayEnabled, multiplayer){
this.selectedSong = selectedSong this.selectedSong = selectedSong
this.multiplayer = multiplayer
this.autoPlayEnabled = autoPlayEnabled this.autoPlayEnabled = autoPlayEnabled
this.diff = this.selectedSong.difficulty.slice(0, -4)
this.songFilePath = "/songs/" + this.selectedSong.folder + "/" + this.selectedSong.difficulty this.songFilePath = "/songs/" + this.selectedSong.folder + "/" + this.selectedSong.difficulty
$("#screen").load("/src/views/loadsong.html", () => { $("#screen").load("/src/views/loadsong.html", () => {
this.run() this.run()
...@@ -42,11 +44,46 @@ class loadSong{ ...@@ -42,11 +44,46 @@ class loadSong{
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
$("#screen").load("/src/views/game.html", () => { $("#screen").load("/src/views/game.html", () => {
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled) this.setupMultiplayer()
taikoGame.run()
}) })
}, () => { }, error => {
console.error(error)
alert("An error occurred, please refresh") alert("An error occurred, please refresh")
}) })
} }
} setupMultiplayer(){
\ No newline at end of file if(this.multiplayer){
this.song2Data = this.songData
this.selectedSong2 = this.selectedSong
p2.onmessage("gamestart", () => {
var taikoGame1 = new Controller(this.selectedSong, this.songData, false, 1)
var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2)
taikoGame1.run(taikoGame2)
}, true)
p2.onmessage("gameload", response => {
if(response == this.diff){
p2.send("gamestart")
}else{
this.selectedSong2 = {
title: this.selectedSong.title,
folder: this.selectedSong.folder,
difficulty: response + ".osu"
}
ajax("/songs/" + this.selectedSong2.folder + "/" + this.selectedSong2.difficulty).then(data => {
this.song2Data = data.replace(/\0/g, "").split("\n")
p2.send("gamestart")
}, () => {
p2.send("gamestart")
})
}
}, true)
p2.send("join", {
id: this.selectedSong.folder,
diff: this.diff
})
}else{
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled)
taikoGame.run()
}
}
}
...@@ -7,27 +7,56 @@ class Mekadon{ ...@@ -7,27 +7,56 @@ class Mekadon{
} }
play(circle){ play(circle){
if(circle.getStatus() == 450){ if(circle.getStatus() == 450){
var kbd = this.controller.getBindings() return this.playNow(circle)
if(circle.getType() == "don"){
this.setKey(this.lr ? kbd["don_l"] : kbd["don_r"])
this.lr = !this.lr
}else if(circle.getType() == "daiDon"){
this.setKey(kbd["don_l"])
this.setKey(kbd["don_r"])
this.lr = false
}else if(circle.getType() == "ka"){
this.setKey(this.lr ? kbd["ka_l"] : kbd["ka_r"])
this.lr = !this.lr
}else if(circle.getType() == "daiKa"){
this.setKey(kbd["ka_l"])
this.setKey(kbd["ka_r"])
this.lr = false
}
var score = this.game.checkScore(circle);
circle.played(score);
this.game.updateCurrentCircle();
} }
} }
playAt(circle, ms, score){
var currentMs = circle.getMS() - this.controller.getElapsedTime().ms
if(ms > currentMs - 10){
return this.playNow(circle, score)
}
}
miss(circle){
var currentMs = circle.getMS() - this.controller.getElapsedTime().ms
if(0 > currentMs - 10){
circle.updateStatus(-1)
circle.played(0)
this.controller.displayScore(0, true)
this.game.updateCurrentCircle()
this.game.updateCombo(0)
this.game.updateGlobalScore(0)
return true
}
}
playNow(circle, score){
var kbd = this.controller.getBindings()
if(circle.getType() == "don"){
this.setKey(this.lr ? kbd["don_l"] : kbd["don_r"])
this.lr = !this.lr
}else if(circle.getType() == "daiDon"){
this.setKey(kbd["don_l"])
this.setKey(kbd["don_r"])
this.lr = false
}else if(circle.getType() == "ka"){
this.setKey(this.lr ? kbd["ka_l"] : kbd["ka_r"])
this.lr = !this.lr
}else if(circle.getType() == "daiKa"){
this.setKey(kbd["ka_l"])
this.setKey(kbd["ka_r"])
this.lr = false
}
if(typeof score == "undefined"){
score = this.game.checkScore(circle)
}else{
this.controller.displayScore(score)
this.game.updateCombo(score)
this.game.updateGlobalScore(score)
}
circle.updateStatus(score)
circle.played(score)
this.game.updateCurrentCircle()
return true
}
setKey(keyCode){ setKey(keyCode){
var self = this var self = this
if(this.keys[keyCode]){ if(this.keys[keyCode]){
......
class P2Connection{
constructor(){
this.closed = true
this.lastMessages = {}
this.msgCallbacks = {}
this.closeCallbacks = new Set()
this.openCallbacks = new Set()
this.notes = []
this.otherConnected = false
this.onmessage("gamestart", () => {
this.otherConnected = true
this.notes = []
})
this.onmessage("gameend", () => {
this.otherConnected = false
})
this.onmessage("note", response => {
this.notes.push(response)
})
}
open(){
this.closed = false
var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:"
this.socket = new WebSocket(wsProtocol + "//" + window.location.hostname + "/p2")
var events = ["open", "close", "message"]
events.forEach(eventName => {
this.socket.addEventListener(eventName, event => {
this[eventName + "Event"](event)
})
})
}
openEvent(event){
this.openCallbacks.forEach(obj => {
obj.callback()
if(obj.once){
this.openCallbacks.delete(obj)
}
})
}
onopen(callback, once){
this.openCallbacks.add({
callback: callback,
once: once
})
}
close(){
this.closed = true
this.socket.close()
}
closeEvent(event){
if(!this.closed){
setTimeout(() => {
if(this.socket.readyState != this.socket.OPEN){
this.open()
}
}, 500)
}
this.closeCallbacks.forEach(obj => {
obj.callback()
if(obj.once){
this.closeCallbacks.delete(obj)
}
})
}
onclose(callback, once){
this.closeCallbacks.add({
callback: callback,
once: once
})
}
send(type, value){
if(this.socket.readyState == this.socket.OPEN){
if(typeof value == "undefined"){
this.socket.send(JSON.stringify({type: type}))
}else{
this.socket.send(JSON.stringify({type: type, value: value}))
}
}else{
this.onopen(() => {
this.send(type, value)
}, true)
}
}
messageEvent(event){
try{
var data = JSON.parse(event.data)
}catch(e){
var data = {}
}
this.lastMessages[data.type] = data.value
if(this.msgCallbacks[data.type]){
this.msgCallbacks[data.type].forEach(obj => {
obj.callback(data.value)
if(obj.once){
delete this.msgCallbacks[obj]
}
})
}
}
onmessage(type, callback, once){
if(!(type in this.msgCallbacks)){
this.msgCallbacks[type] = new Set()
}
this.msgCallbacks[type].add({
callback: callback,
once: once
})
}
getMessage(type, callback){
if(type in this.lastMessages){
callback(this.lastMessages[type])
}
}
play(circle, mekadon){
if(this.otherConnected){
if(this.notes.length == 0){
mekadon.play(circle)
}else{
var note = this.notes[0]
if(note.score >= 0){
if(mekadon.playAt(circle, note.ms, note.score)){
this.notes.shift()
}
}else{
if(mekadon.miss(circle)){
this.notes.shift()
}
}
}
}
}
}
...@@ -56,6 +56,7 @@ function SongSelect(){ ...@@ -56,6 +56,7 @@ function SongSelect(){
this.run = function(){ this.run = function(){
_this.createCode(); _this.createCode();
_this.startP2();
$("#song-container").show(); $("#song-container").show();
...@@ -84,7 +85,7 @@ function SongSelect(){ ...@@ -84,7 +85,7 @@ function SongSelect(){
_selectedSong.folder = songID; _selectedSong.folder = songID;
snd.musicGain.fadeIn() snd.musicGain.fadeIn()
new loadSong(_selectedSong, e.shiftKey); new loadSong(_selectedSong, e.shiftKey, e.ctrlKey);
}); });
$(".song").hover(function(){ $(".song").hover(function(){
...@@ -236,6 +237,41 @@ function SongSelect(){ ...@@ -236,6 +237,41 @@ function SongSelect(){
$('.difficulty').hide(); $('.difficulty').hide();
} }
this.onusers = function(response){
var oldP2Elements = document.getElementsByClassName("p2")
for(var i = oldP2Elements.length; i--;){
oldP2Elements[i].classList.remove("p2")
}
if(response){
response.forEach(idDiff => {
id = idDiff.id |0
diff = idDiff.diff
if(diff in _diffNames){
var idElement = document.getElementById("song-" + id)
if(idElement){
idElement.classList.add("p2")
var diffElement = idElement.getElementsByClassName("difficulty " + diff)[0]
if(diffElement){
diffElement.classList.add("p2")
}
}
}
})
}
}
this.startP2 = function(){
p2.getMessage("users", response =>{
this.onusers(response)
})
p2.onmessage("users", response => {
this.onusers(response)
})
if(p2.closed){
p2.open()
}
}
$("#screen").load("/src/views/songselect.html", _this.run); $("#screen").load("/src/views/songselect.html", _this.run);
} }
\ No newline at end of file
...@@ -4,7 +4,14 @@ class View{ ...@@ -4,7 +4,14 @@ class View{
this.bg = bg this.bg = bg
this.diff = diff this.diff = diff
this.canvas = new ScalableCanvas("canvas", $(window).width(), $(window).height()) if(this.controller.multiplayer == 2){
this.canvas = new ScalableCanvas("canvas-p2", $(window).width(), $(window).height() / 3 * 2)
this.canvas.canvas.style.position = "absolute"
this.canvas.canvas.style.top = "33%"
document.getElementById("game").appendChild(this.canvas.canvas)
}else{
this.canvas = new ScalableCanvas("canvas", $(window).width(), $(window).height())
}
this.winW = this.canvas.scaledWidth this.winW = this.canvas.scaledWidth
this.winH = this.canvas.scaledHeight this.winH = this.canvas.scaledHeight
this.ctx = this.canvas.ctx this.ctx = this.canvas.ctx
...@@ -50,9 +57,16 @@ class View{ ...@@ -50,9 +57,16 @@ class View{
positionning(){ positionning(){
this.canvas.rescale() this.canvas.rescale()
this.canvas.resize($(window).width(), $(window).height()) var height = $(window).height()
if(this.controller.multiplayer == 2){
height = height / 3 * 2
}
this.canvas.resize($(window).width(), height)
this.winW = this.canvas.scaledWidth this.winW = this.canvas.scaledWidth
this.winH = this.canvas.scaledHeight this.winH = this.canvas.scaledHeight
if(this.controller.multiplayer == 2){
this.winH = this.winH / 2 * 3
}
this.barY = 0.25 * this.winH this.barY = 0.25 * this.winH
this.barH = 0.23 * this.winH this.barH = 0.23 * this.winH
...@@ -113,7 +127,7 @@ class View{ ...@@ -113,7 +127,7 @@ class View{
} }
updateDonFaces(){ updateDonFaces(){
if(this.controller.getEllapsedTime().ms >= this.nextBeat){ if(this.controller.getElapsedTime().ms >= this.nextBeat){
this.nextBeat += this.controller.getSongData().beatInfo.beatInterval this.nextBeat += this.controller.getSongData().beatInfo.beatInterval
if(this.controller.getCombo() >= 50){ if(this.controller.getCombo() >= 50){
this.currentBigDonFace = (this.currentBigDonFace + 1) % 2 this.currentBigDonFace = (this.currentBigDonFace + 1) % 2
...@@ -190,7 +204,7 @@ class View{ ...@@ -190,7 +204,7 @@ class View{
drawMeasures(){ drawMeasures(){
var measures = this.controller.getSongData().measures var measures = this.controller.getSongData().measures
var currentTime = this.controller.getEllapsedTime().ms var currentTime = this.controller.getElapsedTime().ms
measures.forEach((measure, index)=>{ measures.forEach((measure, index)=>{
var timeForDistance = 70 / this.circleSize * this.distanceForCircle / measure.speed var timeForDistance = 70 / this.circleSize * this.distanceForCircle / measure.speed
...@@ -206,7 +220,7 @@ class View{ ...@@ -206,7 +220,7 @@ class View{
drawMeasure(measure){ drawMeasure(measure){
var z = this.canvas.scale var z = this.canvas.scale
var currentTime = this.controller.getEllapsedTime().ms var currentTime = this.controller.getElapsedTime().ms
var measureX = this.slotX + measure.speed / (70 / this.circleSize) * (measure.ms - currentTime) var measureX = this.slotX + measure.speed / (70 / this.circleSize) * (measure.ms - currentTime)
this.ctx.strokeStyle = "#bab8b8" this.ctx.strokeStyle = "#bab8b8"
this.ctx.lineWidth = 2 this.ctx.lineWidth = 2
...@@ -388,7 +402,7 @@ class View{ ...@@ -388,7 +402,7 @@ class View{
for(var i = circles.length; i--;){ for(var i = circles.length; i--;){
var circle = circles[i] var circle = circles[i]
var currentTime = this.controller.getEllapsedTime().ms var currentTime = this.controller.getElapsedTime().ms
var timeForDistance = 70 / this.circleSize * this.distanceForCircle / circle.getSpeed() + this.bigCircleSize / 2 var timeForDistance = 70 / this.circleSize * this.distanceForCircle / circle.getSpeed() + this.bigCircleSize / 2
var startingTime = circle.getMS() - timeForDistance var startingTime = circle.getMS() - timeForDistance
// At circle.getMS(), the cirlce fits the slot // At circle.getMS(), the cirlce fits the slot
...@@ -459,7 +473,7 @@ class View{ ...@@ -459,7 +473,7 @@ class View{
var fill, size, faceID var fill, size, faceID
if(!circlePos){ if(!circlePos){
var currentTime = this.controller.getEllapsedTime().ms var currentTime = this.controller.getElapsedTime().ms
circlePos = { circlePos = {
x: this.slotX + circle.getSpeed() / (70 / this.circleSize) * (circle.getMS() - currentTime), x: this.slotX + circle.getSpeed() / (70 / this.circleSize) * (circle.getMS() - currentTime),
y: this.circleY y: this.circleY
...@@ -549,7 +563,7 @@ class View{ ...@@ -549,7 +563,7 @@ class View{
drawTime(){ drawTime(){
var z = this.canvas.scale var z = this.canvas.scale
var time = this.controller.getEllapsedTime() var time = this.controller.getElapsedTime()
this.ctx.globalAlpha = 0.7 this.ctx.globalAlpha = 0.7
this.ctx.fillStyle = "#000" this.ctx.fillStyle = "#000"
...@@ -579,7 +593,7 @@ class View{ ...@@ -579,7 +593,7 @@ class View{
this.ctx.closePath() this.ctx.closePath()
this.ctx.fill() this.ctx.fill()
var currentTime = this.controller.getEllapsedTime().ms var currentTime = this.controller.getElapsedTime().ms
var keyTime = this.controller.getKeyTime() var keyTime = this.controller.getKeyTime()
var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka" var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka"
if(keyTime[sound] > currentTime - 200){ if(keyTime[sound] > currentTime - 200){
......
#!/usr/bin/env python
import asyncio
import websockets
import json
users = []
server_status = {
"waiting": {}
}
def msgobj(type, value=None):
if value == None:
return json.dumps({"type": type})
else:
return json.dumps({"type": type, "value": value})
def status_event():
value = []
for id, userDiff in server_status["waiting"].items():
value.append({
"id": id,
"diff": userDiff["diff"]
})
return msgobj("users", value)
async def notify_status():
ready_users = [user for user in users if "ws" in user and user["action"] == "ready"]
if ready_users:
sent_msg = status_event()
await asyncio.wait([user["ws"].send(sent_msg) for user in ready_users])
async def connection(ws, path):
# User connected
user = {
"ws": ws,
"action": "ready"
}
users.append(user)
try:
# Notify user about other users
await ws.send(status_event())
while True:
try:
message = await asyncio.wait_for(ws.recv(), timeout=5)
except asyncio.TimeoutError:
# Keep user connected
pong_waiter = await ws.ping()
try:
await asyncio.wait_for(pong_waiter, timeout=5)
except asyncio.TimeoutError:
# Disconnect
break
else:
# Message received
try:
data = json.loads(message)
except json.decoder.JSONDecodeError:
data = {}
action = user["action"]
type = data["type"] if "type" in data else None
value = data["value"] if "value" in data else None
if action == "ready":
# Not playing or waiting
if type == "join":
waiting = server_status["waiting"]
id = value["id"] if "id" in value else None
diff = value["diff"] if "diff" in value else None
if not id or not diff:
continue
if id not in waiting:
# Wait for another user
user["action"] = "waiting"
user["gameid"] = id
waiting[id] = {
"user": user,
"diff": diff
}
await ws.send(msgobj("waiting"))
else:
# Join the other user and start game
user["other_user"] = waiting[id]["user"]
waiting_diff = waiting[id]["diff"]
del waiting[id]
if "ws" in user["other_user"]:
user["action"] = "loading"
user["other_user"]["action"] = "loading"
user["other_user"]["other_user"] = user
await asyncio.wait([
ws.send(msgobj("gameload", waiting_diff)),
user["other_user"]["ws"].send(msgobj("gameload", diff))
])
else:
# Wait for another user
user["action"] = "waiting"
user["gameid"] = id
waiting[id] = {
"user": user,
"diff": diff
}
await ws.send(msgobj("waiting"))
# Update others on waiting players
await notify_status()
elif action == "waiting" or action == "loading" or action == "loaded":
# Waiting for another user
if type == "leave":
# Stop waiting
del server_status["waiting"][user["gameid"]]
del user["gameid"]
user["action"] = "ready"
await asyncio.wait([
ws.send(msgobj("left")),
notify_status()
])
if action == "loading":
if type == "gamestart":
user["action"] = "loaded"
if user["other_user"]["action"] == "loaded":
user["action"] = "playing"
user["other_user"]["action"] = "playing"
sent_msg = msgobj("gamestart")
await asyncio.wait([
ws.send(sent_msg),
user["other_user"]["ws"].send(sent_msg)
])
elif action == "playing":
# Playing with another user
if "other_user" in user and "ws" in user["other_user"]:
if type == "note":
await user["other_user"]["ws"].send(msgobj("note", value))
if type == "gameend":
# User wants to disconnect
user["action"] = "ready"
user["other_user"]["action"] = "ready"
sent_msg1 = msgobj("gameend")
sent_msg2 = status_event()
await asyncio.wait([
ws.send(sent_msg1),
ws.send(sent_msg2),
user["other_user"]["ws"].send(sent_msg1),
user["other_user"]["ws"].send(sent_msg2)
])
del user["other_user"]
else:
# Other user disconnected
user["action"] = "ready"
await asyncio.wait([
ws.send(msgobj("gameend")),
ws.send(status_event())
])
finally:
# User disconnected
del user["ws"]
del users[users.index(user)]
if "other_user" in user and "ws" in user["other_user"]:
user["other_user"]["action"] = "ready"
await asyncio.wait([
user["other_user"]["ws"].send(msgobj("gameend")),
user["other_user"]["ws"].send(status_event())
])
if user["action"] == "waiting":
del server_status["waiting"][user["gameid"]]
await notify_status()
asyncio.get_event_loop().run_until_complete(
websockets.serve(connection, "localhost", 34802)
)
asyncio.get_event_loop().run_forever()
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