Commit d910de6b authored by Bui's avatar Bui

Merge branch 'add-lyrics'

parents 47545e9d 967d882a
...@@ -36,6 +36,7 @@ $RECYCLE.BIN/ ...@@ -36,6 +36,7 @@ $RECYCLE.BIN/
.Trashes .Trashes
.vscode .vscode
*.pyc
# Directories potentially created on remote AFP share # Directories potentially created on remote AFP share
.AppleDB .AppleDB
...@@ -48,5 +49,5 @@ public/api ...@@ -48,5 +49,5 @@ public/api
taiko.db taiko.db
version.json version.json
public/index.html public/index.html
config.json config.py
public/assets/song_skins public/assets/song_skins
This diff is collapsed.
{
"songs_baseurl": "",
"assets_baseurl": ""
}
# The full URL base asset URL, with trailing slash.
ASSETS_BASEURL = ''
# The full URL base song URL, with trailing slash.
SONGS_BASEURL = ''
# The email address to display in the "About Simulator" menu.
EMAIL = 'taiko@example.com'
# Whether to use the user account system.
ACCOUNTS = True
# Custom JavaScript file to load with the simulator.
CUSTOM_JS = ''
# MongoDB server settings.
MONGO = {
'host': ['127.0.0.1:27017'],
'database': 'taiko'
}
# Redis server settings, used for sessions + cache.
REDIS = {
'CACHE_TYPE': 'redis',
'CACHE_REDIS_HOST': '127.0.0.1',
'CACHE_REDIS_PORT': 6379,
'CACHE_REDIS_PASSWORD': None,
'CACHE_REDIS_DB': None
}
# Secret key used for sessions.
SECRET_KEY = 'change-me'
# Git repository base URL.
URL = 'https://github.com/bui/taiko-web/'
body {
margin: 0;
font-family: 'Noto Sans JP', sans-serif;
background: #FF7F00;
}
.nav {
margin: 0;
padding: 0;
width: 200px;
background-color: #A01300;
position: fixed;
height: 100%;
overflow: auto;
}
.nav a {
display: block;
color: #FFF;
padding: 16px;
text-decoration: none;
}
.nav a.active {
background-color: #4CAF50;
color: white;
}
.nav a:hover:not(.active) {
background-color: #555;
color: white;
}
main {
margin-left: 200px;
padding: 1px 16px;
height: 1000px;
}
@media screen and (max-width: 700px) {
.nav {
width: 100%;
height: auto;
position: relative;
}
.nav a {float: left;}
main {margin-left: 0;}
}
@media screen and (max-width: 400px) {
.sidebar a {
text-align: center;
float: none;
}
}
.container {
margin-bottom: 40px;
}
.song {
background: #F84828;
color: white;
padding: 10px;
font-size: 14pt;
margin: 10px 0;
}
.song p {
margin: 0;
}
.song-link {
text-decoration: none;
}
.song-form {
background: #ff5333;
color: #FFF;
padding: 20px;
}
.form-field {
background: #555555;
padding: 15px 20px 20px 20px;
margin-bottom: 20px;
}
.form-field p {
margin: 0;
font-size: 18pt;
}
.form-field > label {
display: block;
}
.form-field input {
margin: 5px 0;
}
.form-field input[type="text"] {
width: 300px;
}
.form-field input[type="number"] {
width: 50px;
}
h1 small {
color: #4a4a4a;
}
.form-field-indent {
margin-left: 20px;
}
.checkbox {
display: inline-block;
}
.checkbox input {
margin-right: 3px;
margin-left: 5px;
}
.message {
background: #2c862f;
padding: 15px;
margin-bottom: 10px;
color: white;
}
.message-error {
background: #b92222;
}
.save-song {
font-size: 22pt;
width: 120px;
}
.delete-song button {
float: right;
margin-top: -25px;
font-size: 12pt;
}
.side-button {
float: right;
background: green;
padding: 5px 20px;
color: white;
text-decoration: none;
margin-top: 25px;
}
...@@ -123,6 +123,7 @@ ...@@ -123,6 +123,7 @@
} }
#debug .autoplay-label, #debug .autoplay-label,
#debug .branch-hide{ #debug .branch-hide,
#debug .lyrics-hide{
display: none; display: none;
} }
...@@ -89,3 +89,39 @@ ...@@ -89,3 +89,39 @@
.fix-animations *{ .fix-animations *{
animation: none !important; animation: none !important;
} }
#song-lyrics{
position: absolute;
right: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale));
bottom: calc(44 / 720 * 100vh - 30px * var(--scale));
left: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale));
text-align: center;
font-family: Meiryo, sans-serif;
font-weight: bold;
font-size: calc(45px * var(--scale));
line-height: 1.2;
white-space: pre-wrap;
}
#game.portrait #song-lyrics{
right: calc(20px * var(--scale));
left: calc(20px * var(--scale));
}
#song-lyrics .stroke,
#song-lyrics .fill{
position: absolute;
right: 0;
bottom: 0;
left: 0;
}
#song-lyrics .stroke{
-webkit-text-stroke: calc(7px * var(--scale)) #00a;
}
#song-lyrics .fill{
color: #fff;
}
#song-lyrics ruby{
display: inline-flex;
flex-direction: column-reverse;
}
#song-lyrics rt{
line-height: 1;
}
...@@ -117,3 +117,20 @@ body{ ...@@ -117,3 +117,20 @@ body{
color: #777; color: #777;
text-shadow: 0.05em 0.05em #fff; text-shadow: 0.05em 0.05em #fff;
} }
.view-outer.loader-error-div,
.loader-error-div .diag-txt{
display: none
}
.loader-error-div{
font-family: sans-serif;
}
.loader-error-div .debug-link{
color: #00f;
text-decoration: underline;
cursor: pointer;
float: right;
}
.loader-error-div .diag-txt textarea,
.loader-error-div .diag-txt iframe{
height: 10em;
}
...@@ -108,8 +108,8 @@ kbd{ ...@@ -108,8 +108,8 @@ kbd{
.left-buttons .taibtn{ .left-buttons .taibtn{
margin-right: 0.4em; margin-right: 0.4em;
} }
#diag-txt textarea, .diag-txt textarea,
#diag-txt iframe{ .diag-txt iframe{
width: 100%; width: 100%;
height: 5em; height: 5em;
font-size: inherit; font-size: inherit;
...@@ -119,6 +119,7 @@ kbd{ ...@@ -119,6 +119,7 @@ kbd{
background: #fff; background: #fff;
border: 1px solid #a9a9a9; border: 1px solid #a9a9a9;
user-select: all; user-select: all;
box-sizing: border-box;
} }
.text-warn{ .text-warn{
color: #d00; color: #d00;
...@@ -291,3 +292,88 @@ kbd{ ...@@ -291,3 +292,88 @@ kbd{
.left-buttons .taibtn{ .left-buttons .taibtn{
z-index: 1; z-index: 1;
} }
.accountpass-form,
.accountdel-form,
.login-form{
text-align: center;
width: 80%;
margin: auto;
}
.accountpass-form .accountpass-div,
.accountdel-form .accountdel-div,
.login-form .password2-div{
display: none;
}
.account-view .displayname,
.accountpass-form input[type=password],
.accountdel-form input[type=password],
.login-form input[type=text],
.login-form input[type=password]{
width: 100%;
font-size: 1.4em;
margin: 0.1em 0;
padding: 0.3em;
box-sizing: border-box;
}
.accountpass-form input[type=password]{
width: calc(100% / 3);
}
.accountpass-form input[type=password]::placeholder{
font-size: 0.8em;
}
.login-form input[type=checkbox]{
transform: scale(1.4);
}
.account-view .displayname-hint,
.login-form .username-hint,
.login-form .password-hint,
.login-form .remember-label{
display: block;
font-size: 1.1em;
padding: 0.5em;
}
.login-form .remember-label{
padding: 0.85em;
}
.account-view .save-btn{
float: right;
padding: 0.4em 1.5em;
font-weight: bold;
border-color: #000;
color: #000;
z-index: 1;
}
.account-view .view-end-button{
margin-right: 0.4em;
font-weight: normal;
border-color: #dacdb2;
color: #555;
}
.account-view .save-btn:hover,
.account-view .save-btn.selected,
.account-view .view-end-button:hover,
.account-view .view-end-button.selected{
color: #fff;
border-color: #fff;
}
.account-view .displayname-div{
width: 80%;
margin: 0 auto;
}
.accountpass-form .accountpass-btn,
.accountdel-form .accountdel-btn,
.login-form .login-btn{
z-index: 1;
}
.accountpass-form,
.accountdel-form{
margin: 0.3em auto;
}
.view-content .error-div{
display: none;
width: 80%;
margin: 0 auto;
padding: 0.5em;
font-size: 1.1em;
color: #d00;
}
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
cancelTouch = false cancelTouch = false
this.endButton = this.getElement("view-end-button") this.endButton = this.getElement("view-end-button")
this.diagTxt = document.getElementById("diag-txt") this.diagTxt = this.getElement("diag-txt")
this.version = document.getElementById("version-link").href this.version = document.getElementById("version-link").href
this.tutorialOuter = this.getElement("view-outer") this.tutorialOuter = this.getElement("view-outer")
if(touchEnabled){ if(touchEnabled){
......
This diff is collapsed.
...@@ -31,7 +31,9 @@ var assets = { ...@@ -31,7 +31,9 @@ var assets = {
"importsongs.js", "importsongs.js",
"logo.js", "logo.js",
"settings.js", "settings.js",
"scorestorage.js" "scorestorage.js",
"account.js",
"lyrics.js"
], ],
"css": [ "css": [
"main.css", "main.css",
...@@ -86,11 +88,7 @@ var assets = { ...@@ -86,11 +88,7 @@ var assets = {
"settings_gamepad.png" "settings_gamepad.png"
], ],
"audioSfx": [ "audioSfx": [
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_pause.wav", "se_pause.wav",
"se_jump.wav",
"se_calibration.wav", "se_calibration.wav",
"v_results.wav", "v_results.wav",
...@@ -102,6 +100,10 @@ var assets = { ...@@ -102,6 +100,10 @@ var assets = {
"audioSfxLR": [ "audioSfxLR": [
"neiro_1_don.wav", "neiro_1_don.wav",
"neiro_1_ka.wav", "neiro_1_ka.wav",
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_jump.wav",
"se_balloon.wav", "se_balloon.wav",
"se_gameclear.wav", "se_gameclear.wav",
...@@ -137,7 +139,9 @@ var assets = { ...@@ -137,7 +139,9 @@ var assets = {
"about.html", "about.html",
"debug.html", "debug.html",
"session.html", "session.html",
"settings.html" "settings.html",
"account.html",
"login.html"
], ],
"songs": [], "songs": [],
......
...@@ -706,12 +706,12 @@ ...@@ -706,12 +706,12 @@
}) })
}else if(r.smallHiragana.test(symbol)){ }else if(r.smallHiragana.test(symbol)){
// Small hiragana, small katakana // Small hiragana, small katakana
drawn.push({text: symbol, x: 0, y: 0, w: 30}) drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 30})
}else if(r.hiragana.test(symbol)){ }else if(r.hiragana.test(symbol)){
// Hiragana, katakana // Hiragana, katakana
drawn.push({text: symbol, x: 0, y: 0, w: 35}) drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 35})
}else{ }else{
drawn.push({text: symbol, x: 0, y: 0, w: 39}) drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 39})
} }
} }
...@@ -720,6 +720,9 @@ ...@@ -720,6 +720,9 @@
if(config.letterSpacing){ if(config.letterSpacing){
symbol.w += config.letterSpacing symbol.w += config.letterSpacing
} }
if(config.kanaSpacing && symbol.kana){
symbol.w += config.kanaSpacing
}
drawnWidth += symbol.w * mul drawnWidth += symbol.w * mul
} }
...@@ -924,8 +927,22 @@ ...@@ -924,8 +927,22 @@
} }
} }
} }
var search = () => {
var end = line.length
var dist = end
while(dist){
dist >>= 1
line = words[i].slice(0, end)
lastWidth = ctx.measureText(line).width
end += lastWidth < config.width ? dist : -dist
}
if(line !== words[i]){
words.splice(i + 1, 0, words[i].slice(line.length))
words[i] = line
}
}
for(var i in words){ for(var i = 0; i < words.length; i++){
var skip = words[i].substitute || words[i] === "\n" var skip = words[i].substitute || words[i] === "\n"
if(!skip){ if(!skip){
var currentWidth = ctx.measureText(line + words[i]).width var currentWidth = ctx.measureText(line + words[i]).width
...@@ -957,8 +974,22 @@ ...@@ -957,8 +974,22 @@
recenter() recenter()
x = 0 x = 0
y += lineHeight y += lineHeight
line = words[i] === "\n" ? "" : words[i] if(words[i] === "\n"){
lastWidth = ctx.measureText(line).width line = ""
lastWidth = 0
}else{
line = words[i]
lastWidth = ctx.measureText(line).width
if(line.length !== 1 && lastWidth > config.width){
search()
}
}
}
}else if(!line){
line = words[i]
lastWidth = ctx.measureText(line).width
if(line.length !== 1 && lastWidth > config.width){
search()
} }
}else{ }else{
line += words[i] line += words[i]
...@@ -1549,6 +1580,99 @@ ...@@ -1549,6 +1580,99 @@
ctx.restore() ctx.restore()
} }
nameplate(config){
var ctx = config.ctx
var w = 264
var h = 57
var r = h / 2
var pi = Math.PI
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale, config.scale)
}
ctx.fillStyle="rgba(0, 0, 0, 0.25)"
ctx.beginPath()
ctx.arc(r + 4, r + 5, r, pi / 2, pi / -2)
ctx.arc(w - r + 4, r + 5, r, pi / -2, pi / 2)
ctx.fill()
ctx.beginPath()
ctx.moveTo(r, 0)
this.roundedCorner(ctx, w, 0, r, 1)
ctx.lineTo(r, r)
ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d"
ctx.fill()
ctx.beginPath()
ctx.moveTo(r, r)
this.roundedCorner(ctx, w, h, r, 2)
ctx.lineTo(r, h)
ctx.fillStyle = "rgba(255, 255, 255, 0.8)"
ctx.fill()
ctx.strokeStyle = "#000"
ctx.lineWidth = 4
ctx.beginPath()
ctx.moveTo(r, 0)
ctx.arc(w - r, r, r, pi / -2, pi / 2)
ctx.lineTo(r, h)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(r, r - 1)
ctx.lineTo(w, r - 1)
ctx.lineWidth = 2
ctx.stroke()
ctx.beginPath()
ctx.arc(r, r, r, 0, pi * 2)
ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d"
ctx.fill()
ctx.lineWidth = 4
ctx.stroke()
ctx.font = this.bold(config.font) + "28px " + config.font
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.lineWidth = 5
ctx.miterLimit = 1
ctx.strokeStyle = "#fff"
ctx.fillStyle = "#000"
var text = config.blue ? "2P" : "1P"
ctx.strokeText(text, r + 2, r + 1)
ctx.fillText(text, r + 2, r + 1)
if(config.rank){
this.layeredText({
ctx: ctx,
text: config.rank,
fontSize: 20,
fontFamily: config.font,
x: w / 2 + r * 0.7,
y: r * 0.5,
width: 180,
align: "center",
baseline: "middle"
}, [
{fill: "#000"}
])
}
this.layeredText({
ctx: ctx,
text: config.name || "",
fontSize: 21,
fontFamily: config.font,
x: w / 2 + r * 0.7,
y: r * 1.5 - 0.5,
width: 180,
kanaSpacing: 10,
align: "center",
baseline: "middle"
}, [
{outline: "#000", letterBorder: 6},
{fill: "#fff"}
])
ctx.restore()
}
alpha(amount, ctx, callback, winW, winH){ alpha(amount, ctx, callback, winW, winH){
if(amount >= 1){ if(amount >= 1){
return callback(ctx) return callback(ctx)
......
...@@ -6,7 +6,11 @@ class Controller{ ...@@ -6,7 +6,11 @@ class Controller{
this.saveScore = !autoPlayEnabled this.saveScore = !autoPlayEnabled
this.multiplayer = multiplayer this.multiplayer = multiplayer
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
this.snd = this.multiplayer ? "_p" + this.multiplayer : "" if(multiplayer === 2){
this.snd = p2.player === 2 ? "_p1" : "_p2"
}else{
this.snd = multiplayer ? "_p" + p2.player : ""
}
this.calibrationMode = selectedSong.folder === "calibration" this.calibrationMode = selectedSong.folder === "calibration"
this.audioLatency = 0 this.audioLatency = 0
...@@ -53,6 +57,15 @@ class Controller{ ...@@ -53,6 +57,15 @@ class Controller{
if(song.id == this.selectedSong.folder){ if(song.id == this.selectedSong.folder){
this.mainAsset = song.sound this.mainAsset = song.sound
this.volume = song.volume || 1 this.volume = song.volume || 1
if(!multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
if(song.lyricsData){
var lyricsDiv = document.getElementById("song-lyrics")
this.lyrics = new Lyrics(song.lyricsData, selectedSong.offset, lyricsDiv)
}else if(this.parsedSongData.lyrics){
var lyricsDiv = document.getElementById("song-lyrics")
this.lyrics = new Lyrics(this.parsedSongData.lyrics, selectedSong.offset, lyricsDiv, true)
}
}
} }
}) })
} }
...@@ -155,10 +168,16 @@ class Controller{ ...@@ -155,10 +168,16 @@ class Controller{
if(this.mainLoopRunning){ if(this.mainLoopRunning){
if(this.multiplayer !== 2){ if(this.multiplayer !== 2){
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.viewLoop() var player = this.multiplayer ? p2.player : 1
if(player === 1){
this.viewLoop()
}
if(this.multiplayer === 1){ if(this.multiplayer === 1){
this.syncWith.viewLoop() this.syncWith.viewLoop()
} }
if(player === 2){
this.viewLoop()
}
if(this.scoresheet){ if(this.scoresheet){
if(this.view.ctx){ if(this.view.ctx){
this.view.ctx.save() this.view.ctx.save()
...@@ -197,14 +216,14 @@ class Controller{ ...@@ -197,14 +216,14 @@ class Controller{
displayScore(score, notPlayed, bigNote){ displayScore(score, notPlayed, bigNote){
this.view.displayScore(score, notPlayed, bigNote) this.view.displayScore(score, notPlayed, bigNote)
} }
songSelection(fadeIn){ songSelection(fadeIn, showWarning){
if(!fadeIn){ if(!fadeIn){
this.clean() this.clean()
} }
if(this.calibrationMode){ if(this.calibrationMode){
new SettingsView(this.touchEnabled, false, null, "latency") new SettingsView(this.touchEnabled, false, null, "latency")
}else{ }else{
new SongSelect(false, fadeIn, this.touchEnabled) new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning)
} }
} }
restartSong(){ restartSong(){
...@@ -217,20 +236,27 @@ class Controller{ ...@@ -217,20 +236,27 @@ class Controller{
resolve() resolve()
}else{ }else{
var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) var songObj = assets.songs.find(song => song.id === this.selectedSong.folder)
var promises = []
if(songObj.chart && songObj.chart !== "blank"){ if(songObj.chart && songObj.chart !== "blank"){
var reader = new FileReader() var reader = new FileReader()
var promise = pageEvents.load(reader).then(event => { promises.push(pageEvents.load(reader).then(event => {
this.songData = event.target.result.replace(/\0/g, "").split("\n") this.songData = event.target.result.replace(/\0/g, "").split("\n")
resolve() return Promise.resolve()
}) }))
if(this.selectedSong.type === "tja"){ if(this.selectedSong.type === "tja"){
reader.readAsText(songObj.chart, "sjis") reader.readAsText(songObj.chart, "sjis")
}else{ }else{
reader.readAsText(songObj.chart) reader.readAsText(songObj.chart)
} }
}else{
resolve()
} }
if(songObj.lyricsFile){
var reader = new FileReader()
promises.push(pageEvents.load(reader).then(event => {
songObj.lyricsData = event.target.result
}, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath)
reader.readAsText(songObj.lyricsFile)
}
Promise.all(promises).then(resolve)
} }
}).then(() => { }).then(() => {
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
...@@ -306,5 +332,8 @@ class Controller{ ...@@ -306,5 +332,8 @@ class Controller{
debugObj.debug.updateStatus() debugObj.debug.updateStatus()
} }
} }
if(this.lyrics){
this.lyrics.clean()
}
} }
} }
...@@ -17,6 +17,8 @@ class Debug{ ...@@ -17,6 +17,8 @@ class Debug{
this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0] this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0]
this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0] this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0]
this.volumeDiv = this.byClass("music-volume") this.volumeDiv = this.byClass("music-volume")
this.lyricsHideDiv = this.byClass("lyrics-hide")
this.lyricsOffsetDiv = this.byClass("lyrics-offset")
this.restartLabel = this.byClass("change-restart-label") this.restartLabel = this.byClass("change-restart-label")
this.restartCheckbox = this.byClass("change-restart") this.restartCheckbox = this.byClass("change-restart")
this.autoplayLabel = this.byClass("autoplay-label") this.autoplayLabel = this.byClass("autoplay-label")
...@@ -50,6 +52,9 @@ class Debug{ ...@@ -50,6 +52,9 @@ class Debug{
this.volumeSlider.onchange(this.volumeChange.bind(this)) this.volumeSlider.onchange(this.volumeChange.bind(this))
this.volumeSlider.set(1) this.volumeSlider.set(1)
this.lyricsSlider = new InputSlider(this.lyricsOffsetDiv, -60, 60, 3)
this.lyricsSlider.onchange(this.lyricsChange.bind(this))
this.moveTo(100, 100) this.moveTo(100, 100)
this.restore() this.restore()
this.updateStatus() this.updateStatus()
...@@ -129,6 +134,9 @@ class Debug{ ...@@ -129,6 +134,9 @@ class Debug{
if(this.controller.parsedSongData.branches){ if(this.controller.parsedSongData.branches){
this.branchHideDiv.style.display = "block" this.branchHideDiv.style.display = "block"
} }
if(this.controller.lyrics){
this.lyricsHideDiv.style.display = "block"
}
var selectedSong = this.controller.selectedSong var selectedSong = this.controller.selectedSong
this.defaultOffset = selectedSong.offset || 0 this.defaultOffset = selectedSong.offset || 0
...@@ -136,19 +144,21 @@ class Debug{ ...@@ -136,19 +144,21 @@ class Debug{
this.offsetChange(this.offsetSlider.get(), true) this.offsetChange(this.offsetSlider.get(), true)
this.branchChange(null, true) this.branchChange(null, true)
this.volumeChange(this.volumeSlider.get(), true) this.volumeChange(this.volumeSlider.get(), true)
this.lyricsChange(this.lyricsSlider.get(), true)
}else{ }else{
this.songHash = selectedSong.hash this.songHash = selectedSong.hash
this.offsetSlider.set(this.defaultOffset) this.offsetSlider.set(this.defaultOffset)
this.branchReset(null, true) this.branchReset(null, true)
this.volumeSlider.set(this.controller.volume) this.volumeSlider.set(this.controller.volume)
this.lyricsSlider.set(this.controller.lyrics ? this.controller.lyrics.vttOffset / 1000 : 0)
} }
var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => { var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => {
return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01 return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01
}) })
this.measureNumSlider.setMinMax(0, measures.length - 1) this.measureNumSlider.setMinMax(0, measures.length - 1)
if(this.measureNum && measures.length > this.measureNum){ if(this.measureNum > 0 && measures.length >= this.measureNum){
var measureMS = measures[this.measureNum].ms var measureMS = measures[this.measureNum - 1].ms
var game = this.controller.game var game = this.controller.game
game.started = true game.started = true
var timestamp = Date.now() var timestamp = Date.now()
...@@ -174,6 +184,7 @@ class Debug{ ...@@ -174,6 +184,7 @@ class Debug{
this.restartBtn.style.display = "" this.restartBtn.style.display = ""
this.autoplayLabel.style.display = "" this.autoplayLabel.style.display = ""
this.branchHideDiv.style.display = "" this.branchHideDiv.style.display = ""
this.lyricsHideDiv.style.display = ""
this.controller = null this.controller = null
} }
this.stopMove() this.stopMove()
...@@ -194,6 +205,9 @@ class Debug{ ...@@ -194,6 +205,9 @@ class Debug{
branch.ms = branch.originalMS + offset branch.ms = branch.originalMS + offset
}) })
} }
if(this.controller.lyrics){
this.controller.lyrics.offsetChange(value * 1000)
}
if(this.restartCheckbox.checked && !noRestart){ if(this.restartCheckbox.checked && !noRestart){
this.restartSong() this.restartSong()
} }
...@@ -213,6 +227,14 @@ class Debug{ ...@@ -213,6 +227,14 @@ class Debug{
this.restartSong() this.restartSong()
} }
} }
lyricsChange(value, noRestart){
if(this.controller && this.controller.lyrics){
this.controller.lyrics.offsetChange(undefined, value * 1000)
}
if(this.restartCheckbox.checked && !noRestart){
this.restartSong()
}
}
restartSong(){ restartSong(){
if(this.controller){ if(this.controller){
this.controller.restartSong() this.controller.restartSong()
...@@ -259,6 +281,7 @@ class Debug{ ...@@ -259,6 +281,7 @@ class Debug{
this.offsetSlider.clean() this.offsetSlider.clean()
this.measureNumSlider.clean() this.measureNumSlider.clean()
this.volumeSlider.clean() this.volumeSlider.clean()
this.lyricsSlider.clean()
pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol) pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol)
pageEvents.mouseRemove(this) pageEvents.mouseRemove(this)
...@@ -285,6 +308,8 @@ class Debug{ ...@@ -285,6 +308,8 @@ class Debug{
delete this.branchSelect delete this.branchSelect
delete this.branchResetBtn delete this.branchResetBtn
delete this.volumeDiv delete this.volumeDiv
delete this.lyricsHideDiv
delete this.lyricsOffsetDiv
delete this.restartCheckbox delete this.restartCheckbox
delete this.autoplayLabel delete this.autoplayLabel
delete this.autoplayCheckbox delete this.autoplayCheckbox
......
...@@ -5,6 +5,7 @@ class Game{ ...@@ -5,6 +5,7 @@ class Game{
this.songData = songData this.songData = songData
this.elapsedTime = 0 this.elapsedTime = 0
this.currentCircle = -1 this.currentCircle = -1
this.currentEvent = 0
this.updateCurrentCircle() this.updateCurrentCircle()
this.combo = 0 this.combo = 0
this.rules = new GameRules(this) this.rules = new GameRules(this)
...@@ -47,13 +48,7 @@ class Game{ ...@@ -47,13 +48,7 @@ class Game{
} }
initTiming(){ initTiming(){
// Date when the chrono is started (before the game begins) // Date when the chrono is started (before the game begins)
var firstCircle var firstCircle = this.songData.circles[0]
for(var i = 0; i < this.songData.circles.length; i++){
firstCircle = this.songData.circles[i]
if(firstCircle.type !== "event"){
break
}
}
if(this.controller.calibrationMode){ if(this.controller.calibrationMode){
var offsetTime = 0 var offsetTime = 0
}else{ }else{
...@@ -238,9 +233,6 @@ class Game{ ...@@ -238,9 +233,6 @@ class Game{
} }
} }
skipNote(circle){ skipNote(circle){
if(circle.type === "event"){
return
}
if(circle.section){ if(circle.section){
this.resetSection() this.resetSection()
} }
...@@ -258,9 +250,6 @@ class Game{ ...@@ -258,9 +250,6 @@ class Game{
checkPlays(){ checkPlays(){
var circles = this.songData.circles var circles = this.songData.circles
var circle = circles[this.currentCircle] var circle = circles[this.currentCircle]
if(circle && circle.type === "event"){
this.updateCurrentCircle()
}
if(this.controller.autoPlayEnabled){ if(this.controller.autoPlayEnabled){
while(circle && this.controller.autoPlay(circle)){ while(circle && this.controller.autoPlay(circle)){
...@@ -469,9 +458,7 @@ class Game{ ...@@ -469,9 +458,7 @@ class Game{
} }
getLastCircle(circles){ getLastCircle(circles){
for(var i = circles.length; i--;){ for(var i = circles.length; i--;){
if(circles[i].type !== "event"){ return circles[i]
return circles[i]
}
} }
} }
whenLastCirclePlayed(){ whenLastCirclePlayed(){
...@@ -505,7 +492,9 @@ class Game{ ...@@ -505,7 +492,9 @@ class Game{
var musicDuration = duration * 1000 - this.controller.offset var musicDuration = duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){ if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){ if(this.controller.multiplayer === 1){
p2.send("gameresults", this.getGlobalScore()) var obj = this.getGlobalScore()
obj.name = account.loggedIn ? account.displayName : null
p2.send("gameresults", obj)
} }
this.musicFadeOut++ this.musicFadeOut++
}else if(this.musicFadeOut === 1 && ms >= started + 1600){ }else if(this.musicFadeOut === 1 && ms >= started + 1600){
...@@ -621,7 +610,7 @@ class Game{ ...@@ -621,7 +610,7 @@ class Game{
var circles = this.songData.circles var circles = this.songData.circles
do{ do{
var circle = circles[++this.currentCircle] var circle = circles[++this.currentCircle]
}while(circle && (circle.branch && !circle.branch.active || circle.type === "event")) }while(circle && (circle.branch && !circle.branch.active))
} }
getCurrentCircle(){ getCurrentCircle(){
return this.currentCircle return this.currentCircle
......
...@@ -202,12 +202,16 @@ ...@@ -202,12 +202,16 @@
var tja = new ParseTja(data, "oni", 0, 0, true) var tja = new ParseTja(data, "oni", 0, 0, true)
var songObj = { var songObj = {
id: index + 1, id: index + 1,
order: index + 1,
type: "tja", type: "tja",
chart: file, chart: file,
stars: [], courses: {},
music: "muted" music: "muted"
} }
var coursesAdded = false
var titleLang = {} var titleLang = {}
var titleLangAdded = false
var subtitleLangAdded = false
var subtitleLang = {} var subtitleLang = {}
var dir = file.webkitRelativePath.toLowerCase() var dir = file.webkitRelativePath.toLowerCase()
dir = dir.slice(0, dir.lastIndexOf("/") + 1) dir = dir.slice(0, dir.lastIndexOf("/") + 1)
...@@ -221,7 +225,11 @@ ...@@ -221,7 +225,11 @@
} }
songObj.subtitle = subtitle songObj.subtitle = subtitle
songObj.preview = meta.demostart || 0 songObj.preview = meta.demostart || 0
songObj.stars[this.courseTypes[diff]] = (meta.level || "0") + (meta.branch ? " B" : "") songObj.courses[diff] = {
stars: meta.level || 0,
branch: !!meta.branch
}
coursesAdded = true
if(meta.wave){ if(meta.wave){
songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music
} }
...@@ -252,6 +260,15 @@ ...@@ -252,6 +260,15 @@
id: 1 id: 1
} }
} }
if(meta.lyrics){
var lyricsFile = this.normPath(this.joinPath(dir, meta.lyrics))
if(lyricsFile in this.otherFiles){
songObj.lyrics = true
songObj.lyricsFile = this.otherFiles[lyricsFile]
}
}else if(meta.inlineLyrics){
songObj.lyrics = true
}
for(var id in allStrings){ for(var id in allStrings){
var songTitle = songObj.title var songTitle = songObj.title
var ura = "" var ura = ""
...@@ -264,32 +281,27 @@ ...@@ -264,32 +281,27 @@
} }
if(meta["title" + id]){ if(meta["title" + id]){
titleLang[id] = meta["title" + id] titleLang[id] = meta["title" + id]
titleLangAdded = true
}else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){
titleLang[id] = this.songTitle[songTitle][id] + ura titleLang[id] = this.songTitle[songTitle][id] + ura
titleLangAdded = true
} }
if(meta["subtitle" + id]){ if(meta["subtitle" + id]){
subtitleLang[id] = meta["subtitle" + id] subtitleLang[id] = meta["subtitle" + id]
subtitleLangAdded = true
} }
} }
} }
var titleLangArray = [] if(titleLangAdded){
for(var id in titleLang){ songObj.title_lang = titleLang
titleLangArray.push(id + " " + titleLang[id])
}
if(titleLangArray.length !== 0){
songObj.title_lang = titleLangArray.join("\n")
} }
var subtitleLangArray = [] if(subtitleLangAdded){
for(var id in subtitleLang){ songObj.subtitle_lang = subtitleLang
subtitleLangArray.push(id + " " + subtitleLang[id])
}
if(subtitleLangArray.length !== 0){
songObj.subtitle_lang = subtitleLangArray.join("\n")
} }
if(!songObj.category){ if(!songObj.category){
songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))]) songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))])
} }
if(songObj.stars.length !== 0){ if(coursesAdded){
this.songs[index] = songObj this.songs[index] = songObj
} }
var hash = md5.base64(event.target.result).slice(0, -2) var hash = md5.base64(event.target.result).slice(0, -2)
...@@ -316,12 +328,20 @@ ...@@ -316,12 +328,20 @@
dir = dir.slice(0, dir.lastIndexOf("/") + 1) dir = dir.slice(0, dir.lastIndexOf("/") + 1)
var songObj = { var songObj = {
id: index + 1, id: index + 1,
order: index + 1,
type: "osu", type: "osu",
chart: file, chart: file,
subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist,
subtitle_lang: osu.metadata.Artist || osu.metadata.ArtistUnicode, subtitle_lang: {
en: osu.metadata.Artist || osu.metadata.ArtistUnicode
},
preview: osu.generalInfo.PreviewTime / 1000, preview: osu.generalInfo.PreviewTime / 1000,
stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1], courses: {
oni:{
stars: parseInt(osu.difficulty.overallDifficulty) || 0,
branch: false
}
},
music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted" music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted"
} }
var filename = file.name.slice(0, file.name.lastIndexOf(".")) var filename = file.name.slice(0, file.name.lastIndexOf("."))
...@@ -333,7 +353,9 @@ ...@@ -333,7 +353,9 @@
suffix = " " + matches[0] suffix = " " + matches[0]
} }
songObj.title = title + suffix songObj.title = title + suffix
songObj.title_lang = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix songObj.title_lang = {
en: (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix
}
}else{ }else{
songObj.title = filename songObj.title = filename
} }
...@@ -417,7 +439,7 @@ ...@@ -417,7 +439,7 @@
for(var i = path.length - 2; i >= 0; i--){ for(var i = path.length - 2; i >= 0; i--){
var hasTitle = false var hasTitle = false
for(var j in exclude){ for(var j in exclude){
if(path[i].indexOf(exclude[j].toLowerCase()) !== -1){ if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){
hasTitle = true hasTitle = true
break break
} }
......
This diff is collapsed.
...@@ -34,7 +34,7 @@ class LoadSong{ ...@@ -34,7 +34,7 @@ class LoadSong{
run(){ run(){
var song = this.selectedSong var song = this.selectedSong
var id = song.folder var id = song.folder
var promises = [] this.promises = []
if(song.folder !== "calibration"){ if(song.folder !== "calibration"){
assets.sounds["v_start"].play() assets.sounds["v_start"].play()
var songObj = assets.songs.find(song => song.id === id) var songObj = assets.songs.find(song => song.id === id)
...@@ -92,9 +92,9 @@ class LoadSong{ ...@@ -92,9 +92,9 @@ class LoadSong{
img.crossOrigin = "Anonymous" img.crossOrigin = "Anonymous"
} }
let promise = pageEvents.load(img) let promise = pageEvents.load(img)
promises.push(promise.then(() => { this.addPromise(promise.then(() => {
return this.scaleImg(img, filename, prefix, force) return this.scaleImg(img, filename, prefix, force)
})) }), songObj.music ? filename + ".png" : skinBase + filename + ".png")
if(songObj.music){ if(songObj.music){
img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) img.src = URL.createObjectURL(song.songSkin[filename + ".png"])
}else{ }else{
...@@ -102,14 +102,15 @@ class LoadSong{ ...@@ -102,14 +102,15 @@ class LoadSong{
} }
} }
} }
promises.push(this.loadSongBg(id)) this.loadSongBg(id)
promises.push(new Promise((resolve, reject) => { var url = gameConfig.songs_baseurl + id + "/main.mp3"
this.addPromise(new Promise((resolve, reject) => {
if(songObj.sound){ if(songObj.sound){
songObj.sound.gain = snd.musicGain songObj.sound.gain = snd.musicGain
resolve() resolve()
}else if(!songObj.music){ }else if(!songObj.music){
snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { snd.musicGain.load(url).then(sound => {
songObj.sound = sound songObj.sound = sound
resolve() resolve()
}, reject) }, reject)
...@@ -121,84 +122,120 @@ class LoadSong{ ...@@ -121,84 +122,120 @@ class LoadSong{
}else{ }else{
resolve() resolve()
} }
})) }), songObj.music ? songObj.music.webkitRelativePath : url)
if(songObj.chart){ if(songObj.chart){
if(songObj.chart === "blank"){ if(songObj.chart === "blank"){
this.songData = "" this.songData = ""
}else{ }else{
var reader = new FileReader() var reader = new FileReader()
promises.push(pageEvents.load(reader).then(event => { this.addPromise(pageEvents.load(reader).then(event => {
this.songData = event.target.result.replace(/\0/g, "").split("\n") this.songData = event.target.result.replace(/\0/g, "").split("\n")
})) }), songObj.chart.webkitRelativePath)
if(song.type === "tja"){ if(song.type === "tja"){
reader.readAsText(songObj.chart, "sjis") reader.readAsText(songObj.chart, "sjis")
}else{ }else{
reader.readAsText(songObj.chart) reader.readAsText(songObj.chart)
} }
} }
if(songObj.lyricsFile && settings.getItem("showLyrics")){
var reader = new FileReader()
this.addPromise(pageEvents.load(reader).then(event => {
songObj.lyricsData = event.target.result
}, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath)
reader.readAsText(songObj.lyricsFile)
}
}else{ }else{
promises.push(loader.ajax(this.getSongPath(song)).then(data => { var url = this.getSongPath(song)
this.addPromise(loader.ajax(url).then(data => {
this.songData = data.replace(/\0/g, "").split("\n") this.songData = data.replace(/\0/g, "").split("\n")
})) }), url)
if(song.lyrics && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
var url = this.getSongDir(song) + "main.vtt"
this.addPromise(loader.ajax(url).then(data => {
songObj.lyricsData = data
}), url)
}
} }
if(this.touchEnabled && !assets.image["touch_drum"]){ if(this.touchEnabled && !assets.image["touch_drum"]){
let img = document.createElement("img") let img = document.createElement("img")
if(this.imgScale !== 1){ if(this.imgScale !== 1){
img.crossOrigin = "Anonymous" img.crossOrigin = "Anonymous"
} }
promises.push(pageEvents.load(img).then(() => { var url = gameConfig.assets_baseurl + "img/touch_drum.png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, "touch_drum", "") return this.scaleImg(img, "touch_drum", "")
})) }), url)
img.src = gameConfig.assets_baseurl + "img/touch_drum.png" img.src = url
} }
Promise.all(promises).then(() => { Promise.all(this.promises).then(() => {
this.setupMultiplayer() if(!this.error){
}, error => { this.setupMultiplayer()
if(Array.isArray(error) && error[1] instanceof HTMLElement){
error = error[0] + ": " + error[1].outerHTML
} }
console.error(error)
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
alert(strings.errorOccured)
}) })
} }
loadSongBg(){ addPromise(promise, url){
return new Promise((resolve, reject) => { this.promises.push(promise.catch(response => {
var promises = [] this.errorMsg(response, url)
var filenames = [] return Promise.resolve()
if(this.selectedSong.songBg !== null){ }))
filenames.push("bg_song_" + this.selectedSong.songBg) }
errorMsg(error, url){
if(!this.error){
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
} }
if(this.selectedSong.donBg !== null){ pageEvents.send("load-song-error", error)
filenames.push("bg_don_" + this.selectedSong.donBg) errorMessage(new Error(error).stack)
if(this.multiplayer){ var title = this.selectedSong.title
filenames.push("bg_don2_" + this.selectedSong.donBg) if(title !== this.selectedSong.originalTitle){
} title += " (" + this.selectedSong.originalTitle + ")"
} }
if(this.selectedSong.songStage !== null){ assets.sounds["v_start"].stop()
filenames.push("bg_stage_" + this.selectedSong.songStage) setTimeout(() => {
this.clean()
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
}
this.error = true
}
loadSongBg(){
var filenames = []
if(this.selectedSong.songBg !== null){
filenames.push("bg_song_" + this.selectedSong.songBg)
}
if(this.selectedSong.donBg !== null){
filenames.push("bg_don_" + this.selectedSong.donBg)
if(this.multiplayer){
filenames.push("bg_don2_" + this.selectedSong.donBg)
} }
for(var i = 0; i < filenames.length; i++){ }
var filename = filenames[i] if(this.selectedSong.songStage !== null){
var stage = filename.startsWith("bg_stage_") filenames.push("bg_stage_" + this.selectedSong.songStage)
for(var letter = 0; letter < (stage ? 1 : 2); letter++){ }
let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b")) for(var i = 0; i < filenames.length; i++){
if(!(filenameAb in assets.image)){ var filename = filenames[i]
let img = document.createElement("img") var stage = filename.startsWith("bg_stage_")
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled for(var letter = 0; letter < (stage ? 1 : 2); letter++){
if(this.imgScale !== 1 || force){ let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b"))
img.crossOrigin = "Anonymous" if(!(filenameAb in assets.image)){
} let img = document.createElement("img")
promises.push(pageEvents.load(img).then(() => { let force = filenameAb.startsWith("bg_song_") && this.touchEnabled
return this.scaleImg(img, filenameAb, "", force) if(this.imgScale !== 1 || force){
})) img.crossOrigin = "Anonymous"
img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
} }
var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, filenameAb, "", force)
}), url)
img.src = url
} }
} }
Promise.all(promises).then(resolve, reject) }
})
} }
scaleImg(img, filename, prefix, force){ scaleImg(img, filename, prefix, force){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -238,8 +275,11 @@ class LoadSong{ ...@@ -238,8 +275,11 @@ class LoadSong{
randInt(min, max){ randInt(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min return Math.floor(Math.random() * (max - min + 1)) + min
} }
getSongDir(selectedSong){
return gameConfig.songs_baseurl + selectedSong.folder + "/"
}
getSongPath(selectedSong){ getSongPath(selectedSong){
var directory = gameConfig.songs_baseurl + selectedSong.folder + "/" var directory = this.getSongDir(selectedSong)
if(selectedSong.type === "tja"){ if(selectedSong.type === "tja"){
return directory + "main.tja" return directory + "main.tja"
}else{ }else{
...@@ -264,14 +304,14 @@ class LoadSong{ ...@@ -264,14 +304,14 @@ class LoadSong{
if(event.type === "gameload"){ if(event.type === "gameload"){
this.cancelButton.style.display = "" this.cancelButton.style.display = ""
if(event.value === song.difficulty){ if(event.value.diff === song.difficulty){
this.startMultiplayer() this.startMultiplayer()
}else{ }else{
this.selectedSong2 = {} this.selectedSong2 = {}
for(var i in this.selectedSong){ for(var i in this.selectedSong){
this.selectedSong2[i] = this.selectedSong[i] this.selectedSong2[i] = this.selectedSong[i]
} }
this.selectedSong2.difficulty = event.value this.selectedSong2.difficulty = event.value.diff
if(song.type === "tja"){ if(song.type === "tja"){
this.startMultiplayer() this.startMultiplayer()
}else{ }else{
...@@ -297,7 +337,8 @@ class LoadSong{ ...@@ -297,7 +337,8 @@ class LoadSong{
}) })
p2.send("join", { p2.send("join", {
id: song.folder, id: song.folder,
diff: song.difficulty diff: song.difficulty,
name: account.loggedIn ? account.displayName : null
}) })
}else{ }else{
this.clean() this.clean()
...@@ -332,6 +373,7 @@ class LoadSong{ ...@@ -332,6 +373,7 @@ class LoadSong{
pageEvents.send("load-song-cancel") pageEvents.send("load-song-cancel")
} }
clean(){ clean(){
delete this.promises
pageEvents.remove(p2, "message") pageEvents.remove(p2, "message")
if(this.cancelButton){ if(this.cancelButton){
pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"]) pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"])
......
class Lyrics{
constructor(file, songOffset, div, parsed){
this.div = div
this.stroke = document.createElement("div")
this.stroke.classList.add("stroke")
div.appendChild(this.stroke)
this.fill = document.createElement("div")
this.fill.classList.add("fill")
div.appendChild(this.fill)
this.current = 0
this.shown = -1
this.songOffset = songOffset || 0
this.vttOffset = 0
this.rLinebreak = /\n|\r\n/
this.lines = parsed ? file : this.parseFile(file)
this.length = this.lines.length
}
parseFile(file){
var lines = []
var commands = file.split(/\n\n|\r\n\r\n/)
var arrow = " --> "
for(var i in commands){
var matches = commands[i].match(this.rLinebreak)
if(matches){
var cmd = commands[i].slice(0, matches.index)
var value = commands[i].slice(matches.index + 1)
}else{
var cmd = commands[i]
var value = ""
}
if(cmd.startsWith("WEBVTT")){
var nameValue = cmd.slice(7).split(";")
for(var j in nameValue){
var [name, value] = nameValue[j].split(":")
if(name.trim().toLowerCase() === "offset"){
this.vttOffset = (parseFloat(value.trim()) || 0) * 1000
}
}
}else{
var time = null
var index = cmd.indexOf(arrow)
if(index !== -1){
time = cmd
}else{
var matches = value.match(this.rLinebreak)
if(matches){
var value1 = value.slice(0, matches.index)
index = value1.indexOf(arrow)
if(index !== -1){
time = value1
value = value.slice(index)
}
}
}
if(time !== null){
var start = time.slice(0, index)
var end = time.slice(index + arrow.length)
var index = end.indexOf(" ")
if(index !== -1){
end = end.slice(0, index)
}
var text = value.trim()
var textLang = ""
var firstLang = -1
var index2 = -1
while(true){
var index1 = text.indexOf("<lang ", index2 + 1)
if(firstLang === -1){
firstLang = index1
}
if(index1 !== -1){
index2 = text.indexOf(">", index1 + 6)
if(index2 === -1){
break
}
var lang = text.slice(index1 + 6, index2).toLowerCase()
if(strings.id === lang){
var index3 = text.indexOf("<lang ", index2 + 1)
if(index3 !== -1){
textLang = text.slice(index2 + 1, index3)
}else{
textLang = text.slice(index2 + 1)
}
}
}else{
break
}
}
if(!textLang){
textLang = firstLang === -1 ? text : text.slice(0, firstLang)
}
lines.push({
start: this.convertTime(start),
end: this.convertTime(end),
text: textLang
})
}
}
}
return lines
}
convertTime(time){
if(time.startsWith("-")){
var mul = -1
time = time.slice(1)
}else{
var mul = 1
}
var array = time.split(":")
if(array.length === 2){
var h = 0
var m = array[0]
var s = array[1]
}else{
var h = parseInt(array[0])
var m = array[1]
var s = array[2]
}
var index = s.indexOf(",")
if(index !== -1){
s = s.slice(0, index) + "." + s.slice(index + 1)
}
return ((h * 60 + parseInt(m)) * 60 + parseFloat(s)) * 1000 * mul
}
update(ms){
if(this.current >= this.length){
return
}
ms += this.songOffset + this.vttOffset
var currentLine = this.lines[this.current]
while(currentLine && ms > currentLine.end){
currentLine = this.lines[++this.current]
}
if(this.shown !== this.current){
if(currentLine && ms >= currentLine.start){
this.setText(this.lines[this.current].text)
this.shown = this.current
}else if(this.shown !== -1){
this.setText("")
this.shown = -1
}
}
}
setText(text){
this.stroke.innerHTML = this.fill.innerHTML = ""
var hasRuby = false
while(text){
var matches = text.match(this.rLinebreak)
var index1 = matches ? matches.index : -1
var index2 = text.indexOf("<ruby>")
if(index1 !== -1 && (index2 === -1 || index2 > index1)){
this.textNode(text.slice(0, index1))
this.linebreakNode()
text = text.slice(index1 + matches[0].length)
}else if(index2 !== -1){
hasRuby = true
this.textNode(text.slice(0, index2))
text = text.slice(index2 + 6)
var index = text.indexOf("</ruby>")
if(index !== -1){
var ruby = text.slice(0, index)
text = text.slice(index + 7)
}else{
var ruby = text
text = ""
}
var index = ruby.indexOf("<rt>")
if(index !== -1){
var node1 = ruby.slice(0, index)
ruby = ruby.slice(index + 4)
var index = ruby.indexOf("</rt>")
if(index !== -1){
var node2 = ruby.slice(0, index)
}else{
var node2 = ruby
}
}else{
var node1 = ruby
var node2 = ""
}
this.rubyNode(node1, node2)
}else{
this.textNode(text)
break
}
}
}
insertNode(func){
this.stroke.appendChild(func())
this.fill.appendChild(func())
}
textNode(text){
this.insertNode(() => document.createTextNode(text))
}
linebreakNode(){
this.insertNode(() => document.createElement("br"))
}
rubyNode(node1, node2){
this.insertNode(() => {
var ruby = document.createElement("ruby")
var rt = document.createElement("rt")
ruby.appendChild(document.createTextNode(node1))
rt.appendChild(document.createTextNode(node2))
ruby.appendChild(rt)
return ruby
})
}
setScale(ratio){
this.div.style.setProperty("--scale", ratio)
}
offsetChange(songOffset, vttOffset){
if(typeof songOffset !== "undefined"){
this.songOffset = songOffset
}
if(typeof vttOffset !== "undefined"){
this.vttOffset = vttOffset
}
this.setText("")
this.current = 0
this.shown = -1
}
clean(){
if(this.shown !== -1){
this.setText("")
}
delete this.div
delete this.stroke
delete this.fill
delete this.lines
}
}
...@@ -84,6 +84,7 @@ var strings ...@@ -84,6 +84,7 @@ var strings
var vectors var vectors
var settings var settings
var scoreStorage var scoreStorage
var account = {}
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){
......
...@@ -3,6 +3,8 @@ class P2Connection{ ...@@ -3,6 +3,8 @@ class P2Connection{
this.closed = true this.closed = true
this.lastMessages = {} this.lastMessages = {}
this.otherConnected = false this.otherConnected = false
this.name = null
this.player = 1
this.allEvents = new Map() this.allEvents = new Map()
this.addEventListener("message", this.message.bind(this)) this.addEventListener("message", this.message.bind(this))
this.currentHash = "" this.currentHash = ""
...@@ -102,6 +104,10 @@ class P2Connection{ ...@@ -102,6 +104,10 @@ class P2Connection{
} }
message(response){ message(response){
switch(response.type){ switch(response.type){
case "gameload":
if("player" in response.value){
this.player = response.value.player === 2 ? 2 : 1
}
case "gamestart": case "gamestart":
this.otherConnected = true this.otherConnected = true
this.notes = [] this.notes = []
...@@ -110,6 +116,7 @@ class P2Connection{ ...@@ -110,6 +116,7 @@ class P2Connection{
this.kaAmount = 0 this.kaAmount = 0
this.results = false this.results = false
this.branch = "normal" this.branch = "normal"
scoreStorage.clearP2()
break break
case "gameend": case "gameend":
this.otherConnected = false this.otherConnected = false
...@@ -123,11 +130,13 @@ class P2Connection{ ...@@ -123,11 +130,13 @@ class P2Connection{
this.hash("") this.hash("")
this.hashLock = false this.hashLock = false
} }
this.name = null
scoreStorage.clearP2()
break break
case "gameresults": case "gameresults":
this.results = {} this.results = {}
for(var i in response.value){ for(var i in response.value){
this.results[i] = response.value[i].toString() this.results[i] = response.value[i] === null ? null : response.value[i].toString()
} }
break break
case "note": case "note":
...@@ -150,6 +159,44 @@ class P2Connection{ ...@@ -150,6 +159,44 @@ class P2Connection{
this.clearMessage("users") this.clearMessage("users")
this.otherConnected = true this.otherConnected = true
this.session = true this.session = true
scoreStorage.clearP2()
if("player" in response.value){
this.player = response.value.player === 2 ? 2 : 1
}
break
case "name":
this.name = response.value ? response.value.toString() : response.value
break
case "getcrowns":
if(response.value){
var output = {}
for(var i in response.value){
if(response.value[i]){
var score = scoreStorage.get(response.value[i], false, true)
if(score){
var crowns = {}
for(var diff in score){
if(diff !== "title"){
crowns[diff] = {
crown: score[diff].crown
}
}
}
}else{
var crowns = null
}
output[response.value[i]] = crowns
}
}
p2.send("crowns", output)
}
break
case "crowns":
if(response.value){
for(var i in response.value){
scoreStorage.addP2(i, false, response.value[i], true)
}
}
break break
} }
} }
......
...@@ -86,6 +86,9 @@ class PageEvents{ ...@@ -86,6 +86,9 @@ class PageEvents{
}) })
} }
keyEvent(event){ keyEvent(event){
if(!("key" in event) || event.ctrlKey && (event.key === "c" || event.key === "x" || event.key === "v")){
return
}
if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){ if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){
this.lastKeyEvent = Date.now() this.lastKeyEvent = Date.now()
event.preventDefault() event.preventDefault()
......
...@@ -48,6 +48,7 @@ class ParseOsu{ ...@@ -48,6 +48,7 @@ class ParseOsu{
lastBeatInterval: 0, lastBeatInterval: 0,
bpm: 0 bpm: 0
} }
this.events = []
this.generalInfo = this.parseGeneralInfo() this.generalInfo = this.parseGeneralInfo()
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.editor = this.parseEditor() this.editor = this.parseEditor()
...@@ -244,6 +245,18 @@ class ParseOsu{ ...@@ -244,6 +245,18 @@ class ParseOsu{
var circles = [] var circles = []
var circleID = 0 var circleID = 0
var indexes = this.getStartEndIndexes("HitObjects") var indexes = this.getStartEndIndexes("HitObjects")
var lastBeatMS = this.beatInfo.beatInterval
var lastGogo = false
var pushCircle = circle => {
circles.push(circle)
if(lastBeatMS !== circle.beatMS || lastGogo !== circle.gogoTime){
lastBeatMS = circle.beatMS
lastGogo = circle.gogoTime
this.events.push(circle)
}
}
for(var i = indexes.start; i <= indexes.end; i++){ for(var i = indexes.start; i <= indexes.end; i++){
circleID++ circleID++
var values = this.data[i].split(",") var values = this.data[i].split(",")
...@@ -277,7 +290,7 @@ class ParseOsu{ ...@@ -277,7 +290,7 @@ class ParseOsu{
var endTime = parseInt(values[this.osu.ENDTIME]) var endTime = parseInt(values[this.osu.ENDTIME])
var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65 var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65
var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier)) var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier))
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: "balloon", type: "balloon",
...@@ -304,7 +317,7 @@ class ParseOsu{ ...@@ -304,7 +317,7 @@ class ParseOsu{
type = "drumroll" type = "drumroll"
txt = strings.note.drumroll txt = strings.note.drumroll
} }
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: type, type: type,
...@@ -339,7 +352,7 @@ class ParseOsu{ ...@@ -339,7 +352,7 @@ class ParseOsu{
emptyValue = true emptyValue = true
} }
if(!emptyValue){ if(!emptyValue){
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: type, type: type,
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.measures = [] this.measures = []
this.beatInfo = {} this.beatInfo = {}
this.events = []
if(!metaOnly){ if(!metaOnly){
this.circles = this.parseCircles() this.circles = this.parseCircles()
} }
...@@ -83,6 +84,8 @@ ...@@ -83,6 +84,8 @@
} }
}else if(name.startsWith("branchstart") && inSong){ }else if(name.startsWith("branchstart") && inSong){
courses[courseName].branch = true courses[courseName].branch = true
}else if(name.startsWith("lyric") && inSong){
courses[courseName].inlineLyrics = true
} }
}else if(!inSong){ }else if(!inSong){
...@@ -157,6 +160,7 @@ ...@@ -157,6 +160,7 @@
var circleID = 0 var circleID = 0
var regexAZ = /[A-Z]/ var regexAZ = /[A-Z]/
var regexSpace = /\s/ var regexSpace = /\s/
var regexLinebreak = /\\n/g
var isAllDon = (note_chain, start_pos) => { var isAllDon = (note_chain, start_pos) => {
for (var i = start_pos; i < note_chain.length; ++i) { for (var i = start_pos; i < note_chain.length; ++i) {
var note = note_chain[i]; var note = note_chain[i];
...@@ -248,7 +252,12 @@ ...@@ -248,7 +252,12 @@
lastDrumroll = circleObj lastDrumroll = circleObj
} }
circles.push(circleObj) if(note.event){
this.events.push(circleObj)
}
if(note.type !== "event"){
circles.push(circleObj)
}
} else if (!(currentMeasure.length >= 24 && (!currentMeasure[i + 1] || currentMeasure[i + 1].type)) } else if (!(currentMeasure.length >= 24 && (!currentMeasure[i + 1] || currentMeasure[i + 1].type))
&& !(currentMeasure.length >= 48 && (!currentMeasure[i + 2] || currentMeasure[i + 2].type || !currentMeasure[i + 3] || currentMeasure[i + 3].type))) { && !(currentMeasure.length >= 48 && (!currentMeasure[i + 2] || currentMeasure[i + 2].type || !currentMeasure[i + 3] || currentMeasure[i + 3].type))) {
if (note_chain.length > 1 && currentMeasure.length >= 8) { if (note_chain.length > 1 && currentMeasure.length >= 8) {
...@@ -266,9 +275,12 @@ ...@@ -266,9 +275,12 @@
} }
} }
var insertNote = circleObj => { var insertNote = circleObj => {
lastBpm = bpm
lastGogo = gogo
if(circleObj){ if(circleObj){
if(bpm !== lastBpm || gogo !== lastGogo){
circleObj.event = true
lastBpm = bpm
lastGogo = gogo
}
currentMeasure.push(circleObj) currentMeasure.push(circleObj)
} }
} }
...@@ -402,6 +414,18 @@ ...@@ -402,6 +414,18 @@
} }
branchObj[branchName] = currentBranch branchObj[branchName] = currentBranch
break break
case "lyric":
if(!this.lyrics){
this.lyrics = []
}
if(this.lyrics.length !== 0){
this.lyrics[this.lyrics.length - 1].end = ms
}
this.lyrics.push({
start: ms,
text: value.trim().replace(regexLinebreak, "\n")
})
break
} }
}else{ }else{
...@@ -536,6 +560,10 @@ ...@@ -536,6 +560,10 @@
this.scoreinit = autoscore.ScoreInit; this.scoreinit = autoscore.ScoreInit;
this.scorediff = autoscore.ScoreDiff; this.scorediff = autoscore.ScoreDiff;
} }
if(this.lyrics){
var line = this.lyrics[this.lyrics.length - 1]
line.end = Math.max(ms, line.start) + 5000
}
return circles return circles
} }
} }
...@@ -2,9 +2,19 @@ class Scoresheet{ ...@@ -2,9 +2,19 @@ class Scoresheet{
constructor(controller, results, multiplayer, touchEnabled){ constructor(controller, results, multiplayer, touchEnabled){
this.controller = controller this.controller = controller
this.resultsObj = results this.resultsObj = results
this.results = {} this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
var player0 = this.player[0]
this.results = []
this.results[player0] = {}
this.rules = []
this.rules[player0] = this.controller.game.rules
if(multiplayer){
this.player.push(p2.player === 2 ? 0 : 1)
this.results[this.player[1]] = p2.results
this.rules[this.player[1]] = this.controller.syncWith.game.rules
}
for(var i in results){ for(var i in results){
this.results[i] = results[i].toString() this.results[player0][i] = results[i] === null ? null : results[i].toString()
} }
this.multiplayer = multiplayer this.multiplayer = multiplayer
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
...@@ -39,6 +49,7 @@ class Scoresheet{ ...@@ -39,6 +49,7 @@ class Scoresheet{
this.draw = new CanvasDraw(noSmoothing) this.draw = new CanvasDraw(noSmoothing)
this.canvasCache = new CanvasCache(noSmoothing) this.canvasCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.keyboard = new Keyboard({ this.keyboard = new Keyboard({
confirm: ["enter", "space", "esc", "don_l", "don_r"] confirm: ["enter", "space", "esc", "don_l", "don_r"]
...@@ -208,6 +219,7 @@ class Scoresheet{ ...@@ -208,6 +219,7 @@ class Scoresheet{
this.canvas.style.height = (winH / this.pixelRatio) + "px" this.canvas.style.height = (winH / this.pixelRatio) + "px"
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
this.nameplateCache.resize(274, 134, ratio + 0.2)
if(!this.multiplayer){ if(!this.multiplayer){
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio) this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
...@@ -233,6 +245,9 @@ class Scoresheet{ ...@@ -233,6 +245,9 @@ class Scoresheet{
if(!this.canvasCache.canvas){ if(!this.canvasCache.canvas){
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
} }
if(!this.nameplateCache.canvas){
this.nameplateCache.resize(274, 67, ratio + 0.2)
}
} }
this.winW = winW this.winW = winW
this.winH = winH this.winH = winH
...@@ -243,7 +258,7 @@ class Scoresheet{ ...@@ -243,7 +258,7 @@ class Scoresheet{
var frameTop = winH / 2 - 720 / 2 var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 2 var frameLeft = winW / 2 - 1280 / 2
var players = this.multiplayer && p2.results ? 2 : 1 var players = this.multiplayer ? 2 : 1
var p2Offset = 298 var p2Offset = 298
var bgOffset = 0 var bgOffset = 0
...@@ -326,28 +341,21 @@ class Scoresheet{ ...@@ -326,28 +341,21 @@ class Scoresheet{
} }
var rules = this.controller.game.rules var rules = this.controller.game.rules
var gaugePercent = rules.gaugePercent(this.results.gauge) var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
var gaugeClear = [rules.gaugeClear] if(players === 2 && failedOffset !== 0){
if(players === 2){ var p2results = this.results[this.player[1]]
gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear) if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
}
var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000
if(players === 2){
var gauge2 = this.controller.syncWith.game.rules.gaugePercent(p2.results.gauge)
if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){
failedOffset = 0 failedOffset = 0
} }
} }
if(elapsed >= 3100 + failedOffset){ if(elapsed >= 3100 + failedOffset){
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
ctx.save() ctx.save()
var results = this.results var results = this.results[p]
if(p === 1){ if(!results){
results = p2.results continue
} }
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules var clear = this.rules[p].clearReached(results.gauge)
var resultGauge = playerRules.gaugePercent(results.gauge)
var clear = resultGauge >= gaugeClear[p]
if(p === 1 || !this.multiplayer && clear){ if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290) ctx.translate(0, 290)
} }
...@@ -410,7 +418,7 @@ class Scoresheet{ ...@@ -410,7 +418,7 @@ class Scoresheet{
this.draw.layeredText({ this.draw.layeredText({
ctx: ctx, ctx: ctx,
text: this.results.title, text: this.results[this.player[0]].title,
fontSize: 40, fontSize: 40,
fontFamily: this.font, fontFamily: this.font,
x: 1257, x: 1257,
...@@ -426,9 +434,11 @@ class Scoresheet{ ...@@ -426,9 +434,11 @@ class Scoresheet{
ctx.save() ctx.save()
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
...@@ -450,6 +460,30 @@ class Scoresheet{ ...@@ -450,6 +460,30 @@ class Scoresheet{
ctx.fillText(text, 395, 308) ctx.fillText(text, 395, 308)
ctx.miterLimit = 10 ctx.miterLimit = 10
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
if(p === this.player[0]){
var name = account.loggedIn ? account.displayName : defaultName
}else{
var name = results.name || defaultName
}
this.nameplateCache.get({
ctx: ctx,
x: 259,
y: 92,
w: 273,
h: 66,
id: p.toString() + "p" + name,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
font: this.font,
blue: p === 1
})
})
if(this.controller.autoPlayEnabled){ if(this.controller.autoPlayEnabled){
ctx.drawImage(assets.image["badge_auto"], ctx.drawImage(assets.image["badge_auto"],
431, 311, 34, 34 431, 311, 34, 34
...@@ -581,7 +615,7 @@ class Scoresheet{ ...@@ -581,7 +615,7 @@ class Scoresheet{
if(this.tetsuoHanaClass){ if(this.tetsuoHanaClass){
this.tetsuoHana.classList.remove(this.tetsuoHanaClass) this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
} }
this.tetsuoHanaClass = rules.clearReached(this.results.gauge) ? "dance" : "failed" this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
this.tetsuoHana.classList.add(this.tetsuoHanaClass) this.tetsuoHana.classList.add(this.tetsuoHanaClass)
} }
} }
...@@ -595,32 +629,32 @@ class Scoresheet{ ...@@ -595,32 +629,32 @@ class Scoresheet{
ctx.translate(frameLeft, frameTop) ctx.translate(frameLeft, frameTop)
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
var gaugePercent = rules.gaugePercent(results.gauge)
var w = 712 var w = 712
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: 558 + w, x: 558 + w,
y: p === 1 ? 124 : 116, y: p === 1 ? 124 : 116,
clear: gaugeClear[p], clear: this.rules[p].gaugeClear,
percentage: gaugePercent, percentage: this.rules[p].gaugePercent(results.gauge),
font: this.font, font: this.font,
scale: w / 788, scale: w / 788,
scoresheet: true, scoresheet: true,
blue: p === 1, blue: p === 1,
multiplayer: p === 1 multiplayer: p === 1
}) })
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: 1215, x: 1215,
y: 144, y: 144,
scale: 36 / 42, scale: 36 / 42,
cleared: playerRules.clearReached(results.gauge) cleared: this.rules[p].clearReached(results.gauge)
}) })
} }
}) })
...@@ -633,13 +667,12 @@ class Scoresheet{ ...@@ -633,13 +667,12 @@ class Scoresheet{
var noCrownResultWait = -2000; var noCrownResultWait = -2000;
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(p === 1){ if(!results){
results = p2.results continue
} }
var crownType = null var crownType = null
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules if(this.rules[p].clearReached(results.gauge)){
if(playerRules.clearReached(results.gauge)){
crownType = results.bad === "0" ? "gold" : "silver" crownType = results.bad === "0" ? "gold" : "silver"
} }
if(crownType !== null){ if(crownType !== null){
...@@ -702,7 +735,10 @@ class Scoresheet{ ...@@ -702,7 +735,10 @@ class Scoresheet{
var times = {} var times = {}
var lastTime = 0 var lastTime = 0
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = p === 0 ? this.results : p2.results var results = this.results[p]
if(!results){
continue
}
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
if(currentTime > lastTime){ if(currentTime > lastTime){
lastTime = currentTime lastTime = currentTime
...@@ -711,7 +747,10 @@ class Scoresheet{ ...@@ -711,7 +747,10 @@ class Scoresheet{
for(var i in printNumbers){ for(var i in printNumbers){
var largestTime = 0 var largestTime = 0
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = p === 0 ? this.results : p2.results var results = this.results[p]
if(!results){
continue
}
times[printNumbers[i]] = lastTime + 500 times[printNumbers[i]] = lastTime + 500
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
if(currentTime > largestTime){ if(currentTime > largestTime){
...@@ -727,9 +766,11 @@ class Scoresheet{ ...@@ -727,9 +766,11 @@ class Scoresheet{
} }
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
ctx.save() ctx.save()
...@@ -823,7 +864,7 @@ class Scoresheet{ ...@@ -823,7 +864,7 @@ class Scoresheet{
if(elapsed >= 1000){ if(elapsed >= 1000){
this.clean() this.clean()
this.controller.songSelection(true) this.controller.songSelection(true, this.showWarning)
} }
} }
...@@ -890,10 +931,14 @@ class Scoresheet{ ...@@ -890,10 +931,14 @@ class Scoresheet{
delete this.resultsObj.title delete this.resultsObj.title
delete this.resultsObj.difficulty delete this.resultsObj.difficulty
delete this.resultsObj.gauge delete this.resultsObj.gauge
scoreStorage.add(hash, difficulty, this.resultsObj, true, title) scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
this.showWarning = {name: "scoreSaveFailed"}
})
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){ }else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
oldScore.crown = crown oldScore.crown = crown
scoreStorage.add(hash, difficulty, oldScore, true, title) scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
this.showWarning = {name: "scoreSaveFailed"}
})
} }
} }
this.scoreSaved = true this.scoreSaved = true
...@@ -908,7 +953,7 @@ class Scoresheet{ ...@@ -908,7 +953,7 @@ class Scoresheet{
snd.buffer.loadSettings() snd.buffer.loadSettings()
this.redrawRunning = false this.redrawRunning = false
pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
if(this.multiplayer !== 2 && this.touchEnabled){ if(this.touchEnabled){
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
} }
if(this.session){ if(this.session){
...@@ -920,5 +965,7 @@ class Scoresheet{ ...@@ -920,5 +965,7 @@ class Scoresheet{
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas
delete this.fadeScreen delete this.fadeScreen
delete this.results
delete this.rules
} }
} }
class ScoreStorage{ class ScoreStorage{
constructor(){ constructor(){
this.scores = {} this.scores = {}
this.scoresP2 = {}
this.requestP2 = new Set()
this.requestedP2 = new Set()
this.songTitles = {} this.songTitles = {}
this.difficulty = ["oni", "ura", "hard", "normal", "easy"] this.difficulty = ["oni", "ura", "hard", "normal", "easy"]
this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"] this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"]
this.crownValue = ["", "silver", "gold"] this.crownValue = ["", "silver", "gold"]
this.load()
} }
load(){ load(strings, loadFailed){
this.scores = {} var scores = {}
this.scoreStrings = {} var scoreStrings = {}
try{ if(loadFailed){
var localScores = localStorage.getItem("scoreStorage") try{
if(localScores){ var localScores = localStorage.getItem("saveFailed")
this.scoreStrings = JSON.parse(localScores) if(localScores){
} scoreStrings = JSON.parse(localScores)
}catch(e){} }
for(var hash in this.scoreStrings){ }catch(e){}
var scoreString = this.scoreStrings[hash] }else if(strings){
scoreStrings = this.prepareStrings(strings)
}else if(account.loggedIn){
return
}else{
try{
var localScores = localStorage.getItem("scoreStorage")
if(localScores){
scoreStrings = JSON.parse(localScores)
}
}catch(e){}
}
for(var hash in scoreStrings){
var scoreString = scoreStrings[hash]
var songAdded = false var songAdded = false
if(typeof scoreString === "string" && scoreString){ if(typeof scoreString === "string" && scoreString){
var diffArray = scoreString.split(";") var diffArray = scoreString.split(";")
...@@ -37,25 +52,63 @@ class ScoreStorage{ ...@@ -37,25 +52,63 @@ class ScoreStorage{
score[name] = value score[name] = value
} }
if(!songAdded){ if(!songAdded){
this.scores[hash] = {title: null} scores[hash] = {title: null}
songAdded = true songAdded = true
} }
this.scores[hash][this.difficulty[i]] = score scores[hash][this.difficulty[i]] = score
}
}
}
}
if(loadFailed){
for(var hash in scores){
for(var i in this.difficulty){
var diff = this.difficulty[i]
if(scores[hash][diff]){
this.add(hash, diff, scores[hash][diff], true, this.songTitles[hash] || null).then(() => {
localStorage.removeItem("saveFailed")
}, () => {})
} }
} }
} }
}else{
this.scores = scores
this.scoreStrings = scoreStrings
}
if(strings){
this.load(false, true)
} }
} }
prepareScores(scores){
var output = []
for (var k in scores) {
output.push({'hash': k, 'score': scores[k]})
}
return output
}
prepareStrings(scores){
var output = {}
for(var k in scores){
output[scores[k].hash] = scores[k].score
}
return output
}
save(){ save(){
for(var hash in this.scores){ for(var hash in this.scores){
this.writeString(hash) this.writeString(hash)
} }
this.write() this.write()
return this.sendToServer({
scores: this.prepareScores(this.scoreStrings),
is_import: true
})
} }
write(){ write(){
try{ if(!account.loggedIn){
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) try{
}catch(e){} localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
}catch(e){}
}
} }
writeString(hash){ writeString(hash){
var score = this.scores[hash] var score = this.scores[hash]
...@@ -101,17 +154,82 @@ class ScoreStorage{ ...@@ -101,17 +154,82 @@ class ScoreStorage{
} }
} }
} }
add(song, difficulty, scoreObject, isHash, setTitle){ getP2(song, difficulty, isHash){
if(!song){
return this.scoresP2
}else{
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scoresP2) && !this.requestP2.has(hash) && !this.requestedP2.has(hash)){
this.requestP2.add(hash)
this.requestedP2.add(hash)
}
if(difficulty){
if(hash in this.scoresP2){
return this.scoresP2[hash][difficulty]
}
}else{
return this.scoresP2[hash]
}
}
}
add(song, difficulty, scoreObject, isHash, setTitle, saveFailed){
var hash = isHash ? song : this.titleHash(song) var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){ if(!(hash in this.scores)){
this.scores[hash] = {} this.scores[hash] = {}
} }
if(setTitle){ if(difficulty){
this.scores[hash].title = setTitle if(setTitle){
this.scores[hash].title = setTitle
}
this.scores[hash][difficulty] = scoreObject
}else{
this.scores[hash] = scoreObject
if(setTitle){
this.scores[hash].title = setTitle
}
} }
this.scores[hash][difficulty] = scoreObject
this.writeString(hash) this.writeString(hash)
this.write() this.write()
if(saveFailed){
var failedScores = {}
try{
var localScores = localStorage.getItem("saveFailed")
if(localScores){
failedScores = JSON.parse(localScores)
}
}catch(e){}
if(!(hash in failedScores)){
failedScores[hash] = {}
}
failedScores[hash] = this.scoreStrings[hash]
try{
localStorage.setItem("saveFailed", JSON.stringify(failedScores))
}catch(e){}
return Promise.reject()
}else{
var obj = {}
obj[hash] = this.scoreStrings[hash]
return this.sendToServer({
scores: this.prepareScores(obj)
}).catch(() => this.add(song, difficulty, scoreObject, isHash, setTitle, true))
}
}
addP2(song, difficulty, scoreObject, isHash, setTitle){
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){
this.scoresP2[hash] = {}
}
if(difficulty){
if(setTitle){
this.scoresP2[hash].title = setTitle
}
this.scoresP2[hash][difficulty] = scoreObject
}else{
this.scoresP2[hash] = scoreObject
if(setTitle){
this.scoresP2[hash].title = setTitle
}
}
} }
template(){ template(){
var template = {crown: ""} var template = {crown: ""}
...@@ -146,6 +264,62 @@ class ScoreStorage{ ...@@ -146,6 +264,62 @@ class ScoreStorage{
delete this.scoreStrings[hash] delete this.scoreStrings[hash]
} }
this.write() this.write()
this.sendToServer({
scores: this.prepareScores(this.scoreStrings),
is_import: true
})
} }
} }
sendToServer(obj, retry){
if(account.loggedIn){
return loader.getCsrfToken().then(token => {
var request = new XMLHttpRequest()
request.open("POST", "api/scores/save")
var promise = pageEvents.load(request).then(response => {
if(request.status !== 200){
return Promise.reject()
}
}).catch(() => {
if(retry){
this.scoreSaveFailed = true
account.loggedIn = false
delete account.username
delete account.displayName
this.load()
pageEvents.send("logout")
return Promise.reject()
}else{
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 3000)
}).then(() => this.sendToServer(obj, true))
}
})
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.setRequestHeader("X-CSRFToken", token)
request.send(JSON.stringify(obj))
return promise
})
}else{
return Promise.resolve()
}
}
eventLoop(){
if(p2.session && this.requestP2.size){
var req = []
this.requestP2.forEach(hash => {
req.push(hash)
})
this.requestP2.clear()
if(req.length){
p2.send("getcrowns", req)
}
}
}
clearP2(){
this.scoresP2 = {}
this.requestP2.clear()
this.requestedP2.clear()
}
} }
...@@ -34,7 +34,10 @@ class Session{ ...@@ -34,7 +34,10 @@ class Session{
pageEvents.send("session-start", "host") pageEvents.send("session-start", "host")
} }
}) })
p2.send("invite") p2.send("invite", {
id: null,
name: account.loggedIn ? account.displayName : null
})
pageEvents.send("session") pageEvents.send("session")
} }
getElement(name){ getElement(name){
......
...@@ -50,6 +50,10 @@ class Settings{ ...@@ -50,6 +50,10 @@ class Settings{
easierBigNotes: { easierBigNotes: {
type: "toggle", type: "toggle",
default: false default: false
},
showLyrics: {
type: "toggle",
default: true
} }
} }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -18,7 +18,7 @@ class ViewAssets{ ...@@ -18,7 +18,7 @@ class ViewAssets{
sw: imgw, sw: imgw,
sh: imgh - 1, sh: imgh - 1,
x: view.portrait ? -60 : 0, x: view.portrait ? -60 : 0,
y: view.portrait ? (view.multiplayer === 2 ? 560 : 35) : (view.multiplayer === 2 ? 360 : 2), y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 2),
w: w, w: w,
h: h - 1 h: h - 1
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="view"> <div class="view">
<div class="view-title stroke-sub"></div> <div class="view-title stroke-sub"></div>
<div class="view-content"></div> <div class="view-content"></div>
<div id="diag-txt"></div> <div class="diag-txt"></div>
<div class="left-buttons"> <div class="left-buttons">
<div id="link-issues" class="taibtn stroke-sub link-btn"> <div id="link-issues" class="taibtn stroke-sub link-btn">
<a target="_blank"></a> <a target="_blank"></a>
......
<div class="view-outer">
<div class="view account-view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="error-div"></div>
<div class="displayname-div">
<div class="displayname-hint"></div>
<input type="text" class="displayname" maxlength="25">
</div>
<form class="accountpass-form">
<div>
<div class="accountpass-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountpass-div">
<input type="password" name="password"><input type="password" name="newpassword" autocomplete="new-password"><input type="password" name="newpassword2" autocomplete="new-password">
</div>
</form>
<form class="accountdel-form">
<div>
<div class="accountdel-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountdel-div">
<input type="password" name="password">
</div>
</form>
</div>
<div id="diag-txt"></div>
<div class="left-buttons">
<div class="logout-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="save-btn taibtn stroke-sub selected"></div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>
...@@ -24,6 +24,12 @@ ...@@ -24,6 +24,12 @@
<div class="music-volume input-slider"> <div class="music-volume input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span> <span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div> </div>
<div class="lyrics-hide">
<div>Lyrics offset:</div>
<div class="lyrics-offset input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
</div>
<label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label> <label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label>
<label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label> <label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
<div class="bottom-btns"> <div class="bottom-btns">
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<div id="touch-drum-img"></div> <div id="touch-drum-img"></div>
</div> </div>
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<div id="song-lyrics"></div>
<div id="touch-buttons"> <div id="touch-buttons">
<div id="touch-full-btn"></div><div id="touch-pause-btn"></div> <div id="touch-full-btn"></div><div id="touch-pause-btn"></div>
</div> </div>
......
...@@ -2,3 +2,10 @@ ...@@ -2,3 +2,10 @@
<div class="progress"></div> <div class="progress"></div>
<span class="percentage">0%</span> <span class="percentage">0%</span>
</div> </div>
<div class="view-outer loader-error-div">
<div class="view">
<div class="view-content">An error occurred, please refresh</div>
<div class="diag-txt"></div>
<span class="debug-link">Debug</span>
</div>
</div>
<div class="view-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="error-div"></div>
<form class="login-form">
<div class="username-hint"></div>
<input type="text" name="username" maxlength="20" required>
<div class="password-hint"></div>
<input type="password" name="password" required>
<div class="password2-div"></div>
<div class="remember-div">
<label class="remember-label">
<input type="checkbox" checked="checked" name="remember">
</label>
</div>
<div class="login-btn taibtn stroke-sub link-btn"></div>
</form>
</div>
<div class="left-buttons">
<div class="register-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>
import jsonschema
def validate(data, schema):
try:
jsonschema.validate(data, schema)
return True
except jsonschema.exceptions.ValidationError:
return False
register = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'username': {'type': 'string'},
'password': {'type': 'string'}
}
}
login = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'username': {'type': 'string'},
'password': {'type': 'string'},
'remember': {'type': 'boolean'}
}
}
update_display_name = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'display_name': {'type': 'string'}
}
}
update_password = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'current_password': {'type': 'string'},
'new_password': {'type': 'string'}
}
}
delete_account = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'password': {'type': 'string'}
}
}
scores_save = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'scores': {
'type': 'array',
'items': {'$ref': '#/definitions/score'}
},
'is_import': {'type': 'boolean'}
},
'definitions': {
'score': {
'type': 'object',
'properties': {
'hash': {'type': 'string'},
'score': {'type': 'string'}
}
}
}
}
...@@ -13,11 +13,11 @@ server_status = { ...@@ -13,11 +13,11 @@ server_status = {
} }
consonants = "bcdfghjklmnpqrstvwxyz" consonants = "bcdfghjklmnpqrstvwxyz"
def msgobj(type, value=None): def msgobj(msg_type, value=None):
if value == None: if value == None:
return json.dumps({"type": type}) return json.dumps({"type": msg_type})
else: else:
return json.dumps({"type": type, "value": value}) return json.dumps({"type": msg_type, "value": value})
def status_event(): def status_event():
value = [] value = []
...@@ -42,7 +42,8 @@ async def connection(ws, path): ...@@ -42,7 +42,8 @@ async def connection(ws, path):
user = { user = {
"ws": ws, "ws": ws,
"action": "ready", "action": "ready",
"session": False "session": False,
"name": None
} }
server_status["users"].append(user) server_status["users"].append(user)
try: try:
...@@ -69,16 +70,17 @@ async def connection(ws, path): ...@@ -69,16 +70,17 @@ async def connection(ws, path):
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
data = {} data = {}
action = user["action"] action = user["action"]
type = data["type"] if "type" in data else None msg_type = data["type"] if "type" in data else None
value = data["value"] if "value" in data else None value = data["value"] if "value" in data else None
if action == "ready": if action == "ready":
# Not playing or waiting # Not playing or waiting
if type == "join": if msg_type == "join":
if value == None: if value == None:
continue 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
user["name"] = value["name"] if "name" in value else None
if not id or not diff: if not id or not diff:
continue continue
if id not in waiting: if id not in waiting:
...@@ -92,6 +94,7 @@ async def connection(ws, path): ...@@ -92,6 +94,7 @@ async def connection(ws, path):
await ws.send(msgobj("waiting")) await ws.send(msgobj("waiting"))
else: else:
# Join the other user and start game # Join the other user and start game
user["name"] = value["name"] if "name" in value else None
user["other_user"] = waiting[id]["user"] user["other_user"] = waiting[id]["user"]
waiting_diff = waiting[id]["diff"] waiting_diff = waiting[id]["diff"]
del waiting[id] del waiting[id]
...@@ -99,9 +102,13 @@ async def connection(ws, path): ...@@ -99,9 +102,13 @@ async def connection(ws, path):
user["action"] = "loading" user["action"] = "loading"
user["other_user"]["action"] = "loading" user["other_user"]["action"] = "loading"
user["other_user"]["other_user"] = user user["other_user"]["other_user"] = user
user["other_user"]["player"] = 1
user["player"] = 2
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameload", waiting_diff)), ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})),
user["other_user"]["ws"].send(msgobj("gameload", diff)) user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff, "player": 1})),
ws.send(msgobj("name", user["other_user"]["name"])),
user["other_user"]["ws"].send(msgobj("name", user["name"]))
]) ])
else: else:
# Wait for another user # Wait for another user
...@@ -115,28 +122,33 @@ async def connection(ws, path): ...@@ -115,28 +122,33 @@ 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": elif msg_type == "invite":
if value == None: if value and "id" in value and value["id"] == None:
# Session invite link requested # Session invite link requested
invite = get_invite() invite = get_invite()
server_status["invites"][invite] = user server_status["invites"][invite] = user
user["action"] = "invite" user["action"] = "invite"
user["session"] = invite user["session"] = invite
user["name"] = value["name"] if "name" in value else None
await ws.send(msgobj("invite", invite)) await ws.send(msgobj("invite", invite))
elif value in server_status["invites"]: elif value and "id" in value and value["id"] in server_status["invites"]:
# Join a session with the other user # Join a session with the other user
user["other_user"] = server_status["invites"][value] user["name"] = value["name"] if "name" in value else None
del server_status["invites"][value] user["other_user"] = server_status["invites"][value["id"]]
del server_status["invites"][value["id"]]
if "ws" in user["other_user"]: if "ws" in user["other_user"]:
user["other_user"]["other_user"] = user user["other_user"]["other_user"] = user
user["action"] = "invite" user["action"] = "invite"
user["session"] = value user["session"] = value["id"]
sent_msg = msgobj("session") user["other_user"]["player"] = 1
user["player"] = 2
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(msgobj("session", {"player": 2})),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(msgobj("session", {"player": 1})),
ws.send(msgobj("invite")),
ws.send(msgobj("name", user["other_user"]["name"])),
user["other_user"]["ws"].send(msgobj("name", user["name"]))
]) ])
await ws.send(msgobj("invite"))
else: else:
del user["other_user"] del user["other_user"]
await ws.send(msgobj("gameend")) await ws.send(msgobj("gameend"))
...@@ -145,7 +157,7 @@ async def connection(ws, path): ...@@ -145,7 +157,7 @@ async def connection(ws, path):
await ws.send(msgobj("gameend")) 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 msg_type == "leave":
# Stop waiting # Stop waiting
if user["session"]: if user["session"]:
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
...@@ -170,7 +182,7 @@ async def connection(ws, path): ...@@ -170,7 +182,7 @@ async def connection(ws, path):
notify_status() notify_status()
]) ])
if action == "loading": if action == "loading":
if type == "gamestart": if msg_type == "gamestart":
user["action"] = "loaded" user["action"] = "loaded"
if user["other_user"]["action"] == "loaded": if user["other_user"]["action"] == "loaded":
user["action"] = "playing" user["action"] = "playing"
...@@ -183,12 +195,12 @@ async def connection(ws, path): ...@@ -183,12 +195,12 @@ 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"\ if msg_type == "note"\
or type == "drumroll"\ or msg_type == "drumroll"\
or type == "branch"\ or msg_type == "branch"\
or type == "gameresults": or msg_type == "gameresults":
await user["other_user"]["ws"].send(msgobj(type, value)) await user["other_user"]["ws"].send(msgobj(msg_type, value))
elif type == "songsel" and user["session"]: elif msg_type == "songsel" and user["session"]:
user["action"] = "songsel" user["action"] = "songsel"
user["other_user"]["action"] = "songsel" user["other_user"]["action"] = "songsel"
sent_msg1 = msgobj("songsel") sent_msg1 = msgobj("songsel")
...@@ -199,7 +211,7 @@ async def connection(ws, path): ...@@ -199,7 +211,7 @@ async def connection(ws, path):
user["other_user"]["ws"].send(sent_msg1), user["other_user"]["ws"].send(sent_msg1),
user["other_user"]["ws"].send(sent_msg2) user["other_user"]["ws"].send(sent_msg2)
]) ])
elif type == "gameend": elif msg_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"
...@@ -222,7 +234,7 @@ async def connection(ws, path): ...@@ -222,7 +234,7 @@ async def connection(ws, path):
ws.send(status_event()) ws.send(status_event())
]) ])
elif action == "invite": elif action == "invite":
if type == "leave": if msg_type == "leave":
# Cancel session invite # Cancel session invite
if user["session"] in server_status["invites"]: if user["session"] in server_status["invites"]:
del server_status["invites"][user["session"]] del server_status["invites"][user["session"]]
...@@ -243,11 +255,11 @@ async def connection(ws, path): ...@@ -243,11 +255,11 @@ async def connection(ws, path):
ws.send(msgobj("left")), ws.send(msgobj("left")),
ws.send(status_event()) ws.send(status_event())
]) ])
elif type == "songsel" and "other_user" in user: elif msg_type == "songsel" and "other_user" in user:
if "ws" in user["other_user"]: if "ws" in user["other_user"]:
user["action"] = "songsel" user["action"] = "songsel"
user["other_user"]["action"] = "songsel" user["other_user"]["action"] = "songsel"
sent_msg = msgobj(type) sent_msg = msgobj(msg_type)
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(sent_msg),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(sent_msg)
...@@ -262,15 +274,22 @@ async def connection(ws, path): ...@@ -262,15 +274,22 @@ async def connection(ws, path):
elif action == "songsel": elif action == "songsel":
# Session song selection # Session song selection
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
if type == "songsel" or type == "catjump": if msg_type == "songsel" or msg_type == "catjump":
# Change song select position # Change song select position
if user["other_user"]["action"] == "songsel": if user["other_user"]["action"] == "songsel" and type(value) is dict:
sent_msg = msgobj(type, value) value["player"] = user["player"]
sent_msg = msgobj(msg_type, value)
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(sent_msg),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(sent_msg)
]) ])
elif type == "join": elif msg_type == "crowns" or msg_type == "getcrowns":
if user["other_user"]["action"] == "songsel":
sent_msg = msgobj(msg_type, value)
await asyncio.wait([
user["other_user"]["ws"].send(sent_msg)
])
elif msg_type == "join":
# Start game # Start game
if value == None: if value == None:
continue continue
...@@ -282,8 +301,8 @@ async def connection(ws, path): ...@@ -282,8 +301,8 @@ async def connection(ws, path):
user["action"] = "loading" user["action"] = "loading"
user["other_user"]["action"] = "loading" user["other_user"]["action"] = "loading"
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameload", user["other_user"]["gamediff"])), ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})),
user["other_user"]["ws"].send(msgobj("gameload", diff)) user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff}))
]) ])
else: else:
user["action"] = "waiting" user["action"] = "waiting"
...@@ -292,7 +311,7 @@ async def connection(ws, path): ...@@ -292,7 +311,7 @@ async def connection(ws, path):
"id": id, "id": id,
"diff": diff "diff": diff
}])) }]))
elif type == "gameend": elif msg_type == "gameend":
# User wants to disconnect # User wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["session"] = False user["session"] = False
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Taiko Web Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<link href="/src/css/admin.css" rel="stylesheet">
</head>
<body>
<header>
<div class="nav">
<a href="/admin/songs">Songs</a>
</div>
</header>
<main>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
</body>
</html>
{% extends 'admin.html' %}
{% block content %}
<h1>{{ song.title }} <small>(ID: {{ song.id }})</small></h1>
{% for cat, message in get_flashed_messages(with_categories=true) %}
<div class="message{% if cat %} message-{{cat}}{% endif %}">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"{% if song.enabled %} checked{% endif %}{% if admin.user_level < 100 %} disabled {% endif %}><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="{{song.title or ''}}" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="{{song.title_lang.ja or ''}}" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="{{song.title_lang.en or ''}}" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="{{song.title_lang.cn or ''}}" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="{{song.title_lang.tw or ''}}" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="{{song.title_lang.ko or ''}}" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="{{song.subtitle or ''}}" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="{{song.subtitle_lang.ja or ''}}" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="{{song.subtitle_lang.en or ''}}" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="{{song.subtitle_lang.cn or ''}}" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="{{song.subtitle_lang.tw or ''}}" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="{{song.subtitle_lang.ko or ''}}" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="{{song.courses.easy.stars}}" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"{% if song.courses.easy.branch %} checked{% endif %}><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="{{song.courses.normal.stars}}" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"{% if song.courses.normal.branch %} checked{% endif %}><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="{{song.courses.hard.stars}}" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"{% if song.courses.hard.branch %} checked{% endif %}><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="{{song.courses.oni.stars}}" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"{% if song.courses.oni.branch %} checked{% endif %}><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="{{song.courses.ura.stars}}" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"{% if song.courses.ura.branch %} checked{% endif %}><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}"{% if song.category_id == category.id %} selected{% endif %}>{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja"{% if song.type == 'tja' %} selected{% endif %}>TJA</option>
<option value="osu"{% if song.type == 'osu' %} selected{% endif %}>osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="{{song.offset or '0'}}" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}"{% if song.skin_id == skin.id %} selected{% endif %}>{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="{{song.preview or '0'}}" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="{{song.volume or '1.0'}}" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}"{% if song.maker_id == maker.id %} selected{% endif %}>{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="hash">Hash</label></p>
<input type="text" id="hash" value="{{song.hash}}" name="hash"> <span class="checkbox"><input type="checkbox" name="gen_hash" id="gen_hash"{><label for="gen_hash"> Generate</label></span>
</div>
<button type="submit" class="save-song">Save</button>
</form>
{% if admin.user_level >= 100 %}
<form class="delete-song" method="post" action="/admin/songs/{{song.id}}/delete" onsubmit="return confirm('Are you sure you wish to delete this song?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Delete song</button>
</form>
{% endif %}
</div>
{% endblock %}
{% extends 'admin.html' %}
{% block content %}
<h1>New song</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja">TJA</option>
<option value="osu">osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}">{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}">{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="save-song">Save</button>
</form>
</div>
{% endblock %}
{% extends 'admin.html' %}
{% block content %}
{% if admin.user_level >= 100 %}
<a href="/admin/songs/new" class="side-button">New song</a>
{% endif %}
<h1>Songs</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
{% for song in songs %}
<a href="/admin/songs/{{ song.id }}" class="song-link">
<div class="song">
{% if song.title_lang.en %}
<p>{{ song.title_lang.en }} <small>({{ song.title }})</small></p>
{% else %}
<p>{{ song.title }}</p>
{% endif %}
</div>
</a>
{% endfor %}
{% endblock %}
@echo off @echo off
( (
git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%%y.%%m.%%d" git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\"}" --date="format:%%y.%%m.%%d"
) > ../version.json ) > ../version.json
git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%y.%m.%d" > ../version.json #!/bin/bash
toplevel=$( git rev-parse --show-toplevel )
git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\"}" --date="format:%y.%m.%d" > "$toplevel/version.json"
#!/bin/bash
./tools/get_version.sh
#!/bin/bash
./tools/get_version.sh
#!/bin/bash
./tools/get_version.sh
#!/bin/bash
./tools/get_version.sh
#!/usr/bin/env python3
# Migrate old SQLite taiko.db to MongoDB
import sqlite3
from pymongo import MongoClient
import os,sys,inspect
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parent_dir = os.path.dirname(current_dir)
sys.path.insert(0, parent_dir)
import config
client = MongoClient(config.MONGO['host'])
client.drop_database(config.MONGO['database'])
db = client[config.MONGO['database']]
sqdb = sqlite3.connect('taiko.db')
sqdb.row_factory = sqlite3.Row
curs = sqdb.cursor()
def migrate_songs():
curs.execute('select * from songs order by id')
rows = curs.fetchall()
for row in rows:
song = {
'id': row['id'],
'title': row['title'],
'title_lang': {'ja': row['title'], 'en': None, 'cn': None, 'tw': None, 'ko': None},
'subtitle': row['subtitle'],
'subtitle_lang': {'ja': row['subtitle'], 'en': None, 'cn': None, 'tw': None, 'ko': None},
'courses': {'easy': None, 'normal': None, 'hard': None, 'oni': None, 'ura': None},
'enabled': True if row['enabled'] else False,
'category_id': row['category'],
'type': row['type'],
'offset': row['offset'] or 0,
'skin_id': row['skin_id'],
'preview': row['preview'] or 0,
'volume': row['volume'] or 1.0,
'maker_id': row['maker_id'],
'hash': row['hash'],
'order': row['id']
}
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
if row[diff]:
spl = row[diff].split(' ')
branch = False
if len(spl) > 1 and spl[1] == 'B':
branch = True
song['courses'][diff] = {'stars': int(spl[0]), 'branch': branch}
if row['title_lang']:
langs = row['title_lang'].splitlines()
for lang in langs:
spl = lang.split(' ', 1)
if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']:
song['title_lang'][spl[0]] = spl[1]
else:
song['title_lang']['en'] = lang
if row['subtitle_lang']:
langs = row['subtitle_lang'].splitlines()
for lang in langs:
spl = lang.split(' ', 1)
if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']:
song['subtitle_lang'][spl[0]] = spl[1]
else:
song['subtitle_lang']['en'] = lang
db.songs.insert_one(song)
last_song = song['id']
db.seq.insert_one({'name': 'songs', 'value': last_song})
def migrate_makers():
curs.execute('select * from makers')
rows = curs.fetchall()
for row in rows:
db.makers.insert_one({
'id': row['maker_id'],
'name': row['name'],
'url': row['url']
})
def migrate_categories():
curs.execute('select * from categories')
rows = curs.fetchall()
for row in rows:
db.categories.insert_one({
'id': row['id'],
'title': row['title']
})
def migrate_song_skins():
curs.execute('select * from song_skins')
rows = curs.fetchall()
for row in rows:
db.song_skins.insert_one({
'id': row['id'],
'name': row['name'],
'song': row['song'],
'stage': row['stage'],
'don': row['don']
})
if __name__ == '__main__':
migrate_songs()
migrate_makers()
migrate_categories()
migrate_song_skins()
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