From 2af924a9854ee5b043d55726edec98013fbb700d Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Fri, 13 Mar 2020 05:34:54 +0300 Subject: [PATCH 01/41] Add everything for accounts --- public/src/css/view.css | 77 +++++ public/src/js/account.js | 482 +++++++++++++++++++++++++++++ public/src/js/assets.js | 8 +- public/src/js/canvasdraw.js | 102 +++++- public/src/js/game.js | 4 +- public/src/js/importsongs.js | 49 +-- public/src/js/lib/js.cookie.min.js | 2 + public/src/js/loader.js | 134 ++++---- public/src/js/loadsong.js | 3 +- public/src/js/main.js | 1 + public/src/js/p2.js | 5 + public/src/js/pageevents.js | 3 + public/src/js/scoresheet.js | 28 ++ public/src/js/scorestorage.js | 78 ++++- public/src/js/session.js | 5 +- public/src/js/songselect.js | 208 ++++++++++--- public/src/js/strings.js | 109 +++++++ public/src/js/view.js | 55 ++++ public/src/views/account.html | 33 ++ public/src/views/login.html | 24 ++ server.py | 27 +- 21 files changed, 1291 insertions(+), 146 deletions(-) create mode 100644 public/src/js/account.js create mode 100644 public/src/js/lib/js.cookie.min.js create mode 100644 public/src/views/account.html create mode 100644 public/src/views/login.html diff --git a/public/src/css/view.css b/public/src/css/view.css index 76ed3f4..c1f254e 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -291,3 +291,80 @@ kbd{ .left-buttons .taibtn{ 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; +} diff --git a/public/src/js/account.js b/public/src/js/account.js new file mode 100644 index 0000000..35ea69d --- /dev/null +++ b/public/src/js/account.js @@ -0,0 +1,482 @@ +class Account{ + constructor(touchEnabled){ + this.touchEnabled = touchEnabled + cancelTouch = false + this.locked = false + + if(account.loggedIn){ + this.accountForm() + }else{ + this.loginForm() + } + this.selected = this.items.length - 1 + + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + back: ["escape"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + "confirm": ["b", "ls", "rs"], + "previous": ["u", "l", "lb", "lt", "lsu", "lsl"], + "next": ["d", "r", "rb", "rt", "lsd", "lsr"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) + + pageEvents.send("account", account.loggedIn) + } + accountForm(){ + loader.changePage("account", true) + this.mode = "account" + + this.setAltText(this.getElement("view-title"), account.username) + this.items = [] + this.inputForms = [] + this.shownDiv = "" + + this.getElement("displayname-hint").innerText = strings.account.displayName + this.displayname = this.getElement("displayname") + this.displayname.placeholder = strings.account.displayName + this.displayname.value = account.displayName + this.inputForms.push(this.displayname) + + this.accountPassButton = this.getElement("accountpass-btn") + this.setAltText(this.accountPassButton, strings.account.changePassword) + pageEvents.add(this.accountPassButton, ["click", "touchstart"], event => { + this.showDiv(event, "pass") + }) + this.accountPass = this.getElement("accountpass-form") + for(var i = 0; i < this.accountPass.length; i++){ + this.accountPass[i].placeholder = strings.account.currentNewRepeat[i] + this.inputForms.push(this.accountPass[i]) + } + this.accountPassDiv = this.getElement("accountpass-div") + + this.accountDelButton = this.getElement("accountdel-btn") + this.setAltText(this.accountDelButton, strings.account.deleteAccount) + pageEvents.add(this.accountDelButton, ["click", "touchstart"], event => { + this.showDiv(event, "del") + }) + this.accountDel = this.getElement("accountdel-form") + this.accountDel.password.placeholder = strings.account.verifyPassword + this.inputForms.push(this.accountDel.password) + this.accountDelDiv = this.getElement("accountdel-div") + + this.logoutButton = this.getElement("logout-btn") + this.setAltText(this.logoutButton, strings.account.logout) + pageEvents.add(this.logoutButton, ["mousedown", "touchstart"], this.onLogout.bind(this)) + this.items.push(this.logoutButton) + + this.endButton = this.getElement("view-end-button") + this.setAltText(this.endButton, strings.account.cancel) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items.push(this.endButton) + + this.saveButton = this.getElement("save-btn") + this.setAltText(this.saveButton, strings.account.save) + pageEvents.add(this.saveButton, ["mousedown", "touchstart"], this.onSave.bind(this)) + this.items.push(this.saveButton) + + for(var i = 0; i < this.inputForms.length; i++){ + pageEvents.add(this.inputForms[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this)) + } + } + showDiv(event, div){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + var otherDiv = this.shownDiv && this.shownDiv !== div + var display = this.shownDiv === div ? "" : "block" + this.shownDiv = display ? div : "" + switch(div){ + case "pass": + if(otherDiv){ + this.accountDelDiv.style.display = "" + } + this.accountPassDiv.style.display = display + break + case "del": + if(otherDiv){ + this.accountPassDiv.style.display = "" + } + this.accountDelDiv.style.display = display + break + } + } + loginForm(register, fromSwitch){ + loader.changePage("login", true) + this.mode = register ? "register" : "login" + + this.setAltText(this.getElement("view-title"), strings.account[this.mode]) + this.items = [] + this.form = this.getElement("login-form") + this.getElement("username-hint").innerText = strings.account.username + this.form.username.placeholder = strings.account.enterUsername + this.getElement("password-hint").innerText = strings.account.password + this.form.password.placeholder = strings.account.enterPassword + this.password2 = this.getElement("password2-div") + this.remember = this.getElement("remember-div") + this.getElement("remember-label").appendChild(document.createTextNode(strings.account.remember)) + this.loginButton = this.getElement("login-btn") + this.registerButton = this.getElement("register-btn") + + if(register){ + var pass2 = document.createElement("input") + pass2.type = "password" + pass2.name = "password2" + pass2.required = true + pass2.placeholder = strings.account.repeatPassword + this.password2.appendChild(pass2) + this.password2.style.display = "block" + this.remember.style.display = "none" + this.setAltText(this.loginButton, strings.account.registerAccount) + this.setAltText(this.registerButton, strings.account.login) + }else{ + this.setAltText(this.loginButton, strings.account.login) + this.setAltText(this.registerButton, strings.account.register) + } + + pageEvents.add(this.form, "submit", this.onLogin.bind(this)) + pageEvents.add(this.loginButton, ["mousedown", "touchstart"], this.onLogin.bind(this)) + + pageEvents.add(this.registerButton, ["mousedown", "touchstart"], this.onSwitchMode.bind(this)) + this.items.push(this.registerButton) + if(!register){ + this.items.push(this.loginButton) + } + + for(var i = 0; i < this.form.length; i++){ + pageEvents.add(this.form[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this)) + } + + this.endButton = this.getElement("view-end-button") + this.setAltText(this.endButton, strings.account.back) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items.push(this.endButton) + if(fromSwitch){ + this.selected = 0 + this.endButton.classList.remove("selected") + this.registerButton.classList.add("selected") + } + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) + } + keyPressed(pressed, name){ + if(!pressed || this.locked){ + return + } + var selected = this.items[this.selected] + if(name === "confirm"){ + if(selected === this.endButton){ + this.onEnd() + }else if(selected === this.registerButton){ + this.onSwitchMode() + }else if(selected === this.loginButton){ + this.onLogin() + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + }else if(name === "back"){ + this.onEnd() + } + } + mod(length, index){ + return ((index % length) + length) % length + } + onFormPress(event){ + event.stopPropagation() + if(event.type === "keypress" && event.keyCode === 13){ + if(this.mode === "account"){ + this.onSave() + }else{ + this.onLogin() + } + } + } + onSwitchMode(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + this.clean(true) + this.loginForm(this.mode === "login", true) + } + onLogin(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + var obj = { + username: this.form.username.value, + password: this.form.password.value + } + if(!obj.username || !obj.password){ + alert(strings.account.cannotBeEmpty.replace("%s", strings.account[!obj.username ? "username" : "password"])) + return + } + if(this.mode === "login"){ + obj.remember = this.form.remember.checked + }else{ + if(obj.password !== this.form.password2.value){ + alert(strings.account.passwordsDoNotMatch) + return + } + } + this.request(this.mode, obj).then(response => { + account.loggedIn = true + account.username = response.username + account.displayName = response.display_name + var loadScores = scores => { + scoreStorage.load(scores) + this.onEnd(false, true) + pageEvents.send("login", account.username) + } + if(this.mode === "login"){ + this.request("scores/get").then(response => { + loadScores(response.scores) + }, () => { + loadScores({}) + }) + }else{ + scoreStorage.save().catch(() => {}).finally(() => { + this.onEnd(false, true) + pageEvents.send("login", account.username) + }) + } + }, response => { + if(response && response.status === "error" && response.message){ + alert(response.message) + }else{ + alert(strings.account.error) + } + }) + } + onLogout(){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + account.loggedIn = false + delete account.username + delete account.displayName + var loadScores = scores => { + Cookies.remove("token") + scoreStorage.load() + this.onEnd(false, true) + pageEvents.send("logout") + } + this.request("logout").then(response => { + loadScores() + }, () => { + loadScores() + }) + } + onSave(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + var promises = [] + var noNameChange = false + if(this.shownDiv === "pass"){ + var passwords = [] + for(var i = 0; i < this.accountPass.length; i++){ + passwords.push(this.accountPass[i].value) + } + if(passwords[1] === passwords[2]){ + promises.push(this.request("account/password", { + current_password: passwords[0], + new_password: passwords[1] + })) + }else{ + alert(strings.account.passwordsDoNotMatch) + return + } + } + if(this.shownDiv === "del" && this.accountDel.password.value){ + noNameChange = true + promises.push(this.request("account/remove", { + password: this.accountDel.password.value + }).then(() => { + account.loggedIn = false + delete account.username + delete account.displayName + Cookies.remove("token") + scoreStorage.load() + pageEvents.send("logout") + return Promise.resolve + })) + } + var newName = this.displayname.value.trim() + if(!noNameChange && newName !== account.displayName){ + promises.push(this.request("account/display_name", { + display_name: newName + })) + } + var error = false + var errorFunc = response => { + if(error){ + return + } + if(response && response.message){ + alert(response.message) + }else{ + alert(strings.account.error) + } + } + Promise.all(promises).then(() => { + this.onEnd(false, true) + }, errorFunc).catch(errorFunc) + } + onEnd(event, noSound){ + var touched = false + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + this.clean() + assets.sounds["se_don"].play() + setTimeout(() => { + new SongSelect(false, false, touched) + }, 500) + } + request(url, obj){ + this.lock(true) + return new Promise((resolve, reject) => { + var request = new XMLHttpRequest() + request.open(obj ? "POST" : "GET", "api/" + url) + pageEvents.load(request).then(() => { + this.lock(false) + if(request.status !== 200){ + reject() + return + } + try{ + var json = JSON.parse(request.response) + }catch(e){ + reject() + return + } + if(json.status === "ok"){ + resolve(json) + }else{ + reject(json) + } + }, () => { + this.lock(false) + reject() + }) + if(obj){ + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") + request.send(JSON.stringify(obj)) + }else{ + request.send() + } + }) + } + lock(isLocked){ + this.locked = isLocked + if(this.mode === "login" || this.mode === "register"){ + for(var i = 0; i < this.form.length; i++){ + this.form[i].disabled = isLocked + } + }else if(this.mode === "account"){ + for(var i = 0; i < this.inputForms.length; i++){ + this.inputForms[i].disabled = isLocked + } + } + } + clean(eventsOnly){ + if(!eventsOnly){ + cancelTouch = true + this.keyboard.clean() + this.gamepad.clean() + } + if(this.mode === "account"){ + pageEvents.remove(this.accounPassButton, ["click", "touchstart"]) + pageEvents.remove(this.accountDelButton, ["click", "touchstart"]) + pageEvents.remove(this.logoutButton, ["mousedown", "touchstart"]) + pageEvents.remove(this.saveButton, ["mousedown", "touchstart"]) + for(var i = 0; i < this.inputForms.length; i++){ + pageEvents.remove(this.inputForms[i], ["keydown", "keyup", "keypress"]) + } + this.accountPass.reset() + this.accountDel.reset() + delete this.displayname + delete this.accountPassButton + delete this.accountPass + delete this.accountPassDiv + delete this.accountDelButton + delete this.accountDel + delete this.accountDelDiv + delete this.logoutButton + delete this.saveButton + delete this.inputForms + }else if(this.mode === "login" || this.mode === "register"){ + if(!eventsOnly){ + this.form.reset() + } + pageEvents.remove(this.form, "submit") + pageEvents.remove(this.loginButton, ["mousedown", "touchstart"]) + pageEvents.remove(this.registerButton, ["mousedown", "touchstart"]) + for(var i = 0; i < this.form.length; i++){ + pageEvents.remove(this.registerButton, ["keydown", "keyup", "keypress"]) + } + delete this.form + delete this.password2 + delete this.remember + delete this.loginButton + delete this.registerButton + } + pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + delete this.endButton + delete this.items + } +} diff --git a/public/src/js/assets.js b/public/src/js/assets.js index 31e4d8d..f7e3ee7 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -1,6 +1,7 @@ var assets = { "js": [ "lib/md5.min.js", + "lib/js.cookie.min.js", "loadsong.js", "parseosu.js", "titlescreen.js", @@ -31,7 +32,8 @@ var assets = { "importsongs.js", "logo.js", "settings.js", - "scorestorage.js" + "scorestorage.js", + "account.js" ], "css": [ "main.css", @@ -137,7 +139,9 @@ var assets = { "about.html", "debug.html", "session.html", - "settings.html" + "settings.html", + "account.html", + "login.html" ], "songs": [], diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index ca984ad..6da8266 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -706,12 +706,12 @@ }) }else if(r.smallHiragana.test(symbol)){ // 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)){ // 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{ - 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 @@ if(config.letterSpacing){ symbol.w += config.letterSpacing } + if(config.kanaSpacing && symbol.kana){ + symbol.w += config.kanaSpacing + } drawnWidth += symbol.w * mul } @@ -1549,6 +1552,99 @@ 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){ if(amount >= 1){ return callback(ctx) diff --git a/public/src/js/game.js b/public/src/js/game.js index 8fcdd99..da9534e 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -505,7 +505,9 @@ class Game{ var musicDuration = duration * 1000 - this.controller.offset if(this.musicFadeOut === 0){ if(this.controller.multiplayer === 1){ - p2.send("gameresults", this.getGlobalScore()) + var obj = this.getGlobalScore() + obj.name = account.loggedIn ? account.displayName : strings.defaultName + p2.send("gameresults", obj) } this.musicFadeOut++ }else if(this.musicFadeOut === 1 && ms >= started + 1600){ diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index 20a431a..992e96d 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -202,12 +202,16 @@ var tja = new ParseTja(data, "oni", 0, 0, true) var songObj = { id: index + 1, + order: index + 1, type: "tja", chart: file, - stars: [], + stars: {}, music: "muted" } + var coursesAdded = false var titleLang = {} + var titleLangAdded = false + var subtitleLangAdded = false var subtitleLang = {} var dir = file.webkitRelativePath.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) @@ -221,7 +225,11 @@ } songObj.subtitle = subtitle 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){ songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music } @@ -264,32 +272,27 @@ } if(meta["title" + id]){ titleLang[id] = meta["title" + id] + titleLangAdded = true }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ titleLang[id] = this.songTitle[songTitle][id] + ura + titleLangAdded = true } if(meta["subtitle" + id]){ subtitleLang[id] = meta["subtitle" + id] + subtitleLangAdded = true } } } - var titleLangArray = [] - for(var id in titleLang){ - titleLangArray.push(id + " " + titleLang[id]) + if(titleLangAdded){ + songObj.title_lang = titleLang } - if(titleLangArray.length !== 0){ - songObj.title_lang = titleLangArray.join("\n") - } - var subtitleLangArray = [] - for(var id in subtitleLang){ - subtitleLangArray.push(id + " " + subtitleLang[id]) - } - if(subtitleLangArray.length !== 0){ - songObj.subtitle_lang = subtitleLangArray.join("\n") + if(subtitleLangAdded){ + songObj.subtitle_lang = subtitleLang } if(!songObj.category){ 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 } var hash = md5.base64(event.target.result).slice(0, -2) @@ -316,12 +319,20 @@ dir = dir.slice(0, dir.lastIndexOf("/") + 1) var songObj = { id: index + 1, + order: index + 1, type: "osu", chart: file, 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, - 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" } var filename = file.name.slice(0, file.name.lastIndexOf(".")) @@ -333,7 +344,9 @@ suffix = " " + matches[0] } 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{ songObj.title = filename } diff --git a/public/src/js/lib/js.cookie.min.js b/public/src/js/lib/js.cookie.min.js new file mode 100644 index 0000000..a0e6820 --- /dev/null +++ b/public/src/js/lib/js.cookie.min.js @@ -0,0 +1,2 @@ +/*! js-cookie v3.0.0-rc.0 | MIT */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t { @@ -155,65 +156,92 @@ class Loader{ } })) - var readyEvent = "normal" - var songId - var hashLower = location.hash.toLowerCase() - p2 = new P2Connection() - if(hashLower.startsWith("#song=")){ - var number = parseInt(location.hash.slice(6)) - if(number > 0){ - songId = number - readyEvent = "song-id" + if(gameConfig._accounts){ + var token = Cookies.get("token") + if(token){ + this.addPromise(this.ajax("/api/scores/get").then(response => { + response = JSON.parse(response) + if(response.status === "ok"){ + account.loggedIn = true + account.username = response.username + account.displayName = response.display_name + scoreStorage.load(response.scores) + pageEvents.send("login", account.username) + } + })) + }else{ + this.assetLoaded() } - }else if(location.hash.length === 6){ - p2.hashLock = true - this.addPromise(new Promise(resolve => { - p2.open() - pageEvents.add(p2, "message", response => { - if(response.type === "session"){ - pageEvents.send("session-start", "invited") - readyEvent = "session-start" - resolve() - }else if(response.type === "gameend"){ - p2.hash("") - p2.hashLock = false - readyEvent = "session-expired" - resolve() - } - }) - p2.send("invite", location.hash.slice(1).toLowerCase()) - setTimeout(() => { - if(p2.socket.readyState !== 1){ - p2.hash("") - p2.hashLock = false - resolve() - } - }, 10000) - }).then(() => { - pageEvents.remove(p2, "message") - })) - }else{ - p2.hash("") } settings = new Settings() pageEvents.setKbd() - scoreStorage = new ScoreStorage() - for(var i in assets.songsDefault){ - var song = assets.songsDefault[i] - if(!song.hash){ - song.hash = song.title - } - scoreStorage.songTitles[song.title] = song.hash - var score = scoreStorage.get(song.hash, false, true) - if(score){ - score.title = song.title - } - } Promise.all(this.promises).then(() => { - this.canvasTest.drawAllImages().then(result => { + if(!account.loggedIn){ + scoreStorage.load() + } + for(var i in assets.songsDefault){ + var song = assets.songsDefault[i] + if(!song.hash){ + song.hash = song.title + } + scoreStorage.songTitles[song.title] = song.hash + var score = scoreStorage.get(song.hash, false, true) + if(score){ + score.title = song.title + } + } + var promises = [] + + var readyEvent = "normal" + var songId + var hashLower = location.hash.toLowerCase() + p2 = new P2Connection() + if(hashLower.startsWith("#song=")){ + var number = parseInt(location.hash.slice(6)) + if(number > 0){ + songId = number + readyEvent = "song-id" + } + }else if(location.hash.length === 6){ + p2.hashLock = true + promises.push(new Promise(resolve => { + p2.open() + pageEvents.add(p2, "message", response => { + if(response.type === "session"){ + pageEvents.send("session-start", "invited") + readyEvent = "session-start" + resolve() + }else if(response.type === "gameend"){ + p2.hash("") + p2.hashLock = false + readyEvent = "session-expired" + resolve() + } + }) + p2.send("invite", { + id: location.hash.slice(1).toLowerCase(), + name: account.loggedIn ? account.displayName : null + }) + setTimeout(() => { + if(p2.socket.readyState !== 1){ + p2.hash("") + p2.hashLock = false + resolve() + } + }, 10000) + }).then(() => { + pageEvents.remove(p2, "message") + })) + }else{ + p2.hash("") + } + + promises.push(this.canvasTest.drawAllImages()) + + Promise.all(promises).then(result => { perf.allImg = result perf.load = Date.now() - this.startTime this.canvasTest.clean() diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index b40461b..3e5b460 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -297,7 +297,8 @@ class LoadSong{ }) p2.send("join", { id: song.folder, - diff: song.difficulty + diff: song.difficulty, + name: account.loggedIn ? account.displayName : null }) }else{ this.clean() diff --git a/public/src/js/main.js b/public/src/js/main.js index 3a393a4..c01209c 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -84,6 +84,7 @@ var strings var vectors var settings var scoreStorage +var account = {} pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ diff --git a/public/src/js/p2.js b/public/src/js/p2.js index a0c266d..0857381 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -3,6 +3,7 @@ class P2Connection{ this.closed = true this.lastMessages = {} this.otherConnected = false + this.name = null this.allEvents = new Map() this.addEventListener("message", this.message.bind(this)) this.currentHash = "" @@ -123,6 +124,7 @@ class P2Connection{ this.hash("") this.hashLock = false } + this.name = null break case "gameresults": this.results = {} @@ -151,6 +153,9 @@ class P2Connection{ this.otherConnected = true this.session = true break + case "name": + this.name = (response.value || "").toString() || null + break } } onhashchange(){ diff --git a/public/src/js/pageevents.js b/public/src/js/pageevents.js index 56c0d1a..46c0a12 100644 --- a/public/src/js/pageevents.js +++ b/public/src/js/pageevents.js @@ -86,6 +86,9 @@ class PageEvents{ }) } keyEvent(event){ + if(!("key" in event)){ + return + } if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){ this.lastKeyEvent = Date.now() event.preventDefault() diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index 4e5cca6..a2a62c4 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -39,6 +39,7 @@ class Scoresheet{ this.draw = new CanvasDraw(noSmoothing) this.canvasCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) this.keyboard = new Keyboard({ confirm: ["enter", "space", "esc", "don_l", "don_r"] @@ -208,6 +209,7 @@ class Scoresheet{ this.canvas.style.height = (winH / this.pixelRatio) + "px" this.canvasCache.resize(winW / ratio, 80 + 1, ratio) + this.nameplateCache.resize(274, 134, ratio + 0.2) if(!this.multiplayer){ this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio) @@ -233,6 +235,9 @@ class Scoresheet{ if(!this.canvasCache.canvas){ this.canvasCache.resize(winW / ratio, 80 + 1, ratio) } + if(!this.nameplateCache.canvas){ + this.nameplateCache.resize(274, 67, ratio + 0.2) + } } this.winW = winW this.winH = winH @@ -450,6 +455,29 @@ class Scoresheet{ ctx.fillText(text, 395, 308) ctx.miterLimit = 10 + if(p === 0){ + var name = account.loggedIn ? account.displayName : strings.defaultName + }else{ + var name = results.name + } + this.nameplateCache.get({ + ctx: ctx, + x: 259, + y: 92, + w: 273, + h: 66, + id: p.toString() + "p", + }, ctx => { + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: name, + font: this.font, + blue: p === 1 + }) + }) + if(this.controller.autoPlayEnabled){ ctx.drawImage(assets.image["badge_auto"], 431, 311, 34, 34 diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index 87820f5..55dfe02 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -5,17 +5,22 @@ class ScoreStorage{ this.difficulty = ["oni", "ura", "hard", "normal", "easy"] this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"] this.crownValue = ["", "silver", "gold"] - this.load() } - load(){ + load(strings){ this.scores = {} - this.scoreStrings = {} - try{ - var localScores = localStorage.getItem("scoreStorage") - if(localScores){ - this.scoreStrings = JSON.parse(localScores) - } - }catch(e){} + if(strings){ + this.scoreStrings = strings + }else if(account.loggedIn){ + return + }else{ + this.scoreStrings = {} + try{ + var localScores = localStorage.getItem("scoreStorage") + if(localScores){ + this.scoreStrings = JSON.parse(localScores) + } + }catch(e){} + } for(var hash in this.scoreStrings){ var scoreString = this.scoreStrings[hash] var songAdded = false @@ -46,16 +51,22 @@ class ScoreStorage{ } } } - save(){ + save(localOnly){ for(var hash in this.scores){ this.writeString(hash) } this.write() + return this.sendToServer({ + scores: this.scoreStrings, + is_import: true + }) } write(){ - try{ - localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) - }catch(e){} + if(!account.loggedIn){ + try{ + localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) + }catch(e){} + } } writeString(hash){ var score = this.scores[hash] @@ -112,6 +123,11 @@ class ScoreStorage{ this.scores[hash][difficulty] = scoreObject this.writeString(hash) this.write() + var obj = {} + obj[hash] = this.scoreStrings[hash] + this.sendToServer({ + scores: obj + }).catch(() => this.add.apply(this, arguments)) } template(){ var template = {crown: ""} @@ -146,6 +162,42 @@ class ScoreStorage{ delete this.scoreStrings[hash] } this.write() + this.sendToServer({ + scores: this.scoreStrings, + is_import: true + }) + } + } + sendToServer(obj, retry){ + if(account.loggedIn){ + 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){ + account.loggedIn = false + delete account.username + delete account.displayName + Cookies.remove("token") + 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.send(JSON.stringify(obj)) + return promise + }else{ + return Promise.resolve() } } } diff --git a/public/src/js/session.js b/public/src/js/session.js index 0f08c67..f9d267e 100644 --- a/public/src/js/session.js +++ b/public/src/js/session.js @@ -34,7 +34,10 @@ class Session{ pageEvents.send("session-start", "host") } }) - p2.send("invite") + p2.send("invite", { + id: null, + name: account.loggedIn ? account.displayName : null + }) pageEvents.send("session") } getElement(name){ diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 85df8db..64b65de 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -116,7 +116,7 @@ class SongSelect{ originalTitle: song.title, subtitle: subtitle, skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default, - stars: song.stars, + courses: song.courses, category: song.category, preview: song.preview || 0, type: song.type, @@ -126,14 +126,19 @@ class SongSelect{ volume: song.volume, maker: song.maker, canJump: true, - hash: song.hash || song.title + hash: song.hash || song.title, + order: song.order }) } this.songs.sort((a, b) => { var catA = a.category in this.songSkin ? this.songSkin[a.category] : this.songSkin.default var catB = b.category in this.songSkin ? this.songSkin[b.category] : this.songSkin.default if(catA.sort === catB.sort){ - return a.id > b.id ? 1 : -1 + if(a.order === b.order){ + return a.id > b.id ? 1 : -1 + }else{ + return a.order > b.order ? 1 : -1 + } }else{ return catA.sort > catB.sort ? 1 : -1 } @@ -226,6 +231,7 @@ class SongSelect{ this.difficultyCache = new CanvasCache(noSmoothing) this.sessionCache = new CanvasCache(noSmoothing) this.currentSongCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] @@ -450,7 +456,11 @@ class SongSelect{ if(this.state.screen === "song"){ if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ this.categoryJump(mouse.x < 640 ? -1 : 1) - }else if(mouse.x > 641 && mouse.y > 603){ + }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig._accounts){ + this.toAccount() + }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ + this.toSession() + }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ this.toSession() }else{ var moveBy = this.songSelMouse(mouse.x, mouse.y) @@ -501,11 +511,15 @@ class SongSelect{ if(this.state.screen === "song"){ if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext" - }else if(mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ + }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig._accounts){ + moveTo = "account" + }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ + moveTo = "session" + }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ moveTo = "session" }else{ var moveTo = this.songSelMouse(mouse.x, mouse.y) - if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].stars){ + if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].courses){ this.state.moveMS = this.getMS() - this.songSelecting.speed } } @@ -544,7 +558,7 @@ class SongSelect{ var dir = x > 0 ? 1 : -1 x = Math.abs(x) var selectedWidth = this.songAsset.selectedWidth - if(!this.songs[this.selectedSong].stars){ + if(!this.songs[this.selectedSong].courses){ selectedWidth = this.songAsset.width } var moveBy = Math.ceil((x - selectedWidth / 2 - this.songAsset.marginLeft / 2) / (this.songAsset.width + this.songAsset.marginLeft)) * dir @@ -565,7 +579,13 @@ class SongSelect{ }else if(550 < x && x < 1050 && 95 < y && y < 524){ var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length var currentSong = this.songs[this.selectedSong] - if(this.state.ura && moveBy === this.diffOptions.length + 3 || currentSong.stars[moveBy - this.diffOptions.length]){ + if( + this.state.ura + && moveBy === this.diffOptions.length + 3 + || currentSong.courses[ + this.difficultyId[moveBy - this.diffOptions.length] + ] + ){ return moveBy } } @@ -583,7 +603,7 @@ class SongSelect{ }) } }else if(this.state.locked !== 1 || fromP2){ - if(this.songs[this.selectedSong].stars && (this.state.locked === 0 || fromP2)){ + if(this.songs[this.selectedSong].courses && (this.state.locked === 0 || fromP2)){ this.state.moveMS = ms }else{ this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize @@ -645,7 +665,7 @@ class SongSelect{ toSelectDifficulty(fromP2){ var currentSong = this.songs[this.selectedSong] if(p2.session && !fromP2 && currentSong.action !== "random"){ - if(this.songs[this.selectedSong].stars){ + if(this.songs[this.selectedSong].courses){ if(!this.state.selLock){ this.state.selLock = true p2.send("songsel", { @@ -655,7 +675,7 @@ class SongSelect{ } } }else if(this.state.locked === 0 || fromP2){ - if(currentSong.stars){ + if(currentSong.courses){ this.state.screen = "difficulty" this.state.screenMS = this.getMS() this.state.locked = true @@ -677,7 +697,7 @@ class SongSelect{ this.state.locked = true do{ var i = Math.floor(Math.random() * this.songs.length) - }while(!this.songs[i].stars) + }while(!this.songs[i].courses) var moveBy = i - this.selectedSong setTimeout(() => { this.moveToSong(moveBy) @@ -744,17 +764,18 @@ class SongSelect{ }else if(p2.socket.readyState === 1 && !assets.customSongs){ multiplayer = ctrl } + var diff = this.difficultyId[difficulty] new LoadSong({ "title": selectedSong.title, "originalTitle": selectedSong.originalTitle, "folder": selectedSong.id, - "difficulty": this.difficultyId[difficulty], + "difficulty": diff, "category": selectedSong.category, "type": selectedSong.type, "offset": selectedSong.offset, "songSkin": selectedSong.songSkin, - "stars": selectedSong.stars[difficulty], + "stars": selectedSong.courses[diff].stars, "hash": selectedSong.hash }, autoplay, multiplayer, touch) } @@ -797,6 +818,13 @@ class SongSelect{ new SettingsView(this.touchEnabled) }, 500) } + toAccount(){ + this.playSound("se_don") + this.clean() + setTimeout(() => { + new Account(this.touchEnabled) + }, 500) + } toSession(){ if(p2.socket.readyState !== 1 || assets.customSongs){ return @@ -893,6 +921,8 @@ class SongSelect{ var textW = strings.id === "en" ? 350 : 280 this.selectTextCache.resize((textW + 53 + 60 + 1) * 2, this.songAsset.marginTop + 15, ratio + 0.5) + this.nameplateCache.resize(274, 134, ratio + 0.2) + var categories = 0 var lastCategory this.songs.forEach(song => { @@ -921,7 +951,7 @@ class SongSelect{ fontFamily: this.font, x: w / 2, y: 38 / 2, - width: w - 30, + width: id === "sessionend" ? 385 : w - 30, align: "center", baseline: "middle" }, [ @@ -969,7 +999,7 @@ class SongSelect{ } if(screen === "song"){ - if(this.songs[this.selectedSong].stars){ + if(this.songs[this.selectedSong].courses){ selectedWidth = this.songAsset.selectedWidth } @@ -1054,7 +1084,7 @@ class SongSelect{ if(elapsed < resize){ selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width)) }else if(elapsed > resize2){ - this.playBgm(!this.songs[this.selectedSong].stars) + this.playBgm(!this.songs[this.selectedSong].courses) this.state.locked = 1 selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width)) }else{ @@ -1062,7 +1092,7 @@ class SongSelect{ selectedWidth = this.songAsset.width } }else{ - this.playBgm(!this.songs[this.selectedSong].stars) + this.playBgm(!this.songs[this.selectedSong].courses) this.state.locked = 0 } }else if(screen === "difficulty"){ @@ -1071,7 +1101,7 @@ class SongSelect{ this.state.locked = 0 } if(this.state.move){ - var hasUra = currentSong.stars[4] + var hasUra = currentSong.courses.ura var previousSelection = this.selectedDiff do{ if(hasUra && this.state.move > 0){ @@ -1089,12 +1119,12 @@ class SongSelect{ this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move) } }while( - this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length] + this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]] || this.selectedDiff === this.diffOptions.length + 3 && this.state.ura || this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura ) this.state.move = 0 - }else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length]){ + }else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]){ this.selectedDiff = 0 } } @@ -1164,7 +1194,7 @@ class SongSelect{ var currentSong = this.songs[this.selectedSong] var highlight = 0 - if(!currentSong.stars){ + if(!currentSong.courses){ highlight = 2 } if(this.state.moveHover === 0){ @@ -1418,7 +1448,7 @@ class SongSelect{ } } var drawDifficulty = (ctx, i, currentUra) => { - if(currentSong.stars[i] || currentUra){ + if(currentSong.courses[this.difficultyId[i]] || currentUra){ var score = scoreStorage.get(currentSong.hash, false, true) var crownDiff = currentUra ? "ura" : this.difficultyId[i] var crownType = "" @@ -1502,9 +1532,9 @@ class SongSelect{ outlineSize: currentUra ? this.songAsset.letterBorder : 0 }) }) - var songStarsArray = (currentUra ? currentSong.stars[4] : currentSong.stars[i]).toString().split(" ") - var songStars = songStarsArray[0] - var songBranch = songStarsArray[1] === "B" + var songStarsObj = (currentUra ? currentSong.courses.ura : currentSong.courses[this.difficultyId[i]]) + var songStars = songStarsObj.stars + var songBranch = songStarsObj.branch var elapsedMS = this.state.screenMS > this.state.moveMS || !songSel ? this.state.screenMS : this.state.moveMS var fade = ((ms - elapsedMS) % 2000) / 2000 if(songBranch && fade > 0.25 && fade < 0.75){ @@ -1591,8 +1621,8 @@ class SongSelect{ } } } - for(var i = 0; currentSong.stars && i < 4; i++){ - var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.stars[4] && songSel) + for(var i = 0; currentSong.courses && i < 4; i++){ + var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.courses.ura && songSel) if(songSel && currentUra){ drawDifficulty(ctx, i, false) var elapsedMS = this.state.screenMS > this.state.moveMS ? this.state.screenMS : this.state.moveMS @@ -1753,7 +1783,7 @@ class SongSelect{ } } - if(!songSel && currentSong.stars[4]){ + if(!songSel && currentSong.courses.ura){ var fade = ((ms - this.state.screenMS) % 1200) / 1200 var _x = x + 402 + 4 * 100 + fade * 25 var _y = y + 258 @@ -1842,7 +1872,7 @@ class SongSelect{ ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop) var x = 0 var y = frameTop + 603 - var w = frameLeft + 638 + var w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638 var h = 117 + frameTop this.draw.pattern({ ctx: ctx, @@ -1869,7 +1899,81 @@ class SongSelect{ ctx.lineTo(x + w - 4, y + h) ctx.lineTo(x + w - 4, y + 4) ctx.fill() - x = frameLeft + 642 + + this.nameplateCache.get({ + ctx: ctx, + x: frameLeft + 60, + y: frameTop + 640, + w: 273, + h: 66, + id: "1p", + }, ctx => { + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: account.loggedIn ? account.displayName : strings.defaultName, + rank: account.loggedIn || !gameConfig._accounts || p2.session ? false : strings.notLoggedIn, + font: this.font + }) + }) + if(this.state.moveHover === "account"){ + this.draw.highlight({ + ctx: ctx, + x: frameLeft + 59.5, + y: frameTop + 639.5, + w: 271, + h: 64, + radius: 28.5, + opacity: 0.8, + size: 10 + }) + } + + if(p2.session){ + x = x + w + 4 + w = 396 + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_settings"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 11, + dy: frameTop + 45, + scale: 3.1 + }) + ctx.fillStyle = "rgba(255, 255, 255, 0.5)" + ctx.beginPath() + ctx.moveTo(x, y + h) + ctx.lineTo(x, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + 4) + ctx.lineTo(x + 4, y + 4) + ctx.lineTo(x + 4, y + h) + ctx.fill() + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.beginPath() + ctx.moveTo(x + w, y) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - 4, y + h) + ctx.lineTo(x + w - 4, y + 4) + ctx.fill() + if(this.state.moveHover === "session"){ + this.draw.highlight({ + ctx: ctx, + x: x, + y: y, + w: w, + h: h, + opacity: 0.8 + }) + } + } + + x = p2.session ? frameLeft + 642 + 200 : frameLeft + 642 + w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638 if(p2.session){ this.draw.pattern({ ctx: ctx, @@ -1925,7 +2029,7 @@ class SongSelect{ } this.sessionCache.get({ ctx: ctx, - x: winW / 2, + x: p2.session ? winW / 4 : winW / 2, y: y + (h - 32) / 2, w: winW / 2, h: 38, @@ -1933,7 +2037,7 @@ class SongSelect{ }) ctx.globalAlpha = 1 } - if(this.state.moveHover === "session"){ + if(!p2.session && this.state.moveHover === "session"){ this.draw.highlight({ ctx: ctx, x: x, @@ -1944,6 +2048,25 @@ class SongSelect{ }) } } + if(p2.session){ + this.nameplateCache.get({ + ctx: ctx, + x: frameLeft + 949, + y: frameTop + 640, + w: 273, + h: 66, + id: "2p", + }, ctx => { + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: p2.name, + font: this.font, + blue: true + }) + }) + } if(screen === "titleFadeIn"){ ctx.save() @@ -2019,7 +2142,7 @@ class SongSelect{ if(!score){ break } - if(config.song.stars[i] && score[diff] && score[diff].crown){ + if(config.song.courses[this.difficultyId[i]] && score[diff] && score[diff].crown){ this.draw.crown({ ctx: ctx, type: score[diff].crown, @@ -2148,7 +2271,7 @@ class SongSelect{ }) if(currentSong){ currentSong.p2Cursor = diffId - if(p2.session && currentSong.stars){ + if(p2.session && currentSong.courses){ this.selectedSong = index this.state.move = 0 if(this.state.screen !== "difficulty"){ @@ -2192,7 +2315,7 @@ class SongSelect{ } this.moveToSong(moveBy, true) } - }else if(this.songs[song].stars){ + }else if(this.songs[song].courses){ this.selectedSong = song this.state.move = 0 if(this.state.screen !== "difficulty"){ @@ -2238,16 +2361,11 @@ class SongSelect{ getLocalTitle(title, titleLang){ if(titleLang){ - titleLang = titleLang.split("\n") - titleLang.forEach(line => { - var space = line.indexOf(" ") - var id = line.slice(0, space) - if(id === strings.id){ - title = line.slice(space + 1) - }else if(titleLang.length === 1 && strings.id === "en" && !(id in allStrings)){ - title = line + for(var id in titleLang){ + if(id === strings.id && titleLang[id]){ + return titleLang[id] } - }) + } } return title } diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 17986fc..9a6c499 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -36,6 +36,8 @@ this.hard = "むずかしい" this.oni = "おに" this.songBranch = "譜面分岐あり" + this.defaultName = "どんちゃん" + this.notLoggedIn = "ログインしていない" this.sessionStart = "オンラインセッションを開始する!" this.sessionEnd = "オンラインセッションを終了する" this.loading = "ロード中..." @@ -184,6 +186,24 @@ content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." } } + this.account = { + username: "ユーザー名", + enterUsername: "ユーザー名を入力", + password: "パスワード", + enterPassword: "パスワードを入力", + repeatPassword: "パスワードを再入力", + remember: "ログイン状態を保持する", + login: "ログイン", + register: "登録", + registerAccount: "アカウントを登録", + passwordsDoNotMatch: "パスワードが一致しません", + cannotBeEmpty: "%sは空にできません", + error: "リクエストの処理中にエラーが発生しました", + logout: "ログアウト", + back: "もどる", + cancel: "Cancel", + save: "Save" + } this.browserSupport = { browserWarning: "サポートされていないブラウザを実行しています (%s)", details: "詳しく", @@ -233,6 +253,8 @@ function StringsEn(){ this.hard = "Hard" this.oni = "Extreme" this.songBranch = "Diverge Notes" + this.defaultName = "Don-chan" + this.notLoggedIn = "Not logged in" this.sessionStart = "Begin an Online Session!" this.sessionEnd = "End Online Session" this.loading = "Loading..." @@ -381,6 +403,33 @@ function StringsEn(){ content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." } } + this.account = { + username: "Username", + enterUsername: "Enter Username", + password: "Password", + enterPassword: "Enter Password", + repeatPassword: "Repeat Password", + remember: "Remember me", + login: "Log In", + register: "Register", + registerAccount: "Register account", + passwordsDoNotMatch: "Passwords do not match", + cannotBeEmpty: "%s cannot be empty", + error: "An error occurred while processing your request", + logout: "Log Out", + back: "Back", + cancel: "Cancel", + save: "Save", + displayName: "Displayed Name", + changePassword: "Change Password", + currentNewRepeat: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + deleteAccount: "Delete Account", + verifyPassword: "Verify password to delete this account" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -430,6 +479,8 @@ function StringsCn(){ this.hard = "困难" this.oni = "魔王" this.songBranch = "有谱面分歧" + this.defaultName = "小咚" + this.notLoggedIn = "未登录" this.sessionStart = "开始在线会话!" this.sessionEnd = "结束在线会话" this.loading = "加载中..." @@ -578,6 +629,24 @@ function StringsCn(){ content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." } } + this.account = { + username: "登录名", + enterUsername: "输入用户名", + password: "密码", + enterPassword: "输入密码", + repeatPassword: "重新输入密码", + remember: "记住登录", + login: "登录", + register: "注册", + registerAccount: "注册帐号", + passwordsDoNotMatch: "密码不匹配", + cannotBeEmpty: "%s不能为空", + error: "处理您的请求时发生错误", + logout: "登出", + back: "返回", + cancel: "Cancel", + save: "Save" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -627,6 +696,8 @@ function StringsTw(){ this.hard = "困難" this.oni = "魔王" this.songBranch = "有譜面分歧" + this.defaultName = "小咚" + this.notLoggedIn = "未登錄" this.sessionStart = "開始多人模式!" this.sessionEnd = "結束多人模式" this.loading = "讀取中..." @@ -775,6 +846,24 @@ function StringsTw(){ content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." } } + this.account = { + username: "使用者名稱", + enterUsername: "輸入用戶名", + password: "密碼", + enterPassword: "輸入密碼", + repeatPassword: "再次輸入密碼", + remember: "記住登錄", + login: "登入", + register: "註冊", + registerAccount: "註冊帳號", + passwordsDoNotMatch: "密碼不匹配", + cannotBeEmpty: "%s不能為空", + error: "處理您的請求時發生錯誤", + logout: "登出", + back: "返回", + cancel: "Cancel", + save: "Save" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -824,6 +913,8 @@ function StringsKo(){ this.hard = "어려움" this.oni = "귀신" this.songBranch = "악보 분기 있습니다" + this.defaultName = "동이" + this.notLoggedIn = "로그인하지 않았습니다" this.sessionStart = "온라인 세션 시작!" this.sessionEnd = "온라인 세션 끝내기" this.loading = "로딩 중..." @@ -972,6 +1063,24 @@ function StringsKo(){ content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." } } + this.account = { + username: "사용자 이름", + enterUsername: "사용자 이름을 입력하십시오", + password: "비밀번호", + enterPassword: "비밀번호 입력", + repeatPassword: "비밀번호 재입력", + remember: "자동 로그인", + login: "로그인", + register: "가입하기", + registerAccount: "계정 등록", + passwordsDoNotMatch: "비밀번호가 일치하지 않습니다", + cannotBeEmpty: "%s 비어 있을 수 없습니다", + error: "요청을 처리하는 동안 오류가 발생했습니다", + logout: "로그 아웃", + back: "돌아간다", + cancel: "Cancel", + save: "Save" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", diff --git a/public/src/js/view.js b/public/src/js/view.js index 9964c83..a165b09 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -126,6 +126,7 @@ this.comboCache = new CanvasCache(noSmoothing) this.pauseCache = new CanvasCache(noSmoothing) this.branchCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) this.multiplayer = this.controller.multiplayer @@ -235,6 +236,11 @@ if(!this.multiplayer){ this.pauseCache.resize(81 * this.pauseOptions.length * 2, 464, ratio) } + if(this.portrait){ + this.nameplateCache.resize(220, 54, ratio + 0.2) + }else{ + this.nameplateCache.resize(274, 67, ratio + 0.2) + } this.fillComboCache() this.setDonBgHeight() resized = true @@ -388,6 +394,32 @@ h: 130 } + if(this.multiplayer !== 2){ + this.nameplateCache.get({ + ctx: ctx, + x: 167, + y: 160, + w: 219, + h: 53, + id: "1p", + }, ctx => { + if(this.multiplayer === 2){ + var name = p2.name || strings.defaultName + }else{ + var name = account.loggedIn ? account.displayName : strings.defaultName + } + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + scale: 0.8, + name: name, + font: this.font, + blue: this.multiplayer === 2 + }) + }) + } + ctx.fillStyle = "#000" ctx.fillRect( 0, @@ -547,6 +579,29 @@ } var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162} + this.nameplateCache.get({ + ctx: ctx, + x: 320, + y: this.multiplayer === 2 ? frameTop + 305 : frameTop + 20, + w: 273, + h: 66, + id: "1p", + }, ctx => { + if(this.multiplayer === 2){ + var name = p2.name || strings.defaultName + }else{ + var name = account.loggedIn ? account.displayName : strings.defaultName + } + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: name, + font: this.font, + blue: this.multiplayer === 2 + }) + }) + ctx.fillStyle = "#000" ctx.fillRect( 0, diff --git a/public/src/views/account.html b/public/src/views/account.html new file mode 100644 index 0000000..1271156 --- /dev/null +++ b/public/src/views/account.html @@ -0,0 +1,33 @@ +
+ +
diff --git a/public/src/views/login.html b/public/src/views/login.html new file mode 100644 index 0000000..c9a58c5 --- /dev/null +++ b/public/src/views/login.html @@ -0,0 +1,24 @@ +
+
+
+
+ +
+
+ +
+
+
+
diff --git a/server.py b/server.py index 04e802d..eb8e73c 100644 --- a/server.py +++ b/server.py @@ -42,7 +42,8 @@ async def connection(ws, path): user = { "ws": ws, "action": "ready", - "session": False + "session": False, + "name": None } server_status["users"].append(user) try: @@ -79,6 +80,7 @@ async def connection(ws, path): waiting = server_status["waiting"] id = value["id"] if "id" 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: continue if id not in waiting: @@ -92,6 +94,7 @@ async def connection(ws, path): await ws.send(msgobj("waiting")) else: # Join the other user and start game + user["name"] = value["name"] if "name" in value else None user["other_user"] = waiting[id]["user"] waiting_diff = waiting[id]["diff"] del waiting[id] @@ -101,7 +104,9 @@ async def connection(ws, path): user["other_user"]["other_user"] = user await asyncio.wait([ ws.send(msgobj("gameload", waiting_diff)), - user["other_user"]["ws"].send(msgobj("gameload", diff)) + user["other_user"]["ws"].send(msgobj("gameload", diff)), + ws.send(msgobj("name", user["other_user"]["name"])), + user["other_user"]["ws"].send(msgobj("name", user["name"])) ]) else: # Wait for another user @@ -116,27 +121,31 @@ async def connection(ws, path): # Update others on waiting players await notify_status() elif type == "invite": - if value == None: + if value and "id" in value and value["id"] == None: # Session invite link requested invite = get_invite() server_status["invites"][invite] = user user["action"] = "invite" user["session"] = invite + user["name"] = value["name"] if "name" in value else None 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 - user["other_user"] = server_status["invites"][value] - del server_status["invites"][value] + user["name"] = value["name"] if "name" in value else None + user["other_user"] = server_status["invites"][value["id"]] + del server_status["invites"][value["id"]] if "ws" in user["other_user"]: user["other_user"]["other_user"] = user user["action"] = "invite" - user["session"] = value + user["session"] = value["id"] sent_msg = msgobj("session") await asyncio.wait([ ws.send(sent_msg), - user["other_user"]["ws"].send(sent_msg) + user["other_user"]["ws"].send(sent_msg), + 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: del user["other_user"] await ws.send(msgobj("gameend")) From 7519b1c4c29e967e6fc9545cb86baa39c640f4d6 Mon Sep 17 00:00:00 2001 From: Bui Date: Fri, 13 Mar 2020 02:45:42 +0000 Subject: [PATCH 02/41] account system backend, db rewrite --- .gitignore | 2 + app.py | 348 ++++++++++++++++++++++++------- config.example.json | 4 +- public/src/css/admin.css | 125 +++++++++++ schema.py | 73 +++++++ templates/admin.html | 23 ++ templates/admin_song_detail.html | 87 ++++++++ templates/admin_songs.html | 11 + tools/migrate_db.py | 105 ++++++++++ 9 files changed, 700 insertions(+), 78 deletions(-) create mode 100644 public/src/css/admin.css create mode 100644 schema.py create mode 100644 templates/admin.html create mode 100644 templates/admin_song_detail.html create mode 100644 templates/admin_songs.html create mode 100644 tools/migrate_db.py diff --git a/.gitignore b/.gitignore index 6d44399..3b79d0f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ $RECYCLE.BIN/ .Trashes .vscode +*.pyc # Directories potentially created on remote AFP share .AppleDB @@ -50,3 +51,4 @@ version.json public/index.html config.json public/assets/song_skins +secret.txt diff --git a/app.py b/app.py index d1ecbe3..569631f 100644 --- a/app.py +++ b/app.py @@ -1,39 +1,65 @@ -#!/usr/bin/env python2 - -from __future__ import division +#!/usr/bin/env python3 +import bcrypt import json -import sqlite3 import re +import schema import os -from flask import Flask, g, jsonify, render_template, request, abort, redirect + +from functools import wraps +from flask import Flask, g, jsonify, render_template, request, abort, redirect, session from flask_caching import Cache +from flask_session import Session from ffmpy import FFmpeg +from pymongo import MongoClient app = Flask(__name__) -try: - app.cache = Cache(app, config={'CACHE_TYPE': 'redis'}) -except RuntimeError: - import tempfile - app.cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': tempfile.gettempdir()}) +client = MongoClient() + +try: + app.secret_key = open('secret.txt').read().strip() +except FileNotFoundError: + app.secret_key = os.urandom(24).hex() + with open('secret.txt', 'w') as fp: + fp.write(app.secret_key) + fp.close() + +app.config['SESSION_TYPE'] = 'redis' +app.cache = Cache(app, config={'CACHE_TYPE': 'redis'}) +sess = Session() +sess.init_app(app) + +db = client.taiko +db.users.create_index('username', unique=True) -DATABASE = 'taiko.db' DEFAULT_URL = 'https://github.com/bui/taiko-web/' -def get_db(): - db = getattr(g, '_database', None) - if db is None: - db = g._database = sqlite3.connect(DATABASE) - db.row_factory = sqlite3.Row - return db +def api_error(message): + return jsonify({'status': 'error', 'message': message}) -def query_db(query, args=(), one=False): - cur = get_db().execute(query, args) - rv = cur.fetchall() - cur.close() - return (rv[0] if rv else None) if one else rv +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('username'): + return api_error('not_logged_in') + return f(*args, **kwargs) + return decorated_function + + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('username'): + return abort(403) + + user = db.users.find_one({'username': session.get('username')}) + if user['user_level'] < 100: + return abort(403) + + return f(*args, **kwargs) + return decorated_function def get_config(): @@ -72,13 +98,6 @@ def get_version(): return version -@app.teardown_appcontext -def close_connection(exception): - db = getattr(g, '_database', None) - if db is not None: - db.close() - - @app.route('/') @app.cache.cached(timeout=15) def route_index(): @@ -86,6 +105,33 @@ def route_index(): return render_template('index.html', version=version, config=get_config()) +@app.route('/admin') +@admin_required +def route_admin(): + return redirect('/admin/songs') + + +@app.route('/admin/songs') +@admin_required +def route_admin_songs(): + songs = db.songs.find({}) + return render_template('admin_songs.html', songs=list(songs)) + + +@app.route('/admin/songs/') +@admin_required +def route_admin_songs_id(id): + song = db.songs.find_one({'id': id}) + if not song: + return abort(404) + + categories = list(db.categories.find({})) + song_skins = list(db.song_skins.find({})) + + return render_template('admin_song_detail.html', + song=song, categories=categories, song_skins=song_skins) + + @app.route('/api/preview') @app.cache.cached(timeout=15, query_string=True) def route_api_preview(): @@ -93,12 +139,12 @@ def route_api_preview(): if not song_id or not re.match('^[0-9]+$', song_id): abort(400) - song_row = query_db('select * from songs where id = ? and enabled = 1', (song_id,)) - if not song_row: + song = db.songs.find_one({'id': song_id}) + if not song: abort(400) - song_type = song_row[0]['type'] - prev_path = make_preview(song_id, song_type, song_row[0]['preview']) + song_type = song['type'] + prev_path = make_preview(song_id, song_type, song['preview']) if not prev_path: return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id) @@ -108,52 +154,30 @@ def route_api_preview(): @app.route('/api/songs') @app.cache.cached(timeout=15) def route_api_songs(): - songs = query_db('select s.*, m.name, m.url from songs s left join makers m on s.maker_id = m.maker_id where enabled = 1') - - raw_categories = query_db('select * from categories') - categories = {} - for cat in raw_categories: - categories[cat['id']] = cat['title'] - - raw_song_skins = query_db('select * from song_skins') - song_skins = {} - for skin in raw_song_skins: - song_skins[skin[0]] = {'name': skin['name'], 'song': skin['song'], 'stage': skin['stage'], 'don': skin['don']} - - songs_out = [] + songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False})) for song in songs: - song_id = song['id'] - song_type = song['type'] - preview = song['preview'] - - category_out = categories[song['category']] if song['category'] in categories else '' - song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None - maker = None - if song['maker_id'] == 0: - maker = 0 - elif song['maker_id'] and song['maker_id'] > 0: - maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']} - - songs_out.append({ - 'id': song_id, - 'title': song['title'], - 'title_lang': song['title_lang'], - 'subtitle': song['subtitle'], - 'subtitle_lang': song['subtitle_lang'], - 'stars': [ - song['easy'], song['normal'], song['hard'], song['oni'], song['ura'] - ], - 'preview': preview, - 'category': category_out, - 'type': song_type, - 'offset': song['offset'], - 'song_skin': song_skin_out, - 'volume': song['volume'], - 'maker': maker, - 'hash': song['hash'] - }) + if song['maker_id']: + if song['maker_id'] == 0: + song['maker'] = 0 + else: + song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False}) + else: + song['maker'] = None + del song['maker_id'] - return jsonify(songs_out) + if song['category_id']: + song['category'] = db.categories.find_one({'id': song['category_id']})['title'] + else: + song['category'] = None + del song['category_id'] + + if song['skin_id']: + song['song_skin'] = db.song_skins.find_one({'id': song['skin_id']}, {'_id': False, 'id': False}) + else: + song['song_skin'] = None + del song['skin_id'] + + return jsonify(songs) @app.route('/api/config') @@ -163,6 +187,176 @@ def route_api_config(): return jsonify(config) +@app.route('/api/register', methods=['POST']) +def route_api_register(): + if session.get('username'): + return api_error('already_logged_in') + + data = request.get_json() + if not schema.validate(data, schema.register): + return abort(400) + + username = data.get('username', '') + if len(username) > 20 or not re.match('^[a-zA-Z0-9_]{1,20}$', username): + return api_error('invalid_username') + + if db.users.find_one({'username_lower': username.lower()}): + return api_error('username_in_use') + + password = data.get('password', '').encode('utf-8') + if not 8 <= len(password) <= 5000: + return api_error('invalid_password') + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(password, salt) + + db.users.insert_one({ + 'username': username, + 'username_lower': username.lower(), + 'password': hashed, + 'display_name': username, + 'user_level': 1 + }) + + session['username'] = username + session.permanent = True + return jsonify({'status': 'ok', 'username': username, 'display_name': username}) + + +@app.route('/api/login', methods=['POST']) +def route_api_login(): + if session.get('username'): + return api_error('already_logged_in') + + data = request.get_json() + if not schema.validate(data, schema.login): + return abort(400) + + username = data.get('username', '') + result = db.users.find_one({'username_lower': username.lower()}) + if not result: + return api_error('invalid_username_password') + + password = data.get('password', '').encode('utf-8') + if not bcrypt.checkpw(password, result['password']): + return api_error('invalid_username_password') + + session['username'] = result['username'] + if data.get('remember'): + session.permanent = True + + return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name']}) + + +@app.route('/api/logout', methods=['POST']) +@login_required +def route_api_logout(): + session.clear() + return jsonify({'status': 'ok'}) + + +@app.route('/api/account/display_name', methods=['POST']) +@login_required +def route_api_account_display_name(): + data = request.get_json() + if not schema.validate(data, schema.update_display_name): + return abort(400) + + display_name = data.get('display_name', '') + if not display_name or len(display_name) > 20: + return api_error('invalid_display_name') + + db.users.update_one({'username': session.get('username')}, { + '$set': {'display_name': display_name} + }) + + return jsonify({'status': 'ok'}) + + +@app.route('/api/account/password', methods=['POST']) +@login_required +def route_api_account_password(): + data = request.get_json() + if not schema.validate(data, schema.update_password): + return abort(400) + + user = db.users.find_one({'username': session.get('username')}) + current_password = data.get('current_password', '').encode('utf-8') + if not bcrypt.checkpw(current_password, user['password']): + return api_error('current_password_invalid') + + new_password = data.get('new_password', '').encode('utf-8') + if not 8 <= len(new_password) <= 5000: + return api_error('invalid_password') + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(new_password, salt) + + db.users.update_one({'username': session.get('username')}, { + '$set': {'password': hashed} + }) + + return jsonify({'status': 'ok'}) + + +@app.route('/api/account/remove', methods=['POST']) +@login_required +def route_api_account_remove(): + data = request.get_json() + if not schema.validate(data, schema.delete_account): + return abort(400) + + user = db.users.find_one({'username': session.get('username')}) + password = data.get('password', '').encode('utf-8') + if not bcrypt.checkpw(password, user['password']): + return api_error('current_password_invalid') + + db.scores.delete_many({'username': session.get('username')}) + db.users.delete_one({'username': session.get('username')}) + + session.clear() + return jsonify({'status': 'ok'}) + + +@app.route('/api/scores/save', methods=['POST']) +@login_required +def route_api_scores_save(): + data = request.get_json() + if not schema.validate(data, schema.scores_save): + return abort(400) + + username = session.get('username') + if data.get('is_import'): + db.scores.delete_many({'username': username}) + + scores = data.get('scores', []) + for score in scores: + db.scores.update_one({'username': username, 'hash': score['hash']}, + {'$set': { + 'username': username, + 'hash': score['hash'], + 'score': score['score'] + }}, upsert=True) + + return jsonify({'success': True}) + + +@app.route('/api/scores/get') +@login_required +def route_api_scores_get(): + username = session.get('username') + + scores = [] + for score in db.scores.find({'username': username}): + scores.append({ + 'hash': score['hash'], + 'score': score['score'] + }) + + user = db.users.find_one({'username': username}) + return jsonify({'scores': scores, 'username': user['username'], 'display_name': user['display_name']}) + + def make_preview(song_id, song_type, preview): song_path = 'public/songs/%s/main.mp3' % song_id prev_path = 'public/songs/%s/preview.mp3' % song_id diff --git a/config.example.json b/config.example.json index 9bfd207..9ec88e4 100644 --- a/config.example.json +++ b/config.example.json @@ -1,4 +1,6 @@ { "songs_baseurl": "", - "assets_baseurl": "" + "assets_baseurl": "", + "email": "", + "_accounts": true } diff --git a/public/src/css/admin.css b/public/src/css/admin.css new file mode 100644 index 0000000..5b2f22d --- /dev/null +++ b/public/src/css/admin.css @@ -0,0 +1,125 @@ +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; +} \ No newline at end of file diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..1ba0c81 --- /dev/null +++ b/schema.py @@ -0,0 +1,73 @@ +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'} + } + } + } +} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..e92ff9a --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,23 @@ + + + + + Taiko Web Admin + + + + + +
+ +
+ +
+
+ {% block content %}{% endblock %} +
+
+ + diff --git a/templates/admin_song_detail.html b/templates/admin_song_detail.html new file mode 100644 index 0000000..322c835 --- /dev/null +++ b/templates/admin_song_detail.html @@ -0,0 +1,87 @@ +{% extends 'admin.html' %} +{% block content %} +

{{ song.title }} (ID: {{ song.id }})

+
+
+ +
+ +
+ +
+

Title

+ + + + + + + + + + + + +
+ +
+

Subtitle

+ + + + + + + + + + + + +
+ +
+

Courses

+ + + + + + + + + + + + + + + +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ + +
+
+{% endblock %} diff --git a/templates/admin_songs.html b/templates/admin_songs.html new file mode 100644 index 0000000..eb55425 --- /dev/null +++ b/templates/admin_songs.html @@ -0,0 +1,11 @@ +{% extends 'admin.html' %} +{% block content %} +

Songs

+{% for song in songs %} + +
+

{{ song.title }}

+
+
+{% endfor %} +{% endblock %} diff --git a/tools/migrate_db.py b/tools/migrate_db.py new file mode 100644 index 0000000..7ae71c6 --- /dev/null +++ b/tools/migrate_db.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# Migrate old SQLite taiko.db to MongoDB + +import sqlite3 +from pymongo import MongoClient + +client = MongoClient() +#client.drop_database('taiko') +db = client.taiko +sqdb = sqlite3.connect('taiko.db') +sqdb.row_factory = sqlite3.Row +curs = sqdb.cursor() + +def migrate_songs(): + curs.execute('select * from songs') + rows = curs.fetchall() + + for row in rows: + song = { + 'id': row['id'], + 'title': row['title'], + 'title_lang': {'ja': row['title']}, + 'subtitle': row['subtitle'], + 'subtitle_lang': {'ja': row['subtitle']}, + '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'], + 'skin_id': row['skin_id'], + 'preview': row['preview'], + 'volume': row['volume'], + '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) + +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() From 62aca02aab25804ca4941e3364f2050053eb8999 Mon Sep 17 00:00:00 2001 From: Bui Date: Fri, 13 Mar 2020 03:19:26 +0000 Subject: [PATCH 03/41] bugfixes --- app.py | 3 ++- public/src/js/loader.js | 2 +- public/src/js/scorestorage.js | 13 ++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 569631f..781819d 100644 --- a/app.py +++ b/app.py @@ -25,6 +25,7 @@ except FileNotFoundError: fp.close() app.config['SESSION_TYPE'] = 'redis' +app.config['SESSION_COOKIE_HTTPONLY'] = False app.cache = Cache(app, config={'CACHE_TYPE': 'redis'}) sess = Session() sess.init_app(app) @@ -354,7 +355,7 @@ def route_api_scores_get(): }) user = db.users.find_one({'username': username}) - return jsonify({'scores': scores, 'username': user['username'], 'display_name': user['display_name']}) + return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name']}) def make_preview(song_id, song_type, preview): diff --git a/public/src/js/loader.js b/public/src/js/loader.js index 30c1bd0..ac4e279 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -157,7 +157,7 @@ class Loader{ })) if(gameConfig._accounts){ - var token = Cookies.get("token") + var token = Cookies.get("session") if(token){ this.addPromise(this.ajax("/api/scores/get").then(response => { response = JSON.parse(response) diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index 55dfe02..330a639 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -51,13 +51,20 @@ class ScoreStorage{ } } } + prepareScores(scores){ + var output = [] + for (var k in scores) { + songs.push({'hash': k, 'score': scores[k]}) + } + return output + } save(localOnly){ for(var hash in this.scores){ this.writeString(hash) } this.write() return this.sendToServer({ - scores: this.scoreStrings, + scores: this.prepareScores(this.scoreStrings), is_import: true }) } @@ -163,7 +170,7 @@ class ScoreStorage{ } this.write() this.sendToServer({ - scores: this.scoreStrings, + scores: this.prepareScores(this.scoreStrings), is_import: true }) } @@ -181,7 +188,7 @@ class ScoreStorage{ account.loggedIn = false delete account.username delete account.displayName - Cookies.remove("token") + Cookies.remove("session") this.load() pageEvents.send("logout") return Promise.reject() From 73b8da36abeddeb69f3456602642565edeecaa5d Mon Sep 17 00:00:00 2001 From: Bui Date: Fri, 13 Mar 2020 03:54:27 +0000 Subject: [PATCH 04/41] fix registration bug --- app.py | 2 +- public/src/js/scorestorage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 781819d..663d1a6 100644 --- a/app.py +++ b/app.py @@ -339,7 +339,7 @@ def route_api_scores_save(): 'score': score['score'] }}, upsert=True) - return jsonify({'success': True}) + return jsonify({'status': 'ok'}) @app.route('/api/scores/get') diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index 330a639..dc37a80 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -54,7 +54,7 @@ class ScoreStorage{ prepareScores(scores){ var output = [] for (var k in scores) { - songs.push({'hash': k, 'score': scores[k]}) + output.push({'hash': k, 'score': scores[k]}) } return output } From 929471698f10cc84752e93be1f53301453a1c5a9 Mon Sep 17 00:00:00 2001 From: Bui Date: Fri, 13 Mar 2020 04:04:45 +0000 Subject: [PATCH 05/41] fix individual score upload --- public/src/js/scorestorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index dc37a80..e9db533 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -133,7 +133,7 @@ class ScoreStorage{ var obj = {} obj[hash] = this.scoreStrings[hash] this.sendToServer({ - scores: obj + scores: this.prepareScores(obj) }).catch(() => this.add.apply(this, arguments)) } template(){ From ad62ac800c4202b8ce9b2c78a5d1a9fabcc4775e Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Fri, 13 Mar 2020 08:32:53 +0300 Subject: [PATCH 06/41] Some bug fixes --- app.py | 2 +- public/src/js/account.js | 26 +++++++++++--------- public/src/js/scorestorage.js | 9 ++++++- public/src/js/strings.js | 44 ++++++++++++++++++++++++++++++--- public/src/js/view.js | 46 +++++++++++++++++------------------ 5 files changed, 86 insertions(+), 41 deletions(-) diff --git a/app.py b/app.py index 663d1a6..a51e90f 100644 --- a/app.py +++ b/app.py @@ -205,7 +205,7 @@ def route_api_register(): return api_error('username_in_use') password = data.get('password', '').encode('utf-8') - if not 8 <= len(password) <= 5000: + if not 6 <= len(password) <= 5000: return api_error('invalid_password') salt = bcrypt.gensalt() diff --git a/public/src/js/account.js b/public/src/js/account.js index 35ea69d..8476d4a 100644 --- a/public/src/js/account.js +++ b/public/src/js/account.js @@ -256,7 +256,7 @@ class Account{ account.displayName = response.display_name var loadScores = scores => { scoreStorage.load(scores) - this.onEnd(false, true) + this.onEnd(false, true, true) pageEvents.send("login", account.username) } if(this.mode === "login"){ @@ -267,7 +267,7 @@ class Account{ }) }else{ scoreStorage.save().catch(() => {}).finally(() => { - this.onEnd(false, true) + this.onEnd(false, true, true) pageEvents.send("login", account.username) }) } @@ -294,7 +294,7 @@ class Account{ delete account.username delete account.displayName var loadScores = scores => { - Cookies.remove("token") + Cookies.remove("session") scoreStorage.load() this.onEnd(false, true) pageEvents.send("logout") @@ -341,7 +341,7 @@ class Account{ account.loggedIn = false delete account.username delete account.displayName - Cookies.remove("token") + Cookies.remove("session") scoreStorage.load() pageEvents.send("logout") return Promise.resolve @@ -351,6 +351,8 @@ class Account{ if(!noNameChange && newName !== account.displayName){ promises.push(this.request("account/display_name", { display_name: newName + }).then(() => { + account.displayName = newName })) } var error = false @@ -368,7 +370,7 @@ class Account{ this.onEnd(false, true) }, errorFunc).catch(errorFunc) } - onEnd(event, noSound){ + onEnd(event, noSound, noReset){ var touched = false if(event){ if(event.type === "touchstart"){ @@ -381,7 +383,7 @@ class Account{ if(this.locked){ return } - this.clean() + this.clean(false, noReset) assets.sounds["se_don"].play() setTimeout(() => { new SongSelect(false, false, touched) @@ -391,7 +393,7 @@ class Account{ this.lock(true) return new Promise((resolve, reject) => { var request = new XMLHttpRequest() - request.open(obj ? "POST" : "GET", "api/" + url) + request.open("POST", "api/" + url) pageEvents.load(request).then(() => { this.lock(false) if(request.status !== 200){ @@ -433,7 +435,7 @@ class Account{ } } } - clean(eventsOnly){ + clean(eventsOnly, noReset){ if(!eventsOnly){ cancelTouch = true this.keyboard.clean() @@ -447,8 +449,10 @@ class Account{ for(var i = 0; i < this.inputForms.length; i++){ pageEvents.remove(this.inputForms[i], ["keydown", "keyup", "keypress"]) } - this.accountPass.reset() - this.accountDel.reset() + if(!noReset){ + this.accountPass.reset() + this.accountDel.reset() + } delete this.displayname delete this.accountPassButton delete this.accountPass @@ -460,7 +464,7 @@ class Account{ delete this.saveButton delete this.inputForms }else if(this.mode === "login" || this.mode === "register"){ - if(!eventsOnly){ + if(!eventsOnly && !noReset){ this.form.reset() } pageEvents.remove(this.form, "submit") diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index e9db533..db919c6 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -9,7 +9,7 @@ class ScoreStorage{ load(strings){ this.scores = {} if(strings){ - this.scoreStrings = strings + this.scoreStrings = this.prepareStrings(strings) }else if(account.loggedIn){ return }else{ @@ -58,6 +58,13 @@ class ScoreStorage{ } return output } + prepareStrings(scores){ + var output = {} + for(var k in scores){ + output[scores[k].hash] = scores[k].score + } + return output + } save(localOnly){ for(var hash in this.scores){ this.writeString(hash) diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 9a6c499..f4d0d75 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -202,7 +202,16 @@ logout: "ログアウト", back: "もどる", cancel: "Cancel", - save: "Save" + save: "Save", + displayName: "Displayed Name", + changePassword: "Change Password", + currentNewRepeat: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + deleteAccount: "Delete Account", + verifyPassword: "Verify password to delete this account" } this.browserSupport = { browserWarning: "サポートされていないブラウザを実行しています (%s)", @@ -645,7 +654,16 @@ function StringsCn(){ logout: "登出", back: "返回", cancel: "Cancel", - save: "Save" + save: "Save", + displayName: "Displayed Name", + changePassword: "Change Password", + currentNewRepeat: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + deleteAccount: "Delete Account", + verifyPassword: "Verify password to delete this account" } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", @@ -862,7 +880,16 @@ function StringsTw(){ logout: "登出", back: "返回", cancel: "Cancel", - save: "Save" + save: "Save", + displayName: "Displayed Name", + changePassword: "Change Password", + currentNewRepeat: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + deleteAccount: "Delete Account", + verifyPassword: "Verify password to delete this account" } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", @@ -1079,7 +1106,16 @@ function StringsKo(){ logout: "로그 아웃", back: "돌아간다", cancel: "Cancel", - save: "Save" + save: "Save", + displayName: "Displayed Name", + changePassword: "Change Password", + currentNewRepeat: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + deleteAccount: "Delete Account", + verifyPassword: "Verify password to delete this account" } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", diff --git a/public/src/js/view.js b/public/src/js/view.js index a165b09..235b169 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -394,31 +394,29 @@ h: 130 } - if(this.multiplayer !== 2){ - this.nameplateCache.get({ + this.nameplateCache.get({ + ctx: ctx, + x: 167, + y: this.multiplayer === 2 ? 565 : 160, + w: 219, + h: 53, + id: "1p", + }, ctx => { + if(this.multiplayer === 2){ + var name = p2.name || strings.defaultName + }else{ + var name = account.loggedIn ? account.displayName : strings.defaultName + } + this.draw.nameplate({ ctx: ctx, - x: 167, - y: 160, - w: 219, - h: 53, - id: "1p", - }, ctx => { - if(this.multiplayer === 2){ - var name = p2.name || strings.defaultName - }else{ - var name = account.loggedIn ? account.displayName : strings.defaultName - } - this.draw.nameplate({ - ctx: ctx, - x: 3, - y: 3, - scale: 0.8, - name: name, - font: this.font, - blue: this.multiplayer === 2 - }) + x: 3, + y: 3, + scale: 0.8, + name: name, + font: this.font, + blue: this.multiplayer === 2 }) - } + }) ctx.fillStyle = "#000" ctx.fillRect( @@ -582,7 +580,7 @@ this.nameplateCache.get({ ctx: ctx, x: 320, - y: this.multiplayer === 2 ? frameTop + 305 : frameTop + 20, + y: this.multiplayer === 2 ? 460 : 20, w: 273, h: 66, id: "1p", From 7f1bb9d3576221c266042f15fdba0b2a3be0a8bd Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Sat, 14 Mar 2020 07:50:04 +0300 Subject: [PATCH 07/41] Multiplayer improvements and reupload score on error - In multiplayer you will play at the bottom if you joined second - Add a dialog to song select that score has not been saved - Uploads the score when logged in again - Translate error messages from the server --- app.py | 28 ++-- config.example.json | 2 +- public/src/css/view.css | 8 ++ public/src/js/account.js | 64 +++++---- public/src/js/assets.js | 1 - public/src/js/controller.js | 12 +- public/src/js/game.js | 2 +- public/src/js/lib/js.cookie.min.js | 2 - public/src/js/loader.js | 29 ++-- public/src/js/loadsong.js | 4 +- public/src/js/p2.js | 12 +- public/src/js/pageevents.js | 2 +- public/src/js/scoresheet.js | 111 +++++++++------- public/src/js/scorestorage.js | 77 ++++++++--- public/src/js/songselect.js | 205 ++++++++++++++++++++++++++--- public/src/js/strings.js | 66 ++++++++++ public/src/js/view.js | 108 ++++++++------- public/src/js/viewassets.js | 2 +- public/src/views/account.html | 3 +- public/src/views/login.html | 3 +- server.py | 13 +- 21 files changed, 543 insertions(+), 211 deletions(-) delete mode 100644 public/src/js/lib/js.cookie.min.js diff --git a/app.py b/app.py index a51e90f..d7c18cf 100644 --- a/app.py +++ b/app.py @@ -190,15 +190,15 @@ def route_api_config(): @app.route('/api/register', methods=['POST']) def route_api_register(): - if session.get('username'): - return api_error('already_logged_in') - data = request.get_json() if not schema.validate(data, schema.register): return abort(400) + if session.get('username'): + session.clear() + username = data.get('username', '') - if len(username) > 20 or not re.match('^[a-zA-Z0-9_]{1,20}$', username): + if len(username) < 3 or len(username) > 20 or not re.match('^[a-zA-Z0-9_]{3,20}$', username): return api_error('invalid_username') if db.users.find_one({'username_lower': username.lower()}): @@ -226,13 +226,13 @@ def route_api_register(): @app.route('/api/login', methods=['POST']) def route_api_login(): - if session.get('username'): - return api_error('already_logged_in') - data = request.get_json() if not schema.validate(data, schema.login): return abort(400) + if session.get('username'): + session.clear() + username = data.get('username', '') result = db.users.find_one({'username_lower': username.lower()}) if not result: @@ -263,15 +263,17 @@ def route_api_account_display_name(): if not schema.validate(data, schema.update_display_name): return abort(400) - display_name = data.get('display_name', '') - if not display_name or len(display_name) > 20: + display_name = data.get('display_name', '').strip() + if not display_name: + display_name = session.get('username') + elif len(display_name) > 25: return api_error('invalid_display_name') db.users.update_one({'username': session.get('username')}, { '$set': {'display_name': display_name} }) - return jsonify({'status': 'ok'}) + return jsonify({'status': 'ok', 'display_name': display_name}) @app.route('/api/account/password', methods=['POST']) @@ -287,8 +289,8 @@ def route_api_account_password(): return api_error('current_password_invalid') new_password = data.get('new_password', '').encode('utf-8') - if not 8 <= len(new_password) <= 5000: - return api_error('invalid_password') + if not 6 <= len(new_password) <= 5000: + return api_error('invalid_new_password') salt = bcrypt.gensalt() hashed = bcrypt.hashpw(new_password, salt) @@ -310,7 +312,7 @@ def route_api_account_remove(): user = db.users.find_one({'username': session.get('username')}) password = data.get('password', '').encode('utf-8') if not bcrypt.checkpw(password, user['password']): - return api_error('current_password_invalid') + return api_error('verify_password_invalid') db.scores.delete_many({'username': session.get('username')}) db.users.delete_one({'username': session.get('username')}) diff --git a/config.example.json b/config.example.json index 9ec88e4..f81c1b7 100644 --- a/config.example.json +++ b/config.example.json @@ -2,5 +2,5 @@ "songs_baseurl": "", "assets_baseurl": "", "email": "", - "_accounts": true + "accounts": true } diff --git a/public/src/css/view.css b/public/src/css/view.css index c1f254e..4696d02 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -368,3 +368,11 @@ kbd{ .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; +} diff --git a/public/src/js/account.js b/public/src/js/account.js index 8476d4a..22757a7 100644 --- a/public/src/js/account.js +++ b/public/src/js/account.js @@ -35,6 +35,7 @@ class Account{ this.inputForms = [] this.shownDiv = "" + this.errorDiv = this.getElement("error-div") this.getElement("displayname-hint").innerText = strings.account.displayName this.displayname = this.getElement("displayname") this.displayname.placeholder = strings.account.displayName @@ -116,6 +117,8 @@ class Account{ this.mode = register ? "register" : "login" this.setAltText(this.getElement("view-title"), strings.account[this.mode]) + + this.errorDiv = this.getElement("error-div") this.items = [] this.form = this.getElement("login-form") this.getElement("username-hint").innerText = strings.account.username @@ -239,14 +242,14 @@ class Account{ password: this.form.password.value } if(!obj.username || !obj.password){ - alert(strings.account.cannotBeEmpty.replace("%s", strings.account[!obj.username ? "username" : "password"])) + this.error(strings.account.cannotBeEmpty.replace("%s", strings.account[!obj.username ? "username" : "password"])) return } if(this.mode === "login"){ obj.remember = this.form.remember.checked }else{ if(obj.password !== this.form.password2.value){ - alert(strings.account.passwordsDoNotMatch) + this.error(strings.account.passwordsDoNotMatch) return } } @@ -260,7 +263,7 @@ class Account{ pageEvents.send("login", account.username) } if(this.mode === "login"){ - this.request("scores/get").then(response => { + this.request("scores/get", false, true).then(response => { loadScores(response.scores) }, () => { loadScores({}) @@ -273,9 +276,13 @@ class Account{ } }, response => { if(response && response.status === "error" && response.message){ - alert(response.message) + if(response.message in strings.serverError){ + this.error(strings.serverError[response.message]) + }else{ + this.error(response.message) + } }else{ - alert(strings.account.error) + this.error(strings.account.error) } }) } @@ -293,17 +300,12 @@ class Account{ account.loggedIn = false delete account.username delete account.displayName - var loadScores = scores => { - Cookies.remove("session") + var loadScores = () => { scoreStorage.load() this.onEnd(false, true) pageEvents.send("logout") } - this.request("logout").then(response => { - loadScores() - }, () => { - loadScores() - }) + this.request("logout").then(loadScores, loadScores) } onSave(event){ if(event){ @@ -316,6 +318,7 @@ class Account{ if(this.locked){ return } + this.clearError() var promises = [] var noNameChange = false if(this.shownDiv === "pass"){ @@ -329,7 +332,7 @@ class Account{ new_password: passwords[1] })) }else{ - alert(strings.account.passwordsDoNotMatch) + this.error(strings.account.newPasswordsDoNotMatch) return } } @@ -341,7 +344,6 @@ class Account{ account.loggedIn = false delete account.username delete account.displayName - Cookies.remove("session") scoreStorage.load() pageEvents.send("logout") return Promise.resolve @@ -351,8 +353,8 @@ class Account{ if(!noNameChange && newName !== account.displayName){ promises.push(this.request("account/display_name", { display_name: newName - }).then(() => { - account.displayName = newName + }).then(response => { + account.displayName = response.display_name })) } var error = false @@ -361,9 +363,13 @@ class Account{ return } if(response && response.message){ - alert(response.message) + if(response.message in strings.serverError){ + this.error(strings.serverError[response.message]) + }else{ + this.error(response.message) + } }else{ - alert(strings.account.error) + this.error(strings.account.error) } } Promise.all(promises).then(() => { @@ -389,11 +395,11 @@ class Account{ new SongSelect(false, false, touched) }, 500) } - request(url, obj){ + request(url, obj, get){ this.lock(true) return new Promise((resolve, reject) => { var request = new XMLHttpRequest() - request.open("POST", "api/" + url) + request.open(get ? "GET" : "POST", "api/" + url) pageEvents.load(request).then(() => { this.lock(false) if(request.status !== 200){ @@ -435,6 +441,14 @@ class Account{ } } } + error(text){ + this.errorDiv.innerText = text + this.errorDiv.style.display = "block" + } + clearError(){ + this.errorDiv.innerText = "" + this.errorDiv.style.display = "" + } clean(eventsOnly, noReset){ if(!eventsOnly){ cancelTouch = true @@ -442,6 +456,10 @@ class Account{ this.gamepad.clean() } if(this.mode === "account"){ + if(!noReset){ + this.accountPass.reset() + this.accountDel.reset() + } pageEvents.remove(this.accounPassButton, ["click", "touchstart"]) pageEvents.remove(this.accountDelButton, ["click", "touchstart"]) pageEvents.remove(this.logoutButton, ["mousedown", "touchstart"]) @@ -449,10 +467,7 @@ class Account{ for(var i = 0; i < this.inputForms.length; i++){ pageEvents.remove(this.inputForms[i], ["keydown", "keyup", "keypress"]) } - if(!noReset){ - this.accountPass.reset() - this.accountDel.reset() - } + delete this.errorDiv delete this.displayname delete this.accountPassButton delete this.accountPass @@ -473,6 +488,7 @@ class Account{ for(var i = 0; i < this.form.length; i++){ pageEvents.remove(this.registerButton, ["keydown", "keyup", "keypress"]) } + delete this.errorDiv delete this.form delete this.password2 delete this.remember diff --git a/public/src/js/assets.js b/public/src/js/assets.js index f7e3ee7..220e6d5 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -1,7 +1,6 @@ var assets = { "js": [ "lib/md5.min.js", - "lib/js.cookie.min.js", "loadsong.js", "parseosu.js", "titlescreen.js", diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 583ffd3..9b06aa1 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -155,10 +155,16 @@ class Controller{ if(this.mainLoopRunning){ if(this.multiplayer !== 2){ requestAnimationFrame(() => { - this.viewLoop() + var player = this.multiplayer ? p2.player : 1 + if(player === 1){ + this.viewLoop() + } if(this.multiplayer === 1){ this.syncWith.viewLoop() } + if(player === 2){ + this.viewLoop() + } if(this.scoresheet){ if(this.view.ctx){ this.view.ctx.save() @@ -197,14 +203,14 @@ class Controller{ displayScore(score, notPlayed, bigNote){ this.view.displayScore(score, notPlayed, bigNote) } - songSelection(fadeIn){ + songSelection(fadeIn, scoreSaveFailed){ if(!fadeIn){ this.clean() } if(this.calibrationMode){ new SettingsView(this.touchEnabled, false, null, "latency") }else{ - new SongSelect(false, fadeIn, this.touchEnabled) + new SongSelect(false, fadeIn, this.touchEnabled, null, scoreSaveFailed) } } restartSong(){ diff --git a/public/src/js/game.js b/public/src/js/game.js index da9534e..e03e1f5 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -506,7 +506,7 @@ class Game{ if(this.musicFadeOut === 0){ if(this.controller.multiplayer === 1){ var obj = this.getGlobalScore() - obj.name = account.loggedIn ? account.displayName : strings.defaultName + obj.name = account.loggedIn ? account.displayName : null p2.send("gameresults", obj) } this.musicFadeOut++ diff --git a/public/src/js/lib/js.cookie.min.js b/public/src/js/lib/js.cookie.min.js deleted file mode 100644 index a0e6820..0000000 --- a/public/src/js/lib/js.cookie.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! js-cookie v3.0.0-rc.0 | MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t { @@ -156,22 +156,17 @@ class Loader{ } })) - if(gameConfig._accounts){ - var token = Cookies.get("session") - if(token){ - this.addPromise(this.ajax("/api/scores/get").then(response => { - response = JSON.parse(response) - if(response.status === "ok"){ - account.loggedIn = true - account.username = response.username - account.displayName = response.display_name - scoreStorage.load(response.scores) - pageEvents.send("login", account.username) - } - })) - }else{ - this.assetLoaded() - } + if(gameConfig.accounts){ + this.addPromise(this.ajax("/api/scores/get").then(response => { + response = JSON.parse(response) + if(response.status === "ok"){ + account.loggedIn = true + account.username = response.username + account.displayName = response.display_name + scoreStorage.load(response.scores) + pageEvents.send("login", account.username) + } + })) } settings = new Settings() diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index 3e5b460..4b42555 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -264,14 +264,14 @@ class LoadSong{ if(event.type === "gameload"){ this.cancelButton.style.display = "" - if(event.value === song.difficulty){ + if(event.value.diff === song.difficulty){ this.startMultiplayer() }else{ this.selectedSong2 = {} for(var i in this.selectedSong){ this.selectedSong2[i] = this.selectedSong[i] } - this.selectedSong2.difficulty = event.value + this.selectedSong2.difficulty = event.value.diff if(song.type === "tja"){ this.startMultiplayer() }else{ diff --git a/public/src/js/p2.js b/public/src/js/p2.js index 0857381..127b622 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -4,6 +4,7 @@ class P2Connection{ this.lastMessages = {} this.otherConnected = false this.name = null + this.player = 1 this.allEvents = new Map() this.addEventListener("message", this.message.bind(this)) this.currentHash = "" @@ -103,6 +104,10 @@ class P2Connection{ } message(response){ switch(response.type){ + case "gameload": + if("player" in response.value){ + this.player = response.value.player === 2 ? 2 : 1 + } case "gamestart": this.otherConnected = true this.notes = [] @@ -129,7 +134,7 @@ class P2Connection{ case "gameresults": this.results = {} 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 case "note": @@ -152,9 +157,12 @@ class P2Connection{ this.clearMessage("users") this.otherConnected = true this.session = true + if("player" in response.value){ + this.player = response.value.player === 2 ? 2 : 1 + } break case "name": - this.name = (response.value || "").toString() || null + this.name = response.value ? response.value.toString() : response.value break } } diff --git a/public/src/js/pageevents.js b/public/src/js/pageevents.js index 46c0a12..7779a24 100644 --- a/public/src/js/pageevents.js +++ b/public/src/js/pageevents.js @@ -86,7 +86,7 @@ class PageEvents{ }) } keyEvent(event){ - if(!("key" in 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){ diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index a2a62c4..265414e 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -2,9 +2,19 @@ class Scoresheet{ constructor(controller, results, multiplayer, touchEnabled){ this.controller = controller 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){ - this.results[i] = results[i].toString() + this.results[player0][i] = results[i] === null ? null : results[i].toString() } this.multiplayer = multiplayer this.touchEnabled = touchEnabled @@ -248,7 +258,7 @@ class Scoresheet{ var frameTop = winH / 2 - 720 / 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 bgOffset = 0 @@ -331,28 +341,21 @@ class Scoresheet{ } var rules = this.controller.game.rules - var gaugePercent = rules.gaugePercent(this.results.gauge) - var gaugeClear = [rules.gaugeClear] - if(players === 2){ - gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear) - } - 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]){ + var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000 + if(players === 2 && failedOffset !== 0){ + var p2results = this.results[this.player[1]] + if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){ failedOffset = 0 } } if(elapsed >= 3100 + failedOffset){ for(var p = 0; p < players; p++){ ctx.save() - var results = this.results - if(p === 1){ - results = p2.results + var results = this.results[p] + if(!results){ + continue } - var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules - var resultGauge = playerRules.gaugePercent(results.gauge) - var clear = resultGauge >= gaugeClear[p] + var clear = this.rules[p].clearReached(results.gauge) if(p === 1 || !this.multiplayer && clear){ ctx.translate(0, 290) } @@ -415,7 +418,7 @@ class Scoresheet{ this.draw.layeredText({ ctx: ctx, - text: this.results.title, + text: this.results[this.player[0]].title, fontSize: 40, fontFamily: this.font, x: 1257, @@ -431,9 +434,11 @@ class Scoresheet{ ctx.save() for(var p = 0; p < players; p++){ - var results = this.results + var results = this.results[p] + if(!results){ + continue + } if(p === 1){ - results = p2.results ctx.translate(0, p2Offset) } @@ -455,10 +460,11 @@ class Scoresheet{ ctx.fillText(text, 395, 308) ctx.miterLimit = 10 - if(p === 0){ - var name = account.loggedIn ? account.displayName : strings.defaultName + 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 + var name = results.name || defaultName } this.nameplateCache.get({ ctx: ctx, @@ -466,7 +472,7 @@ class Scoresheet{ y: 92, w: 273, h: 66, - id: p.toString() + "p", + id: p.toString() + "p" + name, }, ctx => { this.draw.nameplate({ ctx: ctx, @@ -609,7 +615,7 @@ class Scoresheet{ if(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) } } @@ -623,32 +629,32 @@ class Scoresheet{ ctx.translate(frameLeft, frameTop) for(var p = 0; p < players; p++){ - var results = this.results + var results = this.results[p] + if(!results){ + continue + } if(p === 1){ - results = p2.results ctx.translate(0, p2Offset) } - var gaugePercent = rules.gaugePercent(results.gauge) var w = 712 this.draw.gauge({ ctx: ctx, x: 558 + w, y: p === 1 ? 124 : 116, - clear: gaugeClear[p], - percentage: gaugePercent, + clear: this.rules[p].gaugeClear, + percentage: this.rules[p].gaugePercent(results.gauge), font: this.font, scale: w / 788, scoresheet: true, blue: p === 1, multiplayer: p === 1 }) - var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules this.draw.soul({ ctx: ctx, x: 1215, y: 144, scale: 36 / 42, - cleared: playerRules.clearReached(results.gauge) + cleared: this.rules[p].clearReached(results.gauge) }) } }) @@ -661,13 +667,12 @@ class Scoresheet{ var noCrownResultWait = -2000; for(var p = 0; p < players; p++){ - var results = this.results - if(p === 1){ - results = p2.results + var results = this.results[p] + if(!results){ + continue } var crownType = null - var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules - if(playerRules.clearReached(results.gauge)){ + if(this.rules[p].clearReached(results.gauge)){ crownType = results.bad === "0" ? "gold" : "silver" } if(crownType !== null){ @@ -730,7 +735,10 @@ class Scoresheet{ var times = {} var lastTime = 0 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 if(currentTime > lastTime){ lastTime = currentTime @@ -739,7 +747,10 @@ class Scoresheet{ for(var i in printNumbers){ var largestTime = 0 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 var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame if(currentTime > largestTime){ @@ -755,9 +766,11 @@ class Scoresheet{ } for(var p = 0; p < players; p++){ - var results = this.results + var results = this.results[p] + if(!results){ + continue + } if(p === 1){ - results = p2.results ctx.translate(0, p2Offset) } ctx.save() @@ -851,7 +864,7 @@ class Scoresheet{ if(elapsed >= 1000){ this.clean() - this.controller.songSelection(true) + this.controller.songSelection(true, this.scoreSaveFailed) } } @@ -918,10 +931,14 @@ class Scoresheet{ delete this.resultsObj.title delete this.resultsObj.difficulty delete this.resultsObj.gauge - scoreStorage.add(hash, difficulty, this.resultsObj, true, title) + scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => { + this.scoreSaveFailed = true + }) }else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){ oldScore.crown = crown - scoreStorage.add(hash, difficulty, oldScore, true, title) + scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => { + this.scoreSaveFailed = true + }) } } this.scoreSaved = true @@ -936,7 +953,7 @@ class Scoresheet{ snd.buffer.loadSettings() this.redrawRunning = false pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) - if(this.multiplayer !== 2 && this.touchEnabled){ + if(this.touchEnabled){ pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") } if(this.session){ @@ -948,5 +965,7 @@ class Scoresheet{ delete this.ctx delete this.canvas delete this.fadeScreen + delete this.results + delete this.rules } } diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js index db919c6..db6dd2b 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -6,23 +6,30 @@ class ScoreStorage{ this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"] this.crownValue = ["", "silver", "gold"] } - load(strings){ - this.scores = {} - if(strings){ - this.scoreStrings = this.prepareStrings(strings) + load(strings, loadFailed){ + var scores = {} + var scoreStrings = {} + if(loadFailed){ + try{ + var localScores = localStorage.getItem("saveFailed") + if(localScores){ + scoreStrings = JSON.parse(localScores) + } + }catch(e){} + }else if(strings){ + scoreStrings = this.prepareStrings(strings) }else if(account.loggedIn){ return }else{ - this.scoreStrings = {} try{ var localScores = localStorage.getItem("scoreStorage") if(localScores){ - this.scoreStrings = JSON.parse(localScores) + scoreStrings = JSON.parse(localScores) } }catch(e){} } - for(var hash in this.scoreStrings){ - var scoreString = this.scoreStrings[hash] + for(var hash in scoreStrings){ + var scoreString = scoreStrings[hash] var songAdded = false if(typeof scoreString === "string" && scoreString){ var diffArray = scoreString.split(";") @@ -42,14 +49,32 @@ class ScoreStorage{ score[name] = value } if(!songAdded){ - this.scores[hash] = {title: null} + scores[hash] = {title: null} 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 = [] @@ -126,7 +151,7 @@ class ScoreStorage{ } } } - add(song, difficulty, scoreObject, isHash, setTitle){ + add(song, difficulty, scoreObject, isHash, setTitle, saveFailed){ var hash = isHash ? song : this.titleHash(song) if(!(hash in this.scores)){ this.scores[hash] = {} @@ -137,11 +162,29 @@ class ScoreStorage{ this.scores[hash][difficulty] = scoreObject this.writeString(hash) this.write() - var obj = {} - obj[hash] = this.scoreStrings[hash] - this.sendToServer({ - scores: this.prepareScores(obj) - }).catch(() => this.add.apply(this, arguments)) + 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)) + } } template(){ var template = {crown: ""} @@ -192,10 +235,10 @@ class ScoreStorage{ } }).catch(() => { if(retry){ + this.scoreSaveFailed = true account.loggedIn = false delete account.username delete account.displayName - Cookies.remove("session") this.load() pageEvents.send("logout") return Promise.reject() diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 64b65de..0ae5b14 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -1,5 +1,5 @@ class SongSelect{ - constructor(fromTutorial, fadeIn, touchEnabled, songId){ + constructor(fromTutorial, fadeIn, touchEnabled, songId, scoreSaveFailed){ this.touchEnabled = touchEnabled loader.changePage("songselect", false) @@ -167,6 +167,9 @@ class SongSelect{ category: strings.random }) } + if(scoreSaveFailed){ + scoreStorage.scoreSaveFailed = true + } this.songs.push({ title: strings.aboutSimulator, skin: this.songSkin.about, @@ -379,7 +382,12 @@ class SongSelect{ return } var shift = event ? event.shiftKey : this.pressedKeys["shift"] - if(this.state.screen === "song"){ + if(this.state.scoreSaveFailed){ + if(name === "confirm"){ + this.playSound("se_don") + this.state.scoreSaveFailed = false + } + }else if(this.state.screen === "song"){ if(name === "confirm"){ this.toSelectDifficulty() }else if(name === "back"){ @@ -453,10 +461,15 @@ class SongSelect{ var ctrl = false var touch = true } - if(this.state.screen === "song"){ + if(this.state.scoreSaveFailed){ + if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){ + this.playSound("se_don") + this.state.scoreSaveFailed = false + } + }else if(this.state.screen === "song"){ if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ this.categoryJump(mouse.x < 640 ? -1 : 1) - }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig._accounts){ + }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){ this.toAccount() }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ this.toSession() @@ -508,10 +521,14 @@ class SongSelect{ mouseMove(event){ var mouse = this.mouseOffset(event.offsetX, event.offsetY) var moveTo = null - if(this.state.screen === "song"){ + if(this.state.scoreSaveFailed){ + if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){ + moveTo = "scoreSaveFailed" + } + }else if(this.state.screen === "song"){ if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext" - }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig._accounts){ + }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){ moveTo = "account" }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ moveTo = "session" @@ -831,6 +848,7 @@ class SongSelect{ } if(p2.session){ p2.send("gameend") + this.state.moveHover = null }else{ localStorage["selectedSong"] = this.selectedSong @@ -992,12 +1010,22 @@ class SongSelect{ }else{ this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + (ms - this.state.screenMS - 1000) } - if(ms > this.state.screenMS + 500){ + if(screen === "titleFadeIn" && ms > this.state.screenMS + 500){ this.state.screen = "title" screen = "title" } } + if(screen === "song" && scoreStorage.scoreSaveFailed && !p2.session){ + if(this.bgmEnabled){ + this.playBgm(false) + } + scoreStorage.scoreSaveFailed = false + this.state.scoreSaveFailed = true + this.state.locked = true + this.playSound("se_pause") + } + if(screen === "song"){ if(this.songs[this.selectedSong].courses){ selectedWidth = this.songAsset.selectedWidth @@ -1441,7 +1469,8 @@ class SongSelect{ ctx: ctx, font: this.font, x: _x, - y: _y - 45 + y: _y - 45, + two: p2.session && p2.player === 2 }) } } @@ -1579,15 +1608,15 @@ class SongSelect{ if(this.selectedDiff === 4 + this.diffOptions.length){ currentDiff = 3 } - if(i === currentSong.p2Cursor && p2.socket.readyState === 1){ + if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){ this.draw.diffCursor({ ctx: ctx, font: this.font, x: _x, - y: _y - (songSel ? 45 : 65), - two: true, - side: songSel ? false : (currentSong.p2Cursor === currentDiff), - scale: songSel ? 0.7 : 1 + y: _y - 45, + two: !p2.session || p2.player === 1, + side: false, + scale: 0.7 }) } if(!songSel){ @@ -1603,7 +1632,8 @@ class SongSelect{ font: this.font, x: _x, y: _y - 65, - side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1 + side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1, + two: p2.session && p2.player === 2 }) } if(highlight){ @@ -1644,6 +1674,22 @@ class SongSelect{ drawDifficulty(ctx, i, currentUra) } } + for(var i = 0; currentSong.courses && i < 4; i++){ + if(!songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){ + var _x = x + 402 + i * 100 + var _y = y + 87 + var currentDiff = this.selectedDiff - this.diffOptions.length + this.draw.diffCursor({ + ctx: ctx, + font: this.font, + x: _x, + y: _y - 65, + two: !p2.session || p2.player === 1, + side: currentSong.p2Cursor === currentDiff, + scale: 1 + }) + } + } var borders = (this.songAsset.border + this.songAsset.innerBorder) * 2 var textW = this.songAsset.width - borders @@ -1900,20 +1946,27 @@ class SongSelect{ ctx.lineTo(x + w - 4, y + 4) ctx.fill() + if(!p2.session || p2.player === 1){ + var name = account.loggedIn ? account.displayName : strings.defaultName + var rank = account.loggedIn || !gameConfig.accounts || p2.session ? false : strings.notLoggedIn + }else{ + var name = p2.name || strings.defaultName + var rank = false + } this.nameplateCache.get({ ctx: ctx, x: frameLeft + 60, y: frameTop + 640, w: 273, h: 66, - id: "1p", + id: "1p" + name + "\n" + rank, }, ctx => { this.draw.nameplate({ ctx: ctx, x: 3, y: 3, - name: account.loggedIn ? account.displayName : strings.defaultName, - rank: account.loggedIn || !gameConfig._accounts || p2.session ? false : strings.notLoggedIn, + name: name, + rank: rank, font: this.font }) }) @@ -2049,25 +2102,131 @@ class SongSelect{ } } if(p2.session){ + if(p2.player === 1){ + var name = p2.name || strings.default2PName + }else{ + var name = account.loggedIn ? account.displayName : strings.default2PName + } this.nameplateCache.get({ ctx: ctx, x: frameLeft + 949, y: frameTop + 640, w: 273, h: 66, - id: "2p", + id: "2p" + name, }, ctx => { this.draw.nameplate({ ctx: ctx, x: 3, y: 3, - name: p2.name, + name: name, font: this.font, blue: true }) }) } + if(this.state.scoreSaveFailed){ + if(this.preview){ + this.endPreview() + } + ctx.fillStyle = "rgba(0, 0, 0, 0.5)" + ctx.fillRect(0, 0, winW, winH) + + ctx.save() + ctx.translate(frameLeft, frameTop) + + var pauseRect = (ctx, mul) => { + this.draw.roundedRect({ + ctx: ctx, + x: 269 * mul, + y: 93 * mul, + w: 742 * mul, + h: 494 * mul, + radius: 17 * mul + }) + } + pauseRect(ctx, 1) + ctx.strokeStyle = "#fff" + ctx.lineWidth = 24 + ctx.stroke() + ctx.strokeStyle = "#000" + ctx.lineWidth = 12 + ctx.stroke() + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_pause"], + shape: pauseRect, + dx: 68, + dy: 11 + }) + this.draw.wrappingText({ + ctx: ctx, + text: strings.scoreSaveFailed, + fontSize: 30, + fontFamily: this.font, + x: 300, + y: 130, + width: 680, + height: 300, + lineHeight: 35, + fill: "#000", + verticalAlign: "middle", + textAlign: "center", + }) + + var _x = 640 + var _y = 470 + var _w = 464 + var _h = 80 + ctx.fillStyle = "#ffb447" + this.draw.roundedRect({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + radius: 30 + }) + ctx.fill() + var layers = [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ] + this.draw.layeredText({ + ctx: ctx, + text: strings.ok, + x: _x, + y: _y + 18, + width: _w, + height: _h - 54, + fontSize: 40, + fontFamily: this.font, + letterSpacing: -1, + align: "center" + }, layers) + + var highlight = 1 + if(this.state.moveHover === "scoreSaveFailed"){ + highlight = 2 + } + if(highlight){ + this.draw.highlight({ + ctx: ctx, + x: _x - _w / 2 - 3.5, + y: _y - 3.5, + w: _w + 7, + h: _h + 7, + animate: highlight === 1, + animateMS: this.state.moveMS, + opacity: highlight === 2 ? 0.8 : 1, + radius: 30 + }) + } + + ctx.restore() + } + if(screen === "titleFadeIn"){ ctx.save() @@ -2120,7 +2279,7 @@ class SongSelect{ }) } this.draw.songFrame(config) - if(config.song.p2Cursor && p2.socket.readyState === 1){ + if(config.song.p2Cursor !== null && p2.socket.readyState === 1){ this.draw.diffCursor({ ctx: ctx, font: this.font, @@ -2167,6 +2326,9 @@ class SongSelect{ } startPreview(loadOnly){ + if(!loadOnly && this.state && this.state.scoreSaveFailed){ + return + } var currentSong = this.songs[this.selectedSong] var id = currentSong.id var prvTime = currentSong.preview @@ -2242,6 +2404,9 @@ class SongSelect{ } } playBgm(enabled){ + if(enabled && this.state && this.state.scoreSaveFailed){ + return + } if(enabled && !this.bgmEnabled){ this.bgmEnabled = true snd.musicGain.fadeIn(0.4) diff --git a/public/src/js/strings.js b/public/src/js/strings.js index f4d0d75..1f58f11 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -37,6 +37,7 @@ this.oni = "おに" this.songBranch = "譜面分岐あり" this.defaultName = "どんちゃん" + this.default2PName = "かっちゃん" this.notLoggedIn = "ログインしていない" this.sessionStart = "オンラインセッションを開始する!" this.sessionEnd = "オンラインセッションを終了する" @@ -197,6 +198,7 @@ register: "登録", registerAccount: "アカウントを登録", passwordsDoNotMatch: "パスワードが一致しません", + newPasswordsDoNotMatch: "New passwords do not match", cannotBeEmpty: "%sは空にできません", error: "リクエストの処理中にエラーが発生しました", logout: "ログアウト", @@ -213,6 +215,17 @@ deleteAccount: "Delete Account", verifyPassword: "Verify password to delete this account" } + this.serverError = { + not_logged_in: "Not logged in", + invalid_username: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + username_in_use: "A user already exists with that username", + invalid_password: "Cannot use this password, please check that your password is at least 6 characters long", + invalid_username_password: "Invalid Username or Password", + invalid_display_name: "Cannot use this name, please check that your new name is at most 25 characters long", + current_password_invalid: "Current password does not match", + invalid_new_password: "Cannot use this password, please check that your new password is at least 6 characters long", + verify_password_invalid: "Verification password does not match" + } this.browserSupport = { browserWarning: "サポートされていないブラウザを実行しています (%s)", details: "詳しく", @@ -263,9 +276,11 @@ function StringsEn(){ this.oni = "Extreme" this.songBranch = "Diverge Notes" this.defaultName = "Don-chan" + this.default2PName = "Katsu-chan" this.notLoggedIn = "Not logged in" this.sessionStart = "Begin an Online Session!" this.sessionEnd = "End Online Session" + this.scoreSaveFailed = "Could not connect to the server, your score has not been saved.\n\nPlease log in or refresh the page to try saving the score again." this.loading = "Loading..." this.waitingForP2 = "Waiting for Another Player..." this.cancel = "Cancel" @@ -423,6 +438,7 @@ function StringsEn(){ register: "Register", registerAccount: "Register account", passwordsDoNotMatch: "Passwords do not match", + newPasswordsDoNotMatch: "New passwords do not match", cannotBeEmpty: "%s cannot be empty", error: "An error occurred while processing your request", logout: "Log Out", @@ -439,6 +455,17 @@ function StringsEn(){ deleteAccount: "Delete Account", verifyPassword: "Verify password to delete this account" } + this.serverError = { + not_logged_in: "Not logged in", + invalid_username: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + username_in_use: "A user already exists with that username", + invalid_password: "Cannot use this password, please check that your password is at least 6 characters long", + invalid_username_password: "Invalid Username or Password", + invalid_display_name: "Cannot use this name, please check that your new name is at most 25 characters long", + current_password_invalid: "Current password does not match", + invalid_new_password: "Cannot use this password, please check that your new password is at least 6 characters long", + verify_password_invalid: "Verification password does not match" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -489,6 +516,7 @@ function StringsCn(){ this.oni = "魔王" this.songBranch = "有谱面分歧" this.defaultName = "小咚" + this.default2PName = "小咔" this.notLoggedIn = "未登录" this.sessionStart = "开始在线会话!" this.sessionEnd = "结束在线会话" @@ -649,6 +677,7 @@ function StringsCn(){ register: "注册", registerAccount: "注册帐号", passwordsDoNotMatch: "密码不匹配", + newPasswordsDoNotMatch: "New passwords do not match", cannotBeEmpty: "%s不能为空", error: "处理您的请求时发生错误", logout: "登出", @@ -665,6 +694,17 @@ function StringsCn(){ deleteAccount: "Delete Account", verifyPassword: "Verify password to delete this account" } + this.serverError = { + not_logged_in: "Not logged in", + invalid_username: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + username_in_use: "A user already exists with that username", + invalid_password: "Cannot use this password, please check that your password is at least 6 characters long", + invalid_username_password: "Invalid Username or Password", + invalid_display_name: "Cannot use this name, please check that your new name is at most 25 characters long", + current_password_invalid: "Current password does not match", + invalid_new_password: "Cannot use this password, please check that your new password is at least 6 characters long", + verify_password_invalid: "Verification password does not match" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -715,6 +755,7 @@ function StringsTw(){ this.oni = "魔王" this.songBranch = "有譜面分歧" this.defaultName = "小咚" + this.default2PName = "小咔" this.notLoggedIn = "未登錄" this.sessionStart = "開始多人模式!" this.sessionEnd = "結束多人模式" @@ -875,6 +916,7 @@ function StringsTw(){ register: "註冊", registerAccount: "註冊帳號", passwordsDoNotMatch: "密碼不匹配", + newPasswordsDoNotMatch: "New passwords do not match", cannotBeEmpty: "%s不能為空", error: "處理您的請求時發生錯誤", logout: "登出", @@ -891,6 +933,17 @@ function StringsTw(){ deleteAccount: "Delete Account", verifyPassword: "Verify password to delete this account" } + this.serverError = { + not_logged_in: "Not logged in", + invalid_username: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + username_in_use: "A user already exists with that username", + invalid_password: "Cannot use this password, please check that your password is at least 6 characters long", + invalid_username_password: "Invalid Username or Password", + invalid_display_name: "Cannot use this name, please check that your new name is at most 25 characters long", + current_password_invalid: "Current password does not match", + invalid_new_password: "Cannot use this password, please check that your new password is at least 6 characters long", + verify_password_invalid: "Verification password does not match" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -941,6 +994,7 @@ function StringsKo(){ this.oni = "귀신" this.songBranch = "악보 분기 있습니다" this.defaultName = "동이" + this.default2PName = "딱이" this.notLoggedIn = "로그인하지 않았습니다" this.sessionStart = "온라인 세션 시작!" this.sessionEnd = "온라인 세션 끝내기" @@ -1101,6 +1155,7 @@ function StringsKo(){ register: "가입하기", registerAccount: "계정 등록", passwordsDoNotMatch: "비밀번호가 일치하지 않습니다", + newPasswordsDoNotMatch: "New passwords do not match", cannotBeEmpty: "%s 비어 있을 수 없습니다", error: "요청을 처리하는 동안 오류가 발생했습니다", logout: "로그 아웃", @@ -1117,6 +1172,17 @@ function StringsKo(){ deleteAccount: "Delete Account", verifyPassword: "Verify password to delete this account" } + this.serverError = { + not_logged_in: "Not logged in", + invalid_username: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + username_in_use: "A user already exists with that username", + invalid_password: "Cannot use this password, please check that your password is at least 6 characters long", + invalid_username_password: "Invalid Username or Password", + invalid_display_name: "Cannot use this name, please check that your new name is at most 25 characters long", + current_password_invalid: "Current password does not match", + invalid_new_password: "Cannot use this password, please check that your new password is at least 6 characters long", + verify_password_invalid: "Verification password does not match" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", diff --git a/public/src/js/view.js b/public/src/js/view.js index 235b169..12e48b0 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -129,6 +129,11 @@ this.nameplateCache = new CanvasCache(noSmoothing) this.multiplayer = this.controller.multiplayer + if(this.multiplayer === 2){ + this.player = p2.player === 2 ? 1 : 2 + }else{ + this.player = this.controller.multiplayer ? p2.player : 1 + } this.touchEnabled = this.controller.touchEnabled this.touch = -Infinity @@ -224,13 +229,12 @@ this.winH = winH this.ratio = ratio - if(this.multiplayer !== 2){ + if(this.player !== 2){ this.canvas.width = winW this.canvas.height = winH ctx.scale(ratio, ratio) this.canvas.style.width = (winW / this.pixelRatio) + "px" this.canvas.style.height = (winH / this.pixelRatio) + "px" - this.titleCache.resize(640, 90, ratio) } if(!this.multiplayer){ @@ -246,7 +250,7 @@ resized = true }else if(this.controller.game.paused && !document.hasFocus()){ return - }else if(this.multiplayer !== 2){ + }else if(this.player !== 2){ ctx.clearRect(0, 0, winW / ratio, winH / ratio) } winW /= ratio @@ -263,8 +267,8 @@ var frameTop = winH / 2 - 720 / 2 var frameLeft = winW / 2 - 1280 / 2 } - if(this.multiplayer === 2){ - frameTop += this.multiplayer === 2 ? 165 : 176 + if(this.player === 2){ + frameTop += 165 } if(touchMultiplayer){ if(!this.touchp2Class){ @@ -284,11 +288,11 @@ this.drawGogoTime() - if(!touchMultiplayer || this.multiplayer === 1 && frameTop >= 0){ + if(!touchMultiplayer || this.player === 1 && frameTop >= 0){ this.assets.drawAssets("background") } - if(this.multiplayer !== 2){ + if(this.player !== 2){ this.titleCache.get({ ctx: ctx, x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650), @@ -356,7 +360,7 @@ var score = this.controller.getGlobalScore() var gaugePercent = this.rules.gaugePercent(score.gauge) - if(this.multiplayer === 2){ + if(this.player === 2){ var scoreImg = "bg_score_p2" var scoreFill = "#6bbec0" }else{ @@ -379,17 +383,17 @@ size: 100, paddingLeft: 0 } - this.scorePos = {x: 363, y: frameTop + (this.multiplayer === 2 ? 520 : 227)} + this.scorePos = {x: 363, y: frameTop + (this.player === 2 ? 520 : 227)} var animPos = { x1: this.slotPos.x + 13, - y1: this.slotPos.y + (this.multiplayer === 2 ? 27 : -27), + y1: this.slotPos.y + (this.player === 2 ? 27 : -27), x2: winW - 38, - y2: frameTop + (this.multiplayer === 2 ? 484 : 293) + y2: frameTop + (this.player === 2 ? 484 : 293) } var taikoPos = { x: 19, - y: frameTop + (this.multiplayer === 2 ? 464 : 184), + y: frameTop + (this.player === 2 ? 464 : 184), w: 111, h: 130 } @@ -397,15 +401,16 @@ this.nameplateCache.get({ ctx: ctx, x: 167, - y: this.multiplayer === 2 ? 565 : 160, + y: this.player === 2 ? 565 : 160, w: 219, h: 53, id: "1p", }, ctx => { + var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName if(this.multiplayer === 2){ - var name = p2.name || strings.defaultName + var name = p2.name || defaultName }else{ - var name = account.loggedIn ? account.displayName : strings.defaultName + var name = account.loggedIn ? account.displayName : defaultName } this.draw.nameplate({ ctx: ctx, @@ -414,19 +419,19 @@ scale: 0.8, name: name, font: this.font, - blue: this.multiplayer === 2 + blue: this.player === 2 }) }) ctx.fillStyle = "#000" ctx.fillRect( 0, - this.multiplayer === 2 ? 306 : 288, + this.player === 2 ? 306 : 288, winW, - this.multiplayer === 1 ? 184 : 183 + this.player === 1 ? 184 : 183 ) ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(0, 467) ctx.lineTo(384, 467) ctx.lineTo(384, 512) @@ -445,7 +450,7 @@ ctx.fillStyle = scoreFill var leftSide = (ctx, mul) => { ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(0, 468 * mul) ctx.lineTo(380 * mul, 468 * mul) ctx.lineTo(380 * mul, 512 * mul) @@ -475,7 +480,7 @@ // Score background ctx.fillStyle = "#000" ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ this.draw.roundedCorner(ctx, 184, 512, 20, 0) ctx.lineTo(384, 512) this.draw.roundedCorner(ctx, 384, 560, 12, 2) @@ -493,16 +498,16 @@ ctx.drawImage(assets.image["difficulty"], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 168, 143, - 126, this.multiplayer === 2 ? 497 : 228, + 126, this.player === 2 ? 497 : 228, 62, 53 ) } // Badges - if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ + if(this.controller.autoPlayEnabled && !this.multiplayer){ this.ctx.drawImage(assets.image["badge_auto"], 183, - this.multiplayer === 2 ? 490 : 265, + this.player === 2 ? 490 : 265, 23, 23 ) @@ -512,7 +517,7 @@ ctx.fillStyle = "#000" ctx.beginPath() var gaugeX = winW - 788 * 0.7 - 32 - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(gaugeX, 464) ctx.lineTo(winW, 464) ctx.lineTo(winW, 489) @@ -527,18 +532,18 @@ this.draw.gauge({ ctx: ctx, x: winW, - y: this.multiplayer === 2 ? 468 : 273, + y: this.player === 2 ? 468 : 273, clear: this.rules.gaugeClear, percentage: gaugePercent, font: this.font, scale: 0.7, - multiplayer: this.multiplayer === 2, - blue: this.multiplayer === 2 + multiplayer: this.player === 2, + blue: this.player === 2 }) this.draw.soul({ ctx: ctx, x: winW - 40, - y: this.multiplayer === 2 ? 484 : 293, + y: this.player === 2 ? 484 : 293, scale: 0.75, cleared: this.rules.clearReached(score.gauge) }) @@ -566,29 +571,30 @@ } this.scorePos = { x: 155, - y: frameTop + (this.multiplayer === 2 ? 318 : 193) + y: frameTop + (this.player === 2 ? 318 : 193) } var animPos = { x1: this.slotPos.x + 14, - y1: this.slotPos.y + (this.multiplayer === 2 ? 29 : -29), + y1: this.slotPos.y + (this.player === 2 ? 29 : -29), x2: winW - 55, - y2: frameTop + (this.multiplayer === 2 ? 378 : 165) + y2: frameTop + (this.player === 2 ? 378 : 165) } var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162} this.nameplateCache.get({ ctx: ctx, x: 320, - y: this.multiplayer === 2 ? 460 : 20, + y: this.player === 2 ? 460 : 20, w: 273, h: 66, id: "1p", }, ctx => { + var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName if(this.multiplayer === 2){ - var name = p2.name || strings.defaultName + var name = p2.name || defaultName }else{ - var name = account.loggedIn ? account.displayName : strings.defaultName + var name = account.loggedIn ? account.displayName : defaultName } this.draw.nameplate({ ctx: ctx, @@ -596,7 +602,7 @@ y: 3, name: name, font: this.font, - blue: this.multiplayer === 2 + blue: this.player === 2 }) }) @@ -605,10 +611,10 @@ 0, 184, winW, - this.multiplayer === 1 ? 177 : 176 + this.multiplayer && this.player === 1 ? 177 : 176 ) ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(328, 351) ctx.lineTo(winW, 351) ctx.lineTo(winW, 385) @@ -625,17 +631,17 @@ this.draw.gauge({ ctx: ctx, x: winW, - y: this.multiplayer === 2 ? 357 : 135, + y: this.player === 2 ? 357 : 135, clear: this.rules.gaugeClear, percentage: gaugePercent, font: this.font, - multiplayer: this.multiplayer === 2, - blue: this.multiplayer === 2 + multiplayer: this.player === 2, + blue: this.player === 2 }) this.draw.soul({ ctx: ctx, x: winW - 57, - y: this.multiplayer === 2 ? 378 : 165, + y: this.player === 2 ? 378 : 165, cleared: this.rules.clearReached(score.gauge) }) @@ -667,7 +673,7 @@ ctx.drawImage(assets.image["difficulty"], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 168, 143, - 16, this.multiplayer === 2 ? 194 : 232, + 16, this.player === 2 ? 194 : 232, 141, 120 ) var diff = this.controller.selectedSong.difficulty @@ -679,13 +685,13 @@ ctx.fillStyle = "#fff" ctx.lineWidth = 7 ctx.miterLimit = 1 - ctx.strokeText(text, 87, this.multiplayer === 2 ? 310 : 348) - ctx.fillText(text, 87, this.multiplayer === 2 ? 310 : 348) + ctx.strokeText(text, 87, this.player === 2 ? 310 : 348) + ctx.fillText(text, 87, this.player === 2 ? 310 : 348) ctx.miterLimit = 10 } // Badges - if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ + if(this.controller.autoPlayEnabled && !this.multiplayer){ this.ctx.drawImage(assets.image["badge_auto"], 125, 235, 34, 34 ) @@ -694,7 +700,7 @@ // Score background ctx.fillStyle = "#000" ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(0, 312) this.draw.roundedCorner(ctx, 176, 312, 20, 1) ctx.lineTo(176, 353) @@ -719,11 +725,11 @@ }, { // 560, 10 x: animPos.x1 + animPos.w / 6, - y: animPos.y1 - animPos.h * (this.multiplayer === 2 ? 2.5 : 3.5) + y: animPos.y1 - animPos.h * (this.player === 2 ? 2.5 : 3.5) }, { // 940, -150 x: animPos.x2 - animPos.w / 3, - y: animPos.y2 - animPos.h * (this.multiplayer === 2 ? 3.5 : 5) + y: animPos.y2 - animPos.h * (this.player === 2 ? 3.5 : 5) }, { // 1225, 165 x: animPos.x2, @@ -1443,12 +1449,12 @@ var selectedSong = this.controller.selectedSong var songSkinName = selectedSong.songSkin.name var donLayers = [] - var filename = !selectedSong.songSkin.don && this.multiplayer === 2 ? "bg_don2_" : "bg_don_" + var filename = !selectedSong.songSkin.don && this.player === 2 ? "bg_don2_" : "bg_don_" var prefix = "" this.donBg = document.createElement("div") this.donBg.classList.add("donbg") - if(this.multiplayer === 2){ + if(this.player === 2){ this.donBg.classList.add("donbg-bottom") } for(var layer = 1; layer <= 3; layer++){ diff --git a/public/src/js/viewassets.js b/public/src/js/viewassets.js index dae06c6..9affaf7 100644 --- a/public/src/js/viewassets.js +++ b/public/src/js/viewassets.js @@ -18,7 +18,7 @@ class ViewAssets{ sw: imgw, sh: imgh - 1, 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, h: h - 1 } diff --git a/public/src/views/account.html b/public/src/views/account.html index 1271156..b236652 100644 --- a/public/src/views/account.html +++ b/public/src/views/account.html @@ -2,9 +2,10 @@