From ff09cb83bda914bfade80b0f85804dd0d0870feb Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Thu, 28 Nov 2019 09:04:40 +0300 Subject: [PATCH] Add global offset Adds new settings for controlling the note offset while playing. It can be either an actual offset (it is called "Audio Latency" in the settings) or just the visual offset ("Video Latency"). With higher audio latency it means you have to press the button sooner than what you hear, similarly with higher video latency it is sooner than what you see. By offsetting these events the game would play better, however, the sound effect of you hitting the drum would still play at the wrong time, the code cannot anticipate you to hit the drum in the future so to work around this issue a new option that disables drum sounds is also included. These settings could be set through trial and error but it would be better to get the correct values through the automated latency calibration, where you can hit the drum as you hear sounds or see a blinking animation. I tried making one by measuring latency from user input, adding all the latency up, and dividing, but that gives unreliable results. I hope someone suggests to me what I should be doing during the calibration to get better results, as I cannot figure what to do on my own. --- public/assets/audio/calibration.wav | Bin 0 -> 9238 bytes public/src/css/game.css | 8 - public/src/css/view.css | 72 +++- public/src/js/about.js | 35 +- public/src/js/assets.js | 3 +- public/src/js/canvascache.js | 6 +- public/src/js/canvasdraw.js | 170 ++++++++- public/src/js/controller.js | 87 +++-- public/src/js/game.js | 172 +++++++-- public/src/js/gameinput.js | 16 +- public/src/js/loadsong.js | 30 +- public/src/js/parsetja.js | 2 +- public/src/js/scoresheet.js | 16 +- public/src/js/settings.js | 485 +++++++++++++++++++++--- public/src/js/songselect.js | 70 ++-- public/src/js/strings.js | 155 ++++++++ public/src/js/view.js | 550 +++++++++++++++++++++------- public/src/views/about.html | 4 +- public/src/views/game.html | 1 - public/src/views/settings.html | 10 + 20 files changed, 1579 insertions(+), 313 deletions(-) create mode 100644 public/assets/audio/calibration.wav diff --git a/public/assets/audio/calibration.wav b/public/assets/audio/calibration.wav new file mode 100644 index 0000000000000000000000000000000000000000..f1459e17977ef835fdd23966ff2531ff8ba3d64b GIT binary patch literal 9238 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 F{{u=r{4f9j literal 0 HcmV?d00001 diff --git a/public/src/css/game.css b/public/src/css/game.css index f8b7214..69d1127 100644 --- a/public/src/css/game.css +++ b/public/src/css/game.css @@ -16,14 +16,6 @@ width: 100%; height: 100%; } -#cursor{ - position: fixed; - width: 1px; - height: 1px; - cursor: none; - pointer-events: none; - z-index: 1; -} #touch-drum{ display: none; position: absolute; diff --git a/public/src/css/view.css b/public/src/css/view.css index 0340d97..76ed3f4 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -158,8 +158,7 @@ kbd{ .setting-box:first-child{ margin-top: 0; } -.settings-outer .view-content:not(:hover) .setting-box.selected, -.view-outer:not(.settings-outer) .setting-box.selected, +.view-content:not(:hover) .setting-box.selected, .setting-box:hover{ background: #ffb547; animation: 2s linear border-pulse infinite; @@ -177,7 +176,6 @@ kbd{ overflow: hidden; } .view-content:not(:hover) .setting-box.selected .setting-name, -.view-outer:not(.settings-outer) .setting-box.selected .setting-name, .setting-box:hover .setting-name, .setting-box:hover #gamepad-value{ color: #fff; @@ -193,6 +191,8 @@ kbd{ border-radius: 0.2em; padding: 0.5em; box-sizing: border-box; + overflow: hidden; + white-space: nowrap; } .setting-value.selected{ width: calc(50% + 0.2em); @@ -215,27 +215,26 @@ kbd{ background: rgba(0, 0, 0, 0.5); z-index: 1; } -#settings-gamepad{ +#settings-gamepad, +#settings-latency{ display: none; } #settings-gamepad .view{ - position: absolute; - margin: auto; - top: 0; - right: 0; - bottom: 0; - left: 0; - width: 574px; - height: 428px; - max-height: calc(100vh - 14em + 88px); + width: 29.9em; + max-width: 100vw; } #settings-gamepad .setting-box{ height: auto; + overflow: hidden; +} +#gamepad-bg, +#gamepad-buttons{ + background-size: 20.53em; } #gamepad-bg{ position: relative; - width: 550px; - height: 317px; + width: 20.53em; + height: 11.83em; max-height: none; background-repeat: no-repeat; text-align: center; @@ -244,11 +243,11 @@ kbd{ } #gamepad-buttons{ position: absolute; - left: 141px; - top: 120px; - width: 282px; - height: 131px; - background-position: 0 -318px; + left: 5.26em; + top: 4.48em; + width: 10.52em; + height: 4.89em; + background-position: 0 -11.87em; background-repeat: no-repeat; pointer-events: none; } @@ -259,3 +258,36 @@ kbd{ #gamepad-value::before{ left: auto; } +#settings-latency .view{ + width: 30em; +} +#settings-latency .setting-value{ + position: relative; +} +.setting-value:not(.selected) .latency-buttons{ + display: none; +} +.setting-value .latency-buttons{ + position: absolute; + top: 0; + right: 0; + bottom: 0; + padding: 0; +} +.latency-buttons span{ + display: inline-block; + width: 2em; + height: 100%; + text-align: center; + background-color: #c3862a; + color: #fff; + line-height: 2em; + outline: none; +} +.latency-buttons span:hover, +.latency-buttons span:active{ + background-color: #946013; +} +.left-buttons .taibtn{ + z-index: 1; +} diff --git a/public/src/js/about.js b/public/src/js/about.js index ad571ba..afa4a23 100644 --- a/public/src/js/about.js +++ b/public/src/js/about.js @@ -29,23 +29,30 @@ this.endButton.innerText = strings.tutorial.ok this.endButton.setAttribute("alt", strings.tutorial.ok) + this.items = [] + var versionUrl = gameConfig._version.url this.getLink(this.linkIssues).href = versionUrl + "issues" - + this.items.push(this.linkIssues) + var contactEmail = gameConfig.email - if (typeof contactEmail === 'string') { + this.hasEmail = typeof contactEmail === "string" + if(this.hasEmail){ this.linkEmail.setAttribute("alt", contactEmail) this.getLink(this.linkEmail).href = "mailto:" + contactEmail - this.getLink(this.linkEmail).text = contactEmail - } else { - this.linkEmail.style.display = "none" + this.getLink(this.linkEmail).innerText = contactEmail + this.items.push(this.linkEmail) + }else{ + this.linkEmail.parentNode.removeChild(this.linkEmail) } - + pageEvents.add(this.linkIssues, ["click", "touchend"], this.linkButton.bind(this)) - pageEvents.add(this.linkEmail, ["click", "touchend"], this.linkButton.bind(this)) + if(this.hasEmail){ + pageEvents.add(this.linkEmail, ["click", "touchend"], this.linkButton.bind(this)) + } pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) - this.items = [this.linkIssues, this.linkEmail, this.endButton] - this.selected = 2 + this.items.push(this.endButton) + this.selected = this.items.length - 1 this.keyboard = new Keyboard({ confirm: ["enter", "space", "don_l", "don_r"], @@ -146,6 +153,8 @@ } } diag.push("Language: " + strings.id + userLangStr) + var latency = settings.getItem("latency") + diag.push("Audio Latency: " + (latency.audio > 0 ? "+" : "") + latency.audio.toString() + "ms, Video Latency: " + (latency.video > 0 ? "+" : "") + latency.video.toString() + "ms") var errorObj = {} if(localStorage["lastError"]){ try{ @@ -195,7 +204,9 @@ } var issueBody = strings.about.issueTemplate + "\n\n\n\n" + diag - this.getLink(this.linkEmail).href += "?body=" + encodeURIComponent(issueBody.replace(/\n/g, "
\r\n")) + if(this.hasEmail){ + this.getLink(this.linkEmail).href += "?body=" + encodeURIComponent(issueBody.replace(/\n/g, "
\r\n")) + } return diag } @@ -214,7 +225,9 @@ this.keyboard.clean() this.gamepad.clean() pageEvents.remove(this.linkIssues, ["click", "touchend"]) - pageEvents.remove(this.linkEmail, ["click", "touchend"]) + if(this.hasEmail){ + pageEvents.remove(this.linkEmail, ["click", "touchend"]) + } pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) if(this.textarea){ pageEvents.remove(this.textarea, ["focus", "blur"]) diff --git a/public/src/js/assets.js b/public/src/js/assets.js index 6245548..5c3e3bf 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -114,7 +114,8 @@ var assets = { "v_sanka.wav", "v_songsel.wav", "v_start.wav", - "v_title.wav" + "v_title.wav", + "calibration.wav" ], "audioSfxLR": [ "neiro_1_don.wav", diff --git a/public/src/js/canvascache.js b/public/src/js/canvascache.js index a731bcb..c3c6913 100644 --- a/public/src/js/canvascache.js +++ b/public/src/js/canvascache.js @@ -1,5 +1,6 @@ class CanvasCache{ - constructor(w, h, scale){ + constructor(noSmoothing, w, h, scale){ + this.noSmoothing = noSmoothing if(w){ this.resize(w, h, scale) } @@ -11,6 +12,9 @@ class CanvasCache{ this.map = new Map() this.canvas = document.createElement("canvas") this.ctx = this.canvas.getContext("2d") + if(this.noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } } this.scale = scale this.x = 0 diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index 0e77fbb..baea01c 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -1,5 +1,5 @@ class CanvasDraw{ - constructor(){ + constructor(noSmoothing){ this.diffStarPath = new Path2D(vectors.diffStar) this.longVowelMark = new Path2D(vectors.longVowelMark) @@ -68,7 +68,8 @@ emCap: /[MWMW]/, rWidth: /[abdfIjo-rtvabdfIjo-rtv]/, lWidth: /[ilil]/, - ura: /\s*[\((]裏[\))]$/ + ura: /\s*[\((]裏[\))]$/, + cjk: /[\u3040-ゞ゠-ヾ一-\u9ffe]/ } var numbersFull = "0123456789" @@ -78,10 +79,12 @@ this.numbersFullToHalf[numbersFull[i]] = numbersHalf[i] this.numbersFullToHalf[numbersHalf[i]] = numbersHalf[i] } + this.wrapOn = [" ", "\n", "%s"] + this.stickySymbols = "!,.:;?~‐–‼、。々〜ぁぃぅぇぉっゃゅょァィゥェォッャュョ・ーヽヾ!:;?" - this.songFrameCache = new CanvasCache() - this.diffStarCache = new CanvasCache() - this.crownCache = new CanvasCache() + this.songFrameCache = new CanvasCache(noSmoothing) + this.diffStarCache = new CanvasCache(noSmoothing) + this.crownCache = new CanvasCache(noSmoothing) this.tmpCanvas = document.createElement("canvas") this.tmpCtx = this.tmpCanvas.getContext("2d") @@ -818,6 +821,163 @@ ctx.restore() } + wrappingText(config){ + var ctx = config.ctx + var inputText = config.text.toString() + var words = [] + var start = 0 + var substituteIndex = 0 + while(start < inputText.length){ + var character = inputText.slice(start, start + 1) + if(words.length !== 0){ + var previous = words[words.length - 1] + if(!previous.substitute && previous !== "\n" && this.stickySymbols.indexOf(character) !== -1){ + words[words.length - 1] += character + start++ + continue + } + } + var index = Infinity + var currentIndex = inputText.slice(start).search(this.regex.cjk) + if(currentIndex !== -1){ + index = start + currentIndex + var on = inputText.charAt(index) + } + for(var i = 0; i < this.wrapOn.length; i++){ + var currentIndex = inputText.indexOf(this.wrapOn[i], start) + if(currentIndex !== -1 && currentIndex < index){ + var on = this.wrapOn[i] + index = currentIndex + } + } + if(index === Infinity){ + if(start !== inputText.length){ + words.push(inputText.slice(start, inputText.length)) + } + break + } + var end = index + (on === " " ? 1 : 0) + if(start !== end){ + words.push(inputText.slice(start, end)) + } + if(on === "%s" && config.substitute){ + words.push({ + substitute: true, + index: substituteIndex, + width: config.substitute(config, substituteIndex, true) || 0 + }) + substituteIndex++ + }else if(on !== " "){ + words.push(on) + } + start = index + on.length + } + + ctx.save() + + var bold = this.bold(config.fontFamily) + ctx.font = bold + config.fontSize + "px " + config.fontFamily + ctx.textBaseline = config.baseline || "top" + ctx.textAlign = "left" + ctx.fillStyle = config.fill + var lineHeight = config.lineHeight || config.fontSize + + var x = 0 + var y = 0 + var totalW = 0 + var totalH = 0 + var line = "" + var toDraw = [] + var lastWidth = 0 + + var addToDraw = obj => { + toDraw.push(obj) + if(x + lastWidth > totalW){ + totalW = x + lastWidth + } + if(y + lineHeight > totalH){ + totalH = y + lineHeight + } + } + var recenter = () => { + if(config.textAlign === "center"){ + for(var j in toDraw){ + if(toDraw[j].y === y){ + toDraw[j].x += (config.width - x - lastWidth) / 2 + } + } + } + } + + for(var i in words){ + var skip = words[i].substitute || words[i] === "\n" + if(!skip){ + var currentWidth = ctx.measureText(line + words[i]).width + } + if(skip || (x !== 0 || line) && x + currentWidth > config.width){ + if(line){ + addToDraw({ + text: line, + x: x, y: y + }) + } + if(words[i].substitute){ + line = "" + var currentWidth = words[i].width + if(x + lastWidth + currentWidth > config.width){ + recenter() + x = 0 + y += lineHeight + lastWidth = 0 + } + addToDraw({ + substitute: true, + index: words[i].index, + x: x + lastWidth, y: y + }) + x += lastWidth + currentWidth + lastWidth = currentWidth + }else{ + recenter() + x = 0 + y += lineHeight + line = words[i] === "\n" ? "" : words[i] + lastWidth = ctx.measureText(line).width + } + }else{ + line += words[i] + lastWidth = currentWidth + } + } + if(line){ + addToDraw({ + text: line, + x: x, y: y + }) + recenter() + } + + var addX = 0 + var addY = 0 + if(config.verticalAlign === "middle"){ + addY = ((config.height || 0) - totalH) / 2 + } + for(var i in toDraw){ + var x = config.x + toDraw[i].x + addX + var y = config.y + toDraw[i].y + addY + if(toDraw[i].text){ + ctx.fillText(toDraw[i].text, x, y) + }else if(toDraw[i].substitute){ + ctx.save() + ctx.translate(x, y) + config.substitute(config, toDraw[i].index) + ctx.restore() + } + } + + ctx.restore() + } + diffIcon(config){ var ctx = config.ctx var scale = config.scale diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 6020d2a..78f64ef 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -7,6 +7,16 @@ class Controller{ this.touchEnabled = touchEnabled this.snd = this.multiplayer ? "_p" + this.multiplayer : "" + this.calibrationMode = selectedSong.folder === "calibration" + this.audioLatency = 0 + this.videoLatency = 0 + if(!this.calibrationMode){ + var latency = settings.getItem("latency") + if(!autoPlayEnabled){ + this.audioLatency = Math.round(latency.audio) || 0 + } + this.videoLatency = Math.round(latency.video) || 0 + this.audioLatency + } if(this.multiplayer !== 2){ loader.changePage("game", false) } @@ -18,18 +28,23 @@ class Controller{ } this.offset = this.parsedSongData.soundOffset - assets.songs.forEach(song => { - if(song.id == this.selectedSong.folder){ - this.mainAsset = song.sound - this.volume = song.volume || 1 - } - }) + if(this.calibrationMode){ + this.volume = 1 + }else{ + assets.songs.forEach(song => { + if(song.id == this.selectedSong.folder){ + this.mainAsset = song.sound + this.volume = song.volume || 1 + } + }) + } this.game = new Game(this, this.selectedSong, this.parsedSongData) this.view = new View(this) this.mekadon = new Mekadon(this, this.game) this.keyboard = new GameInput(this) + this.drumSounds = settings.getItem("latency").drumSounds this.playedSounds = {} } run(syncWith){ @@ -72,8 +87,8 @@ class Controller{ } stopMainLoop(){ this.mainLoopRunning = false - if(this.mainAsset){ - this.mainAsset.stop() + if(this.game.mainAsset){ + this.game.mainAsset.stop() } if(this.multiplayer !== 2){ clearInterval(this.gameInterval) @@ -90,13 +105,18 @@ class Controller{ if(this.game.musicFadeOut < 3){ this.keyboard.checkMenuKeys() } + if(this.calibrationMode){ + this.game.calibration() + } if(!this.game.isPaused()){ this.keyboard.checkGameKeys() if(ms < 0){ this.game.updateTime() }else{ - this.game.update() + if(!this.calibrationMode){ + this.game.update() + } if(!this.mainLoopRunning){ return } @@ -158,7 +178,11 @@ class Controller{ if(!fadeIn){ this.clean() } - new SongSelect(false, fadeIn, this.touchEnabled) + if(this.calibrationMode){ + new SettingsView(this.touchEnabled, false, null, "latency") + }else{ + new SongSelect(false, fadeIn, this.touchEnabled) + } } restartSong(){ this.clean() @@ -166,20 +190,24 @@ class Controller{ new LoadSong(this.selectedSong, false, true, this.touchEnabled) }else{ new Promise(resolve => { - var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) - if(songObj.chart){ - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => { - this.songData = event.target.result.replace(/\0/g, "").split("\n") - resolve() - }) - if(this.selectedSong.type === "tja"){ - reader.readAsText(songObj.chart, "sjis") - }else{ - reader.readAsText(songObj.chart) - } - }else{ + if(this.calibrationMode){ resolve() + }else{ + var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) + if(songObj.chart && songObj.chart !== "blank"){ + var reader = new FileReader() + var promise = pageEvents.load(reader).then(event => { + this.songData = event.target.result.replace(/\0/g, "").split("\n") + resolve() + }) + if(this.selectedSong.type === "tja"){ + reader.readAsText(songObj.chart, "sjis") + }else{ + reader.readAsText(songObj.chart) + } + }else{ + resolve() + } } }).then(() => { var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) @@ -187,10 +215,13 @@ class Controller{ }) } } - playSound(id, time){ + playSound(id, time, noSnd){ + if(!this.drumSounds && (id === "neiro_1_don" || id === "neiro_1_ka" || id === "se_don" || id === "se_ka")){ + return + } var ms = Date.now() + (time || 0) * 1000 if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ - assets.sounds[id + this.snd].play(time) + assets.sounds[id + (noSnd ? "" : this.snd)].play(time) this.playedSounds[id] = ms } } @@ -201,11 +232,11 @@ class Controller{ } this.playSound(soundID + meka, time) } - togglePause(){ + togglePause(forcePause, pauseMove, noSound){ if(this.multiplayer === 1){ - this.syncWith.game.togglePause() + this.syncWith.game.togglePause(forcePause, pauseMove, noSound) } - this.game.togglePause() + this.game.togglePause(forcePause, pauseMove, noSound) } getKeys(){ return this.keyboard.getKeys() diff --git a/public/src/js/game.js b/public/src/js/game.js index dd05956..6a2aa13 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -45,7 +45,12 @@ class Game{ } initTiming(){ // Date when the chrono is started (before the game begins) - var offsetTime = Math.max(0, this.timeForDistanceCircle - this.songData.circles[0].ms) |0 + var firstCircle = this.songData.circles[0] + if(this.controller.calibrationMode){ + var offsetTime = 0 + }else{ + var offsetTime = Math.max(0, this.timeForDistanceCircle - (firstCircle ? firstCircle.ms : 0)) |0 + } if(this.controller.multiplayer){ var syncWith = this.controller.syncWith var syncCircles = syncWith.game.songData.circles @@ -57,8 +62,8 @@ class Game{ this.startDate = Date.now() + offsetTime } update(){ - // Main operations this.updateTime() + // Main operations this.updateCirclesStatus() this.checkPlays() // Event operations @@ -82,10 +87,10 @@ class Game{ if(circle && (!circle.branch || circle.branch.active) && !circle.isPlayed){ var type = circle.type var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" - var endTime = circle.endTime + (drumrollNotes ? 0 : this.rules.bad) + var endTime = circle.endTime + (drumrollNotes ? 0 : this.rules.bad) + this.controller.audioLatency - if(ms >= circle.ms){ - if(drumrollNotes && !circle.rendaPlayed && ms < endTime){ + if(ms >= circle.ms + this.controller.audioLatency){ + if(drumrollNotes && !circle.rendaPlayed && ms < endTime + this.controller.audioLatency){ circle.rendaPlayed = true if(this.rules.difficulty === "easy"){ assets.sounds["v_renda" + this.controller.snd].stop() @@ -109,7 +114,7 @@ class Game{ this.updateCurrentCircle() if(this.controller.multiplayer === 1){ var value = { - pace: (ms - circle.ms) / circle.timesHit + pace: (ms - circle.ms - this.controller.audioLatency) / circle.timesHit } if(type === "drumroll" || type === "daiDrumroll"){ value.kaAmount = circle.timesKa / circle.timesHit @@ -211,7 +216,7 @@ class Game{ for(var i = this.currentCircle + 1; i < circles.length; i++){ var circle = circles[i] - var relative = ms - circle.ms + var relative = ms - circle.ms - this.controller.audioLatency if(!circle.branch || circle.branch.active){ if((!circleIsNote(circle) || relative < -this.rules.bad)){ break @@ -310,7 +315,7 @@ class Game{ var keyTime = this.controller.getKeyTime() var currentTime = keysDon ? keyTime["don"] : keyTime["ka"] - var relative = currentTime - circle.ms + var relative = currentTime - circle.ms - this.controller.audioLatency if(relative >= this.rules.ok){ var fixedNote = this.fixNoteStream(keysDon) @@ -366,7 +371,7 @@ class Game{ if(this.controller.multiplayer === 1){ var value = { score: score, - ms: circle.ms - currentTime, + ms: circle.ms - currentTime - this.controller.audioLatency, dai: typeDai ? (keyDai ? 2 : 1) : 0 } if((!keysDon || !typeDon) && (!keysKa || !typeKa)){ @@ -375,7 +380,7 @@ class Game{ p2.send("note", value) } }else{ - if(circle.ms > currentTime || currentTime > circle.endTime){ + if(circle.ms + this.controller.audioLatency > currentTime || currentTime > circle.endTime + this.controller.audioLatency){ return true } if(keysDon && type === "balloon"){ @@ -400,7 +405,7 @@ class Game{ circle.played(score) if(this.controller.multiplayer == 1){ p2.send("drumroll", { - pace: (this.elapsedTime - circle.ms) / circle.timesHit + pace: (this.elapsedTime - circle.ms + this.controller.audioLatency) / circle.timesHit }) } }else{ @@ -447,17 +452,19 @@ class Game{ var ms = this.elapsedTime if(!this.lastCircle){ var circles = this.songData.circles - this.lastCircle = circles[circles.length - 1].endTime + var circle = circles[circles.length - 1] + this.lastCircle = circle ? circle.endTime : 0 if(this.controller.multiplayer){ var syncWith = this.controller.syncWith var syncCircles = syncWith.game.songData.circles - var syncLastCircle = syncCircles[syncCircles.length - 1].endTime + circle = syncCircles[syncCircles.length - 1] + var syncLastCircle = circle ? circle.endTime : 0 if(syncLastCircle > this.lastCircle){ this.lastCircle = syncLastCircle } } } - if(!this.fadeOutStarted && ms >= this.lastCircle + 2000){ + if(!this.fadeOutStarted && ms >= this.lastCircle + 2000 + this.controller.audioLatency){ this.fadeOutStarted = ms if(this.controller.multiplayer){ this.controller.syncWith.game.fadeOutStarted = ms @@ -495,28 +502,51 @@ class Game{ playMainMusic(){ var ms = this.elapsedTime + this.controller.offset if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms < this.fadeOutStarted + 1600)){ - if(this.controller.multiplayer !== 2 && this.mainAsset){ + if(this.calibrationState === "audio"){ + var beatInterval = this.controller.view.beatInterval + var startAt = ms % beatInterval + var duration = this.mainAsset.duration * 1000 + if(startAt < duration){ + this.mainAsset.playLoop(0, false, startAt / 1000, 0, beatInterval / 1000) + }else{ + this.mainAsset.playLoop((startAt - duration) / 1000, false, 0, 0, beatInterval / 1000) + } + }else if(this.controller.multiplayer !== 2 && this.mainAsset){ this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000)) } this.mainMusicPlaying = true } } - togglePause(){ + togglePause(forcePause, pauseMove, noSound){ if(!this.paused){ - assets.sounds["se_pause"].play() + if(forcePause === false){ + return + } + if(!noSound){ + this.controller.playSound("se_pause", 0, true) + } this.paused = true this.latestDate = Date.now() if(this.mainAsset){ this.mainAsset.stop() } this.mainMusicPlaying = false - this.view.pauseMove(0, true) + this.view.pauseMove(pauseMove || 0, true) this.view.gameDiv.classList.add("game-paused") this.view.lastMousemove = this.view.getMS() this.view.cursorHidden = false pageEvents.send("pause") - }else{ - assets.sounds["se_cancel"].play() + }else if(!forcePause){ + if(forcePause !== false && this.calibrationState && ["audioHelp", "audioComplete", "videoHelp", "videoComplete", "results"].indexOf(this.calibrationState) !== -1){ + return + } + if(this.calibrationState === "audioHelp" || this.calibrationState === "videoHelp"){ + this.calibrationState = this.calibrationState === "audioHelp" ? "audio" : "video" + this.controller.view.pauseOptions = strings.pauseOptions + this.controller.playSound("se_don", 0, true) + }else if(!noSound){ + this.controller.playSound("se_cancel", 0, true) + } this.paused = false var currentDate = Date.now() this.startDate += currentDate - this.latestDate @@ -683,7 +713,7 @@ class Game{ if(!circle || circle.branch === currentBranch[pastActive]){ var ms = this.elapsedTime var closestCircle = circles.findIndex(circle => { - return (!circle.branch || circle.branch.active) && circle.endTime >= ms + return (!circle.branch || circle.branch.active) && circle.endTime + this.controller.audioLatency >= ms }) if(closestCircle !== -1){ this.currentCircle = closestCircle @@ -701,4 +731,104 @@ class Game{ this.sectionNotes = [] this.sectionDrumroll = 0 } + clearKeyTime(){ + var keyboard = this.controller.keyboard + for(var key in keyboard.keyTime){ + keyboard.keys[key] = null + keyboard.keyTime[key] = -Infinity + } + } + calibration(){ + var view = this.controller.view + if(!this.calibrationState){ + this.controller.parsedSongData.measures = [] + this.calibrationProgress = { + audio: 0, + video: 0, + requirement: 40 + } + this.calibrationReset("audio", true) + } + var progress = this.calibrationProgress + var state = this.calibrationState + switch(state){ + case "audio": + case "video": + if(state === "audio" && !this.mainAsset){ + this.mainAsset = assets.sounds["calibration"] + this.mainMusicPlaying = false + } + if(progress.hit >= progress.requirement){ + var reduced = 0 + for(var i = 2; i < progress.offsets.length; i++){ + reduced += progress.offsets[i] + } + progress[state] = Math.max(0, Math.round(reduced / progress.offsets.length - 2)) + this.calibrationState += "Complete" + view.pauseOptions = [] + this.clearKeyTime() + this.togglePause(true, 1) + this.mainAsset = null + } + break + case "audioComplete": + case "videoComplete": + if(Date.now() - this.latestDate > 3000){ + var audioComplete = this.calibrationState === "audioComplete" + this.controller.playSound("se_pause", 0, true) + if(audioComplete){ + this.calibrationReset("video") + }else{ + view.pauseOptions = [ + strings.calibration.retryPrevious, + strings.calibration.finish + ] + } + this.calibrationState = audioComplete ? "videoHelp" : "results" + } + break + } + } + calibrationHit(ms){ + var progress = this.calibrationProgress + var beatInterval = this.controller.view.beatInterval + var current = Math.floor((ms + 100) / beatInterval) + if(current !== progress.last){ + var offset = ((ms + 100) % beatInterval) - 100 + var offsets = progress.offsets + if(offsets.length >= progress.requirement){ + offsets.shift() + } + offsets.push(offset) + progress.hit++ + progress.last = current + this.globalScore.gauge = 100 / (progress.requirement / progress.hit) + } + } + calibrationReset(to, togglePause){ + var view = this.controller.view + this.songData.circles = [] + view.pauseOptions = [ + to === "audio" ? strings.calibration.back : strings.calibration.retryPrevious, + strings.calibration.start + ] + this.calibrationState = to + "Help" + var progress = this.calibrationProgress + progress.offsets = [] + progress.hit = 0 + progress.last = null + this.globalScore.gauge = 0 + if(to === "video"){ + this.clearKeyTime() + this.initTiming() + this.latestDate = this.startDate + this.elapsedTime = 0 + view.ms = 0 + } + if(togglePause){ + this.togglePause(true, 1, true) + }else{ + view.pauseMove(1, true) + } + } } diff --git a/public/src/js/gameinput.js b/public/src/js/gameinput.js index 4707652..78f7284 100644 --- a/public/src/js/gameinput.js +++ b/public/src/js/gameinput.js @@ -94,7 +94,7 @@ class GameInput{ } } checkMenuKeys(){ - if(!this.controller.multiplayer && !this.locked){ + if(!this.controller.multiplayer && !this.locked && this.controller.view.pauseOptions.length !== 0){ var moveMenu = 0 var ms = this.game.getAccurateTime() this.gamepadMenu.play((pressed, name) => { @@ -146,7 +146,7 @@ class GameInput{ this.checkKey("don_l", "menu", moveMenuConfirm) this.checkKey("don_r", "menu", moveMenuConfirm) if(moveMenu && this.game.isPaused()){ - assets.sounds["se_ka"].play() + this.controller.playSound("se_ka", 0, true) this.controller.view.pauseMove(moveMenu) } } @@ -197,11 +197,19 @@ class GameInput{ return } this.keyTime[name] = ms + var calibrationState = this.game.calibrationState + var calibration = calibrationState && !this.game.paused if(name == "don_l" || name == "don_r"){ - this.checkKeySound(name, "don") + if(calibration){ + this.game.calibrationHit(ms) + }else{ + this.checkKeySound(name, "don") + } this.keyboardEvents++ }else if(name == "ka_l" || name == "ka_r"){ - this.checkKeySound(name, "ka") + if(!calibration){ + this.checkKeySound(name, "ka") + } this.keyboardEvents++ } }else{ diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index d70b799..df82530 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -35,14 +35,20 @@ class LoadSong{ var song = this.selectedSong var id = song.folder var promises = [] - assets.sounds["v_start"].play() + if(song.folder !== "calibration"){ + assets.sounds["v_start"].play() + var songObj = assets.songs.find(song => song.id === id) + }else{ + var songObj = { + "music": "muted", + "chart": "blank" + } + } song.songBg = this.randInt(1, 5) song.songStage = this.randInt(1, 3) song.donBg = this.randInt(1, 6) - var songObj = assets.songs.find(song => song.id === id) - if(song.songSkin && song.songSkin.name){ var imgLoad = [] for(var type in song.songSkin){ @@ -117,14 +123,18 @@ class LoadSong{ } })) if(songObj.chart){ - var reader = new FileReader() - promises.push(pageEvents.load(reader).then(event => { - this.songData = event.target.result.replace(/\0/g, "").split("\n") - })) - if(song.type === "tja"){ - reader.readAsText(songObj.chart, "sjis") + if(songObj.chart === "blank"){ + this.songData = "" }else{ - reader.readAsText(songObj.chart) + var reader = new FileReader() + promises.push(pageEvents.load(reader).then(event => { + this.songData = event.target.result.replace(/\0/g, "").split("\n") + })) + if(song.type === "tja"){ + reader.readAsText(songObj.chart, "sjis") + }else{ + reader.readAsText(songObj.chart) + } } }else{ promises.push(loader.ajax(this.getSongPath(song)).then(data => { diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index 6979c81..79a20d3 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -122,7 +122,7 @@ return [string.slice(0, index), string.slice(index + delimiter.length)] } parseCircles(){ - var meta = this.metadata[this.difficulty] + var meta = this.metadata[this.difficulty] || {} var ms = (meta.offset || 0) * -1000 + this.offset var bpm = Math.abs(meta.bpm) || 120 var scroll = 1 diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index 24fde5d..a42ecb9 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -10,6 +10,14 @@ class Scoresheet{ this.canvas = document.getElementById("canvas") this.ctx = this.canvas.getContext("2d") + var resolution = settings.getItem("resolution") + var noSmoothing = resolution === "low" || resolution === "lowest" + if(noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + if(resolution === "lowest"){ + this.canvas.style.imageRendering = "pixelated" + } this.game = document.getElementById("game") this.fadeScreen = document.createElement("div") @@ -28,8 +36,8 @@ class Scoresheet{ this.frame = 1000 / 60 this.numbers = "001122334455667788900112233445".split("") - this.draw = new CanvasDraw() - this.canvasCache = new CanvasCache() + this.draw = new CanvasDraw(noSmoothing) + this.canvasCache = new CanvasCache(noSmoothing) this.keyboard = new Keyboard({ confirm: ["enter", "space", "esc", "don_l", "don_r"] @@ -105,7 +113,7 @@ class Scoresheet{ if(!p2.session){ this.state.screen = "scoresShown" this.state.screenMS = this.getMS() - assets.sounds["neiro_1_don"].play() + this.controller.playSound("neiro_1_don", 0, true) } } toSongsel(fromP2){ @@ -114,7 +122,7 @@ class Scoresheet{ this.state.screen = "fadeOut" this.state.screenMS = this.getMS() if(!fromP2){ - assets.sounds["neiro_1_don"].play() + this.controller.playSound("neiro_1_don", 0, true) } } } diff --git a/public/src/js/settings.js b/public/src/js/settings.js index 3c78e6f..b2deaef 100644 --- a/public/src/js/settings.js +++ b/public/src/js/settings.js @@ -2,11 +2,15 @@ class Settings{ constructor(){ var ios = /iPhone|iPad/.test(navigator.userAgent) var phone = /Android|iPhone|iPad/.test(navigator.userAgent) + this.allLanguages = [] + for(var i in allStrings){ + this.allLanguages.push(i) + } this.items = { language: { type: "language", - options: ["ja", "en", "cn", "tw", "ko"], + options: this.allLanguages, default: this.getLang() }, resolution: { @@ -34,6 +38,14 @@ class Settings{ options: ["a", "b", "c"], default: "a", gamepad: true + }, + latency: { + type: "latency", + default: { + "audio": 0, + "video": 0, + "drumSounds": true + } } } @@ -61,6 +73,22 @@ class Settings{ } } this.storage[i] = obj + }else if(current.type === "latency"){ + var obj = {} + for(var j in current.default){ + if(storage[i] && j in storage[i]){ + if(j === "drumSounds"){ + obj[j] = !!storage[i][j] + continue + }else if(!isNaN(storage[i][j])){ + obj[j] = Math.round(parseFloat(storage[i][j]) || 0) + continue + } + } + obj = null + break + } + this.storage[i] = obj }else{ this.storage[i] = storage[i] } @@ -107,7 +135,7 @@ class Settings{ } } } - return "ja" + return this.allLanguages[0] } setLang(lang, noEvent){ strings = lang @@ -122,7 +150,7 @@ class Settings{ } class SettingsView{ - constructor(touchEnabled, tutorial, songId){ + constructor(touchEnabled, tutorial, songId, toSetting){ this.touchEnabled = touchEnabled this.tutorial = tutorial this.songId = songId @@ -130,9 +158,15 @@ class SettingsView{ loader.changePage("settings", tutorial) assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992) this.defaultButton = document.getElementById("settings-default") + this.viewOuter = this.getElement("view-outer") if(touchEnabled){ - this.getElement("view-outer").classList.add("touch-enabled") + this.viewOuter.classList.add("touch-enabled") } + this.touchEnd = [] + pageEvents.add(this.viewOuter, ["mouseup", "touchend"], event => { + this.touchEnd.forEach(func => func(event)) + }) + var gamepadEnabled = false if("getGamepads" in navigator){ var gamepads = navigator.getGamepads() @@ -145,19 +179,22 @@ class SettingsView{ } this.mode = "settings" + this.pressedKeys = {} this.keyboard = new Keyboard({ "confirm": ["enter", "space", "don_l", "don_r"], "up": ["up"], - "previous": ["left", "ka_l"], - "next": ["right", "down", "ka_r"], + "right": ["right", "ka_r"], + "down": ["down"], + "left": ["left", "ka_l"], "back": ["esc"], "other": ["wildcard"] }, this.keyPressed.bind(this)) this.gamepad = new Gamepad({ "confirm": ["b", "ls", "rs"], "up": ["u", "lsu"], - "previous": ["l", "lb", "lt", "lsl"], - "next": ["d", "r", "rb", "rt", "lsd", "lsr"], + "right": ["r", "rb", "rt", "lsr"], + "down": ["d", "lsd"], + "left": ["l", "lb", "lt", "lsl"], "back": ["start", "a"] }, this.keyPressed.bind(this)) @@ -182,15 +219,15 @@ class SettingsView{ var nameDiv = document.createElement("div") nameDiv.classList.add("setting-name", "stroke-sub") var name = strings.settings[i].name - nameDiv.innerText = name - nameDiv.setAttribute("alt", name) + this.setAltText(nameDiv, name) settingBox.appendChild(nameDiv) var valueDiv = document.createElement("div") valueDiv.classList.add("setting-value") this.getValue(i, valueDiv) settingBox.appendChild(valueDiv) content.appendChild(settingBox) - if(this.items.length === this.selected){ + if(!toSetting && this.items.length === this.selected || toSetting === i){ + this.selected = this.items.length settingBox.classList.add("selected") } this.addTouch(settingBox, event => this.setValue(i)) @@ -226,8 +263,99 @@ class SettingsView{ this.gamepadButtons = document.getElementById("gamepad-buttons") this.gamepadValue = document.getElementById("gamepad-value") + this.latencySettings = document.getElementById("settings-latency") + this.addTouch(this.latencySettings, event => { + if(event.target === event.currentTarget){ + this.latencyBack() + } + }) + this.latencyTitle = this.latencySettings.getElementsByClassName("view-title")[0] + this.latencyItems = [] + this.latencySelected = 0 + var latencyContent = this.latencySettings.getElementsByClassName("view-content")[0] + var latencyWindow = ["calibration", "audio", "video", "drumSounds"] + for(let i in latencyWindow){ + let current = latencyWindow[i] + var settingBox = document.createElement("div") + settingBox.classList.add("setting-box") + var nameDiv = document.createElement("div") + nameDiv.classList.add("setting-name", "stroke-sub") + var name = strings.settings.latency[current] + this.setAltText(nameDiv, name) + settingBox.appendChild(nameDiv) + let outputObject = { + id: current, + settingBox: settingBox, + nameDiv: nameDiv + } + if(current === "calibration"){ + nameDiv.style.width = "100%" + }else{ + var valueDiv = document.createElement("div") + valueDiv.classList.add("setting-value") + settingBox.appendChild(valueDiv) + var valueText = document.createTextNode("") + valueDiv.appendChild(valueText) + this.latencyGetValue(current, valueText) + if(current !== "drumSounds"){ + var buttons = document.createElement("div") + buttons.classList.add("latency-buttons") + var buttonMinus = document.createElement("span") + buttonMinus.innerText = "-" + buttons.appendChild(buttonMinus) + this.addTouchRepeat(buttonMinus, event => { + this.latencySetAdjust(outputObject, -1) + }) + var buttonPlus = document.createElement("span") + buttonPlus.innerText = "+" + buttons.appendChild(buttonPlus) + this.addTouchRepeat(buttonPlus, event => { + this.latencySetAdjust(outputObject, 1) + }) + valueDiv.appendChild(buttons) + } + } + latencyContent.appendChild(settingBox) + if(this.latencyItems.length === this.latencySelected){ + settingBox.classList.add("selected") + } + this.addTouch(settingBox, event => { + if(event.target.tagName !== "SPAN"){ + this.latencySetValue(current, event.type === "touchstart") + } + }) + if(current !== "calibration"){ + outputObject.valueDiv = valueDiv + outputObject.valueText = valueText + outputObject.buttonMinus = buttonMinus + outputObject.buttonPlus = buttonPlus + } + this.latencyItems.push(outputObject) + } + this.latencyDefaultButton = document.getElementById("latency-default") + this.latencyItems.push({ + id: "default", + settingBox: this.latencyDefaultButton + }) + this.addTouch(this.latencyDefaultButton, event => this.latencyDefault()) + this.latencyEndButton = this.latencySettings.getElementsByClassName("view-end-button")[0] + this.latencyItems.push({ + id: "back", + settingBox: this.latencyEndButton + }) + this.addTouch(this.latencyEndButton, event => this.latencyBack(true)) + this.setStrings() + this.drumSounds = settings.getItem("latency").drumSounds + this.playedSounds = {} + this.redrawRunning = true + this.redrawBind = this.redraw.bind(this) + this.redraw() + if(toSetting === "latency"){ + this.mode = "latency" + this.latencySet() + } pageEvents.send("settings") } getElement(name){ @@ -246,6 +374,23 @@ class SettingsView{ callback(event) }) } + addTouchRepeat(element, callback){ + this.addTouch(element, event => { + var active = true + var func = () => { + active = false + this.touchEnd.splice(this.touchEnd.indexOf(func), 1) + } + this.touchEnd.push(func) + var repeat = delay => { + if(active){ + callback() + setTimeout(() => repeat(50), delay) + } + } + repeat(400) + }) + } removeTouch(element){ pageEvents.remove(element, ["mousedown", "touchstart"]) } @@ -274,6 +419,17 @@ class SettingsView{ valueDiv.appendChild(keyDiv) } return + }else if(current.type === "latency"){ + var audioVideo = [Math.round(value.audio), Math.round(value.video)] + var latencyValue = strings.settings[name].value.split("%s") + var latencyIndex = 0 + value = "" + latencyValue.forEach((string, i) => { + if(i !== 0){ + value += this.addMs(audioVideo[latencyIndex++]) + } + value += string + }) } valueDiv.innerText = value } @@ -285,6 +441,7 @@ class SettingsView{ if(this.mode !== "settings"){ if(this.selected === selectedIndex){ this.keyboardBack(selected) + this.playSound("se_don") } return } @@ -303,24 +460,37 @@ class SettingsView{ selected.valueDiv.classList.add("selected") this.keyboardKeys = {} this.keyboardSet() - assets.sounds["se_don"].play() + this.playSound("se_don") return }else if(current.type === "gamepad"){ this.mode = "gamepad" this.gamepadSelected = current.options.indexOf(value) this.gamepadSet() - assets.sounds["se_don"].play() + this.playSound("se_don") + return + }else if(current.type === "latency"){ + this.mode = "latency" + this.latencySet() + this.playSound("se_don") return } settings.setItem(name, value) this.getValue(name, this.items[this.selected].valueDiv) - assets.sounds["se_ka"].play() + this.playSound("se_ka") if(current.type === "language"){ this.setLang(allStrings[value]) } } - keyPressed(pressed, name, event){ - if(!pressed){ + keyPressed(pressed, name, event, repeat){ + if(pressed){ + if(!this.pressedKeys[name]){ + this.pressedKeys[name] = this.getMS() + 300 + } + }else{ + this.pressedKeys[name] = 0 + return + } + if(repeat && name !== "up" && name !== "right" && name !== "down" && name !== "left"){ return } this.touched = false @@ -334,31 +504,31 @@ class SettingsView{ }else{ this.setValue(selected.id) } - }else if(name === "up" || name === "previous" || name === "next"){ + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ selected.settingBox.classList.remove("selected") do{ - this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) - }while(this.items[this.selected].id === "default" && name !== "previous") + this.selected = this.mod(this.items.length, this.selected + ((name === "right" || name === "down") ? 1 : -1)) + }while(this.items[this.selected].id === "default" && name !== "left") selected = this.items[this.selected] selected.settingBox.classList.add("selected") selected.settingBox.scrollIntoView() - assets.sounds["se_ka"].play() + this.playSound("se_ka") }else if(name === "back"){ this.onEnd() } }else if(this.mode === "gamepad"){ if(name === "confirm"){ this.gamepadBack(true) - }else if(name === "up" || name === "previous" || name === "next"){ - this.gamepadSet(name === "next" ? 1 : -1) + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + this.gamepadSet((name === "right" || name === "down") ? 1 : -1) }else if(name === "back"){ this.gamepadBack() } }else if(this.mode === "keyboard"){ if(name === "back"){ this.keyboardBack(selected) - assets.sounds["se_cancel"].play() - }else{ + this.playSound("se_cancel") + }else if(event){ event.preventDefault() var currentKey = event.key.toLowerCase() for(var i in this.keyboardKeys){ @@ -367,10 +537,40 @@ class SettingsView{ } } var current = this.keyboardCurrent - assets.sounds[current === "ka_l" || current === "ka_r" ? "se_ka" : "se_don"].play() + this.playSound(current === "ka_l" || current === "ka_r" ? "se_ka" : "se_don") this.keyboardKeys[current] = [currentKey] this.keyboardSet() } + }else if(this.mode === "latency"){ + var latencySelected = this.latencyItems[this.latencySelected] + if(name === "confirm"){ + if(latencySelected.id === "back"){ + this.latencyBack(true) + }else if(latencySelected.id === "default"){ + this.latencyDefault() + }else{ + this.latencySetValue(latencySelected.id) + } + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + latencySelected.settingBox.classList.remove("selected") + do{ + this.latencySelected = this.mod(this.latencyItems.length, this.latencySelected + ((name === "right" || name === "down") ? 1 : -1)) + }while(this.latencyItems[this.latencySelected].id === "default" && name !== "left") + latencySelected = this.latencyItems[this.latencySelected] + latencySelected.settingBox.classList.add("selected") + latencySelected.settingBox.scrollIntoView() + this.playSound("se_ka") + }else if(name === "back"){ + this.latencyBack() + } + }else if(this.mode === "latencySet"){ + var latencySelected = this.latencyItems[this.latencySelected] + if(name === "confirm" || name === "back"){ + this.latencySetBack(latencySelected) + this.playSound(name === "confirm" ? "se_don" : "se_cancel") + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + this.latencySetAdjust(latencySelected, (name === "up" || name === "right") ? 1 : -1) + } } } keyboardSet(){ @@ -416,14 +616,13 @@ class SettingsView{ var current = settings.items[selected.id] if(diff){ this.gamepadSelected = this.mod(current.options.length, this.gamepadSelected + diff) - assets.sounds["se_ka"].play() + this.playSound("se_ka") } var opt = current.options[this.gamepadSelected] var value = strings.settings[selected.id][opt] - this.gamepadValue.innerText = value - this.gamepadValue.setAttribute("alt", value) - this.gamepadButtons.style.backgroundPosition = "0 " + (-318 - 132 * this.gamepadSelected) + "px" - this.gamepadSettings.style.display = "block" + this.setAltText(this.gamepadValue, value) + this.gamepadButtons.style.backgroundPosition = "0 " + (-11.87 - 4.93 * this.gamepadSelected) + "em" + this.gamepadSettings.style.display = "flex" } gamepadBack(confirm){ if(this.mode !== "gamepad"){ @@ -433,10 +632,142 @@ class SettingsView{ var current = settings.items[selected.id] settings.setItem(selected.id, current.options[this.gamepadSelected]) this.getValue(selected.id, selected.valueDiv) - assets.sounds[confirm ? "se_don" : "se_cancel"].play() + this.playSound(confirm ? "se_don" : "se_cancel") this.gamepadSettings.style.display = "" this.mode = "settings" } + latencySet(){ + if(this.mode !== "latency"){ + return + } + var selected = this.items[this.selected] + var current = settings.items[selected.id] + this.latencySettings.style.display = "flex" + } + latencyGetValue(name, valueText){ + var currentLatency = settings.getItem("latency") + if(name === "drumSounds"){ + valueText.data = currentLatency[name] ? strings.settings.on : strings.settings.off + }else{ + valueText.data = this.addMs(currentLatency[name] || 0) + } + } + latencySetValue(name, touched){ + var selectedIndex = this.latencyItems.findIndex(item => item.id === name) + var selected = this.latencyItems[selectedIndex] + if(this.mode === "latencySet"){ + this.latencySetBack(this.latencyItems[this.latencySelected]) + if(this.latencySelected === selectedIndex){ + this.playSound("se_don") + return + } + }else if(this.mode !== "latency"){ + return + } + if(name === "calibration"){ + this.playSound("se_don") + this.clean() + new LoadSong({ + "title": strings.calibration.title, + "folder": "calibration", + "type": "tja", + "songSkin": {} + }, false, false, touched) + }else if(name === "drumSounds"){ + this.drumSounds = !settings.getItem("latency")[name] + this.latencySave(name, this.drumSounds) + this.latencyGetValue(name, selected.valueText) + this.playSound("se_don") + }else{ + var value = Math.round(settings.getItem("latency")[name] || 0) + if(this.latencySelected !== selectedIndex){ + this.latencyItems[this.latencySelected].settingBox.classList.remove("selected") + this.latencySelected = selectedIndex + selected.settingBox.classList.add("selected") + } + this.mode = "latencySet" + selected.settingBox.style.animation = "none" + selected.valueDiv.classList.add("selected") + selected.value = value + this.playSound("se_don") + } + } + latencySetAdjust(selected, add){ + selected.value += add + if(selected.value > 500){ + selected.value = 500 + }else if(selected.value < -200){ + selected.value = -200 + }else{ + this.playSound("se_ka") + } + selected.valueText.data = this.addMs(selected.value) + } + latencySetBack(selected){ + this.mode = "latency" + selected.settingBox.style.animation = "" + selected.valueDiv.classList.remove("selected") + this.latencySave(selected.id, selected.value) + this.latencyGetValue(selected.id, selected.valueText) + } + latencySave(id, value){ + var input = settings.getItem("latency") + var output = {} + for(var i in input){ + if(i === id){ + output[i] = value + }else{ + output[i] = input[i] + } + } + settings.setItem("latency", output) + } + latencyDefault(){ + if(this.mode === "latencySet"){ + this.latencySetBack(this.latencyItems[this.latencySelected]) + }else if(this.mode !== "latency"){ + return + } + settings.setItem("latency", null) + this.latencyItems.forEach(item => { + if(item.id === "audio" || item.id === "video" || item.id === "drumSounds"){ + this.latencyGetValue(item.id, item.valueText) + } + }) + this.drumSounds = settings.getItem("latency").drumSounds + this.playSound("se_don") + } + latencyBack(confirm){ + if(this.mode === "latencySet"){ + this.latencySetBack(this.latencyItems[this.latencySelected]) + if(!confirm){ + this.playSound("se_don") + return + } + } + if(this.mode !== "latency"){ + return + } + var selected = this.items[this.selected] + var current = settings.items[selected.id] + this.getValue(selected.id, selected.valueDiv) + this.playSound(confirm ? "se_don" : "se_cancel") + this.latencySettings.style.display = "" + this.mode = "settings" + } + addMs(input){ + var split = strings.calibration.ms.split("%s") + var index = 0 + var output = "" + var inputStrings = [(input > 0 ? "+" : "") + input.toString()] + split.forEach((string, i) => { + if(i !== 0){ + output += inputStrings[index++] + } + output += string + }) + return output + } defaultSettings(){ if(this.mode === "keyboard"){ this.keyboardBack(this.items[this.selected]) @@ -447,11 +778,17 @@ class SettingsView{ this.setLang(allStrings[settings.getItem("language")]) this.keyboard.update() pageEvents.setKbd() - assets.sounds["se_don"].play() + this.latencyItems.forEach(item => { + if(item.id === "audio" || item.id === "video" || item.id === "drumSounds"){ + this.latencyGetValue(item.id, item.valueText) + } + }) + this.drumSounds = settings.getItem("latency").drumSounds + this.playSound("se_don") } onEnd(){ this.clean() - assets.sounds["se_don"].play() + this.playSound("se_don") setTimeout(() => { if(this.tutorial && !this.touched){ new Tutorial(false, this.songId) @@ -472,41 +809,94 @@ class SettingsView{ var item = this.items[i] if(item.valueDiv){ var name = strings.settings[item.id].name - item.nameDiv.innerText = name - item.nameDiv.setAttribute("alt", name) + this.setAltText(item.nameDiv, name) this.getValue(item.id, item.valueDiv) } } + for(var i in this.latencyItems){ + var current = this.latencyItems[i] + if(current.nameDiv){ + this.setAltText(current.nameDiv, strings.settings.latency[current.id]) + } + if(current.valueText){ + this.latencyGetValue(current.id, current.valueText) + } + } this.setStrings() } setStrings(){ - this.viewTitle.innerText = strings.gameSettings - this.viewTitle.setAttribute("alt", strings.gameSettings) - this.endButton.innerText = strings.settings.ok - this.endButton.setAttribute("alt", strings.settings.ok) - this.gamepadTitle.innerText = strings.settings.gamepadLayout.name - this.gamepadTitle.setAttribute("alt", strings.settings.gamepadLayout.name) - this.gamepadEndButton.innerText = strings.settings.ok - this.gamepadEndButton.setAttribute("alt", strings.settings.ok) - this.defaultButton.innerText = strings.settings.default - this.defaultButton.setAttribute("alt", strings.settings.default) + this.setAltText(this.viewTitle, strings.gameSettings) + this.setAltText(this.endButton, strings.settings.ok) + this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name) + this.setAltText(this.gamepadEndButton, strings.settings.ok) + this.setAltText(this.latencyTitle, strings.settings.latency.name) + this.setAltText(this.latencyDefaultButton, strings.settings.default) + this.setAltText(this.latencyEndButton, strings.settings.ok) + this.setAltText(this.defaultButton, strings.settings.default) + } + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) } mod(length, index){ return ((index % length) + length) % length } + playSound(id, time){ + if(!this.drumSounds && (id === "se_don" || id === "se_ka" || id === "se_cancel")){ + return + } + var ms = Date.now() + (time || 0) * 1000 + if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ + assets.sounds[id].play(time) + this.playedSounds[id] = ms + } + } + redraw(){ + if(!this.redrawRunning){ + return + } + requestAnimationFrame(this.redrawBind) + var ms = this.getMS() + + for(var key in this.pressedKeys){ + if(this.pressedKeys[key]){ + if(ms >= this.pressedKeys[key] + 50){ + this.keyPressed(true, key, null, true) + this.pressedKeys[key] = ms + } + } + } + } + getMS(){ + return Date.now() + } clean(){ + this.redrawRunning = false this.keyboard.clean() this.gamepad.clean() assets.sounds["bgm_settings"].stop() + pageEvents.remove(this.viewOuter, ["mouseup", "touchend"]) for(var i in this.items){ this.removeTouch(this.items[i].settingBox) } + for(var i in this.latencyItems){ + this.removeTouch(this.latencyItems[i].settingBox) + if(this.latencyItems[i].buttonMinus){ + this.removeTouch(this.latencyItems[i].buttonMinus) + this.removeTouch(this.latencyItems[i].buttonPlus) + } + } if(this.defaultButton){ delete this.defaultButton } this.removeTouch(this.gamepadSettings) this.removeTouch(this.gamepadEndButton) this.removeTouch(this.gamepadBox) + this.removeTouch(this.latencySettings) + this.removeTouch(this.latencyDefaultButton) + this.removeTouch(this.latencyEndButton) + delete this.viewOuter + delete this.touchEnd delete this.tutorialTitle delete this.endButton delete this.items @@ -516,6 +906,11 @@ class SettingsView{ delete this.gamepadBox delete this.gamepadButtons delete this.gamepadValue + delete this.latencyItems + delete this.latencySettings + delete this.latencyTitle + delete this.latencyDefaultButton + delete this.latencyEndButton if(this.resolution !== settings.getItem("resolution")){ for(var i in assets.image){ if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_")){ diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 4a3d8b6..80c7671 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -5,6 +5,14 @@ class SongSelect{ loader.changePage("songselect", false) this.canvas = document.getElementById("song-sel-canvas") this.ctx = this.canvas.getContext("2d") + var resolution = settings.getItem("resolution") + var noSmoothing = resolution === "low" || resolution === "lowest" + if(noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + if(resolution === "lowest"){ + this.canvas.style.imageRendering = "pixelated" + } this.songSkin = { "selected": { @@ -207,13 +215,13 @@ class SongSelect{ }] this.optionsList = [strings.none, strings.auto, strings.netplay] - this.draw = new CanvasDraw() - this.songTitleCache = new CanvasCache() - this.selectTextCache = new CanvasCache() - this.categoryCache = new CanvasCache() - this.difficultyCache = new CanvasCache() - this.sessionCache = new CanvasCache() - this.currentSongCache = new CanvasCache() + this.draw = new CanvasDraw(noSmoothing) + this.songTitleCache = new CanvasCache(noSmoothing) + this.selectTextCache = new CanvasCache(noSmoothing) + this.categoryCache = new CanvasCache(noSmoothing) + this.difficultyCache = new CanvasCache(noSmoothing) + this.sessionCache = new CanvasCache(noSmoothing) + this.currentSongCache = new CanvasCache(noSmoothing) this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] @@ -234,6 +242,9 @@ class SongSelect{ fromTutorial = false } + this.drumSounds = settings.getItem("latency").drumSounds + this.playedSounds = {} + var songIdIndex = -1 if(fromTutorial){ this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial) @@ -252,7 +263,7 @@ class SongSelect{ }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) } - assets.sounds[songIdIndex !== -1 ? "v_diffsel" : "v_songsel"].play() + this.playSound(songIdIndex !== -1 ? "v_diffsel" : "v_songsel") snd.musicGain.fadeOut() this.playBgm(false) } @@ -436,7 +447,7 @@ class SongSelect{ window.open(this.songs[this.selectedSong].maker.url) }else if(moveBy === this.diffOptions.length + 4){ this.state.ura = !this.state.ura - assets.sounds["se_ka"].play() + this.playSound("se_ka") if(this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura){ this.state.move = -1 } @@ -564,7 +575,7 @@ class SongSelect{ var soundsDelay = Math.abs((scroll + resize) / moveBy) for(var i = 0; i < Math.abs(moveBy) - 1; i++){ - assets.sounds["se_ka"].play((resize + i * soundsDelay) / 1000) + this.playSound("se_ka", (resize + i * soundsDelay) / 1000) } this.pointer(false) } @@ -574,7 +585,7 @@ class SongSelect{ this.state.move = moveBy this.state.moveMS = this.getMS() - 500 this.state.locked = 1 - assets.sounds["se_ka"].play() + this.playSound("se_ka") } } @@ -605,15 +616,15 @@ class SongSelect{ this.selectedDiff = this.diffOptions.length + 3 } - assets.sounds["se_don"].play() + this.playSound("se_don") assets.sounds["v_songsel"].stop() - assets.sounds["v_diffsel"].play(0.3) + this.playSound("v_diffsel", 0.3) pageEvents.send("song-select-difficulty", currentSong) }else if(currentSong.action === "back"){ this.clean() this.toTitleScreen() }else if(currentSong.action === "random"){ - assets.sounds["se_don"].play() + this.playSound("se_don") this.state.locked = true do{ var i = Math.floor(Math.random() * this.songs.length) @@ -650,7 +661,7 @@ class SongSelect{ this.state.moveHover = null assets.sounds["v_diffsel"].stop() - assets.sounds["se_cancel"].play() + this.playSound("se_cancel") } this.clearHash() pageEvents.send("song-select-back") @@ -659,7 +670,7 @@ class SongSelect{ this.clean() var selectedSong = this.songs[this.selectedSong] assets.sounds["v_diffsel"].stop() - assets.sounds["se_don"].play() + this.playSound("se_don") try{ if(assets.customSongs){ @@ -698,7 +709,7 @@ class SongSelect{ } toOptions(moveBy){ if(!p2.session){ - assets.sounds["se_ka"].play() + this.playSound("se_ka") this.selectedDiff = 1 do{ this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) @@ -707,7 +718,7 @@ class SongSelect{ } toTitleScreen(){ if(!p2.session){ - assets.sounds["se_cancel"].play() + this.playSound("se_cancel") this.clean() setTimeout(() => { new Titlescreen() @@ -715,21 +726,21 @@ class SongSelect{ } } toTutorial(){ - assets.sounds["se_don"].play() + this.playSound("se_don") this.clean() setTimeout(() => { new Tutorial(true) }, 500) } toAbout(){ - assets.sounds["se_don"].play() + this.playSound("se_don") this.clean() setTimeout(() => { new About(this.touchEnabled) }, 500) } toSettings(){ - assets.sounds["se_don"].play() + this.playSound("se_don") this.clean() setTimeout(() => { new SettingsView(this.touchEnabled) @@ -744,7 +755,7 @@ class SongSelect{ }else{ localStorage["selectedSong"] = this.selectedSong - assets.sounds["se_don"].play() + this.playSound("se_don") this.clean() setTimeout(() => { new Session(this.touchEnabled) @@ -755,7 +766,7 @@ class SongSelect{ if(assets.customSongs){ assets.customSongs = false assets.songs = assets.songsDefault - assets.sounds["se_don"].play() + this.playSound("se_don") this.clean() setTimeout(() => { new SongSelect("browse", false, this.touchEnabled) @@ -984,7 +995,7 @@ class SongSelect{ var scroll = resize2 - resize - scrollDelay * 2 var elapsed = ms - this.state.moveMS if(this.state.move && ms > this.state.moveMS + resize2 - scrollDelay){ - assets.sounds["se_ka"].play() + this.playSound("se_ka") var previousSelectedSong = this.selectedSong this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move) if(previousSelectedSong !== this.selectedSong){ @@ -2041,6 +2052,17 @@ class SongSelect{ } } + playSound(id, time){ + if(!this.drumSounds && (id === "se_don" || id === "se_ka" || id === "se_cancel")){ + return + } + var ms = Date.now() + (time || 0) * 1000 + if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ + assets.sounds[id].play(time) + this.playedSounds[id] = ms + } + } + getMS(){ return Date.now() } diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 5bed612..b063ed3 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -126,11 +126,42 @@ b: "タイプB", c: "タイプC" }, + latency: { + name: "Latency", + value: "Audio: %s, Video: %s", + calibration: "Latency Calibration", + audio: "Audio", + video: "Video", + drumSounds: "Drum Sounds" + }, on: "オン", off: "オフ", default: "既定値にリセット", ok: "OK" } + this.calibration = { + title: "Latency Calibration", + ms: "%sms", + back: "Back to Settings", + retryPrevious: "Retry Previous", + start: "Start", + finish: "Finish", + audioHelp: { + title: "Audio Latency Calibration", + content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" + }, + audioComplete: "Audio Latency Calibration completed!", + videoHelp: { + title: "Video Latency Calibration", + content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" + }, + videoComplete: "Video Latency Calibration completed!", + results: { + title: "Latency Calibration Results", + content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." + } + } this.browserSupport = { browserWarning: "サポートされていないブラウザを実行しています (%s)", details: "詳しく", @@ -270,11 +301,42 @@ function StringsEn(){ b: "Type B", c: "Type C" }, + latency: { + name: "Latency", + value: "Audio: %s, Video: %s", + calibration: "Latency Calibration", + audio: "Audio", + video: "Video", + drumSounds: "Drum Sounds" + }, on: "On", off: "Off", default: "Reset to Defaults", ok: "OK" } + this.calibration = { + title: "Latency Calibration", + ms: "%sms", + back: "Back to Settings", + retryPrevious: "Retry Previous", + start: "Start", + finish: "Finish", + audioHelp: { + title: "Audio Latency Calibration", + content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" + }, + audioComplete: "Audio Latency Calibration completed!", + videoHelp: { + title: "Video Latency Calibration", + content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" + }, + videoComplete: "Video Latency Calibration completed!", + results: { + title: "Latency Calibration Results", + content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." + } + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -414,11 +476,42 @@ function StringsCn(){ b: "类型B", c: "类型C" }, + latency: { + name: "Latency", + value: "Audio: %s, Video: %s", + calibration: "Latency Calibration", + audio: "Audio", + video: "Video", + drumSounds: "Drum Sounds" + }, on: "开", off: "关", default: "重置为默认值", ok: "确定" } + this.calibration = { + title: "Latency Calibration", + ms: "%sms", + back: "Back to Settings", + retryPrevious: "Retry Previous", + start: "Start", + finish: "Finish", + audioHelp: { + title: "Audio Latency Calibration", + content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" + }, + audioComplete: "Audio Latency Calibration completed!", + videoHelp: { + title: "Video Latency Calibration", + content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" + }, + videoComplete: "Video Latency Calibration completed!", + results: { + title: "Latency Calibration Results", + content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." + } + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -558,11 +651,42 @@ function StringsTw(){ b: "類型B", c: "類型C" }, + latency: { + name: "Latency", + value: "Audio: %s, Video: %s", + calibration: "Latency Calibration", + audio: "Audio", + video: "Video", + drumSounds: "Drum Sounds" + }, on: "開", off: "關", default: "重置為默認值", ok: "確定" } + this.calibration = { + title: "Latency Calibration", + ms: "%sms", + back: "Back to Settings", + retryPrevious: "Retry Previous", + start: "Start", + finish: "Finish", + audioHelp: { + title: "Audio Latency Calibration", + content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" + }, + audioComplete: "Audio Latency Calibration completed!", + videoHelp: { + title: "Video Latency Calibration", + content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" + }, + videoComplete: "Video Latency Calibration completed!", + results: { + title: "Latency Calibration Results", + content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." + } + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -702,11 +826,42 @@ function StringsKo(){ b: "타입 B", c: "타입 C" }, + latency: { + name: "Latency", + value: "Audio: %s, Video: %s", + calibration: "Latency Calibration", + audio: "Audio", + video: "Video", + drumSounds: "Drum Sounds" + }, on: "온", off: "오프", default: "기본값으로 재설정", ok: "확인" } + this.calibration = { + title: "Latency Calibration", + ms: "%sms", + back: "Back to Settings", + retryPrevious: "Retry Previous", + start: "Start", + finish: "Finish", + audioHelp: { + title: "Audio Latency Calibration", + content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" + }, + audioComplete: "Audio Latency Calibration completed!", + videoHelp: { + title: "Video Latency Calibration", + content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" + }, + videoComplete: "Video Latency Calibration completed!", + results: { + title: "Latency Calibration Results", + content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." + } + } 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 06c30c7..332cac8 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -4,8 +4,15 @@ this.canvas = document.getElementById("canvas") this.ctx = this.canvas.getContext("2d") + var resolution = settings.getItem("resolution") + var noSmoothing = resolution === "low" || resolution === "lowest" + if(noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + if(resolution === "lowest"){ + this.canvas.style.imageRendering = "pixelated" + } - this.cursor = document.getElementById("cursor") this.gameDiv = document.getElementById("game") this.songBg = document.getElementById("songbg") this.songStage = document.getElementById("song-stage") @@ -73,6 +80,7 @@ } this.nextBeat = 0 this.gogoTime = 0 + this.gogoTimeStarted = -Infinity this.drumroll = [] this.touchEvents = 0 if(this.controller.parsedSongData.branches){ @@ -103,16 +111,20 @@ } } - this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval + if(this.controller.calibrationMode){ + this.beatInterval = 512 + }else{ + this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval + } this.font = strings.font - this.draw = new CanvasDraw() + this.draw = new CanvasDraw(noSmoothing) this.assets = new ViewAssets(this) - this.titleCache = new CanvasCache() - this.comboCache = new CanvasCache() - this.pauseCache = new CanvasCache() - this.branchCache = new CanvasCache() + this.titleCache = new CanvasCache(noSmoothing) + this.comboCache = new CanvasCache(noSmoothing) + this.pauseCache = new CanvasCache(noSmoothing) + this.branchCache = new CanvasCache(noSmoothing) this.multiplayer = this.controller.multiplayer @@ -120,6 +132,9 @@ this.touch = -Infinity this.touchAnimation = settings.getItem("touchAnimation") + versionDiv.classList.add("version-hide") + loader.screen.parentNode.insertBefore(versionDiv, loader.screen) + if(this.multiplayer !== 2){ if(this.controller.touchEnabled){ @@ -134,7 +149,6 @@ pageEvents.add(this.canvas, "touchstart", this.ontouch.bind(this)) this.gameDiv.classList.add("touch-visible") - document.getElementById("version").classList.add("version-hide") this.touchFullBtn = document.getElementById("touch-full-btn") pageEvents.add(this.touchFullBtn, "touchend", toggleFullscreen) @@ -444,12 +458,14 @@ ctx.fill() // Difficulty - ctx.drawImage(assets.image["difficulty"], - 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], - 168, 143, - 126, this.multiplayer === 2 ? 497 : 228, - 62, 53 - ) + if(this.controller.selectedSong.difficulty){ + ctx.drawImage(assets.image["difficulty"], + 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], + 168, 143, + 126, this.multiplayer === 2 ? 497 : 228, + 62, 53 + ) + } // Badges if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ @@ -593,24 +609,26 @@ ctx.globalAlpha = 1 // Difficulty - ctx.drawImage(assets.image["difficulty"], - 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], - 168, 143, - 16, this.multiplayer === 2 ? 194 : 232, - 141, 120 - ) - var diff = this.controller.selectedSong.difficulty - var text = strings[diff === "ura" ? "oni" : diff] - ctx.font = this.draw.bold(this.font) + "20px " + this.font - ctx.textAlign = "center" - ctx.textBaseline = "bottom" - ctx.strokeStyle = "#000" - 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.miterLimit = 10 + if(this.controller.selectedSong.difficulty){ + ctx.drawImage(assets.image["difficulty"], + 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], + 168, 143, + 16, this.multiplayer === 2 ? 194 : 232, + 141, 120 + ) + var diff = this.controller.selectedSong.difficulty + var text = strings[diff === "ura" ? "oni" : diff] + ctx.font = this.draw.bold(this.font) + "20px " + this.font + ctx.textAlign = "center" + ctx.textBaseline = "bottom" + ctx.strokeStyle = "#000" + 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.miterLimit = 10 + } // Badges if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ @@ -947,6 +965,20 @@ ctx.clip() this.drawCircles(this.controller.getCircles()) + if(this.controller.game.calibrationState === "video"){ + if(ms % this.beatInterval < 1000 / 60 * 5){ + this.drawCircle({ + ms: ms, + type: "don", + endTime: ms + 100, + speed: 0 + }, { + x: this.slotPos.x, + y: this.slotPos.y + }) + } + } + ctx.restore() // Hit notes explosion @@ -1001,6 +1033,22 @@ ctx.translate(frameLeft, frameTop) } + var state = this.controller.game.calibrationState + if(state && state in strings.calibration){ + var boldTitle = strings.calibration[state].title + } + if(boldTitle){ + this.draw.layeredText({ + ctx: ctx, + text: boldTitle, + fontSize: 35, + fontFamily: this.font, + x: 300, + y: 70 + }, [ + {outline: "#fff", letterBorder: 22} + ]) + } var pauseRect = (ctx, mul) => { this.draw.roundedRect({ ctx: ctx, @@ -1025,86 +1073,284 @@ dx: 68, dy: 11 }) - - ctx.drawImage(assets.image["mimizu"], - 313, 247, 136, 315 - ) - - var _y = 108 - var _w = 80 - var _h = 464 - for(var i = 0; i < this.pauseOptions.length; i++){ - var _x = 520 + 110 * i - if(this.state.moveHover !== null){ - var selected = i === this.state.moveHover - }else{ - var selected = i === this.state.pausePos - } - if(selected){ - ctx.fillStyle = "#ffb447" - this.draw.roundedRect({ - ctx: ctx, - x: _x - _w / 2, - y: _y, - w: _w, - h: _h, - radius: 30 - }) - ctx.fill() - } - this.pauseCache.get({ + if(boldTitle){ + this.draw.layeredText({ ctx: ctx, - x: _x - _w / 2, - y: _y, - w: _w, - h: _h, - id: this.pauseOptions[i] + (selected ? "1" : "0") - }, ctx => { - var textConfig = { + text: boldTitle, + fontSize: 35, + fontFamily: this.font, + x: 300, + y: 70 + }, [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ]) + } + + switch(state){ + case "audioHelp": + case "videoHelp": + case "results": + var content = state === "audioHelp" && this.touchEnabled ? "contentAlt" : "content" + if(state === "audioHelp"){ + var kbdSettings = settings.getItem("keyboardSettings") + var keys = [ + kbdSettings.don_l[0].toUpperCase(), + kbdSettings.don_r[0].toUpperCase() + ] + var substitute = (config, index, width) => { + var ctx = config.ctx + var bold = this.draw.bold(config.fontFamily) + ctx.font = bold + (config.fontSize * 0.66) + "px " + config.fontFamily + var w = config.fontSize * 0.6 + ctx.measureText(keys[index]).width + if(width){ + return w + }else{ + var h = 30 + ctx.lineWidth = 3 + ctx.strokeStyle = "rgba(0, 0, 0, 0.2)" + this.draw.roundedRect({ + ctx: ctx, + x: 0, y: 1, w: w, h: h, + radius: 3 + }) + ctx.stroke() + ctx.strokeStyle = "#ccc" + ctx.fillStyle = "#fff" + this.draw.roundedRect({ + ctx: ctx, + x: 0, y: 0, w: w, h: h, + radius: 3 + }) + ctx.stroke() + ctx.fill() + ctx.fillStyle = "#f7f7f7" + ctx.fillRect(2, 2, w - 4, h - 4) + + ctx.fillStyle = "#333" + ctx.textBaseline = "middle" + ctx.textAlign = "center" + ctx.fillText(keys[index], w / 2, h / 2) + } + } + }else if(state === "results"){ + var progress = this.controller.game.calibrationProgress + var latency = [ + progress.audio, + progress.video + ] + var substitute = (config, index, width) => { + var ctx = config.ctx + var bold = this.draw.bold(config.fontFamily) + ctx.font = bold + (config.fontSize * 1.1) + "px " + config.fontFamily + var text = this.addMs(latency[index]) + if(width){ + return ctx.measureText(text).width + }else{ + ctx.fillText(text, 0, 0) + } + } + }else{ + var substitute = null + } + this.draw.wrappingText({ ctx: ctx, - text: this.pauseOptions[i], - x: _w / 2, - y: 18, - width: _w, - height: _h - 54, + text: strings.calibration[state][content], + fontSize: 30, + fontFamily: this.font, + x: 300, + y: 130, + width: 680, + height: 240, + lineHeight: 35, + fill: "#000", + verticalAlign: "middle", + substitute: substitute + }) + + var _x = 640 + var _w = 464 + var _h = 80 + for(var i = 0; i < this.pauseOptions.length; i++){ + var text = this.pauseOptions[i] + var _y = 470 - 90 * (this.pauseOptions.length - i - 1) + if(this.state.moveHover !== null){ + var selected = i === this.state.moveHover + }else{ + var selected = i === this.state.pausePos + } + if(selected){ + ctx.fillStyle = "#ffb447" + this.draw.roundedRect({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + radius: 30 + }) + ctx.fill() + } + if(selected){ + var layers = [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ] + }else{ + var layers = [ + {fill: "#000"} + ] + } + this.draw.layeredText({ + ctx: ctx, + text: text, + x: _x, + y: _y + 18, + width: _w, + height: _h - 54, + fontSize: 40, + fontFamily: this.font, + letterSpacing: -1, + align: "center" + }, layers) + + var highlight = 0 + if(this.state.moveHover === i){ + highlight = 2 + }else if(selected){ + highlight = 1 + } + 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 + }) + } + } + break + case "audioComplete": + case "videoComplete": + this.draw.wrappingText({ + ctx: ctx, + text: strings.calibration[state], fontSize: 40, fontFamily: this.font, - letterSpacing: -1 - } - if(selected){ - textConfig.fill = "#fff" - textConfig.outline = "#000" - textConfig.outlineSize = 10 - }else{ - textConfig.fill = "#000" - } - this.draw.verticalText(textConfig) - }) - - var highlight = 0 - if(this.state.moveHover === i){ - highlight = 2 - }else if(selected){ - highlight = 1 - } - 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 + x: 300, + y: 130, + width: 680, + height: 420, + lineHeight: 47, + fill: "#000", + verticalAlign: "middle", + textAlign: "center", }) - } + break + default: + ctx.drawImage(assets.image["mimizu"], + 313, 247, 136, 315 + ) + + var _y = 108 + var _w = 80 + var _h = 464 + for(var i = 0; i < this.pauseOptions.length; i++){ + var text = this.pauseOptions[i] + if(this.controller.calibrationMode && i === this.pauseOptions.length - 1){ + text = strings.calibration.back + } + var _x = 520 + 110 * i + if(this.state.moveHover !== null){ + var selected = i === this.state.moveHover + }else{ + var selected = i === this.state.pausePos + } + if(selected){ + ctx.fillStyle = "#ffb447" + this.draw.roundedRect({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + radius: 30 + }) + ctx.fill() + } + this.pauseCache.get({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + id: text + (selected ? "1" : "0") + }, ctx => { + var textConfig = { + ctx: ctx, + text: text, + x: _w / 2, + y: 18, + width: _w, + height: _h - 54, + fontSize: 40, + fontFamily: this.font, + letterSpacing: -1 + } + if(selected){ + textConfig.fill = "#fff" + textConfig.outline = "#000" + textConfig.outlineSize = 10 + }else{ + textConfig.fill = "#000" + } + this.draw.verticalText(textConfig) + }) + + var highlight = 0 + if(this.state.moveHover === i){ + highlight = 2 + }else if(selected){ + highlight = 1 + } + 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 + }) + } + } + break } ctx.restore() } } + addMs(input){ + var split = strings.calibration.ms.split("%s") + var index = 0 + var output = "" + var inputStrings = [(input > 0 ? "+" : "") + input.toString()] + split.forEach((string, i) => { + if(i !== 0){ + output += inputStrings[index++] + } + output += string + }) + return output + } setBackground(){ var selectedSong = this.controller.selectedSong var songSkinName = selectedSong.songSkin.name @@ -1219,10 +1465,10 @@ measures.forEach(measure => { var timeForDistance = this.posToMs(distanceForCircle, measure.speed) - var startingTime = measure.ms - timeForDistance - var finishTime = measure.ms + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + 3, measure.speed) + var startingTime = measure.ms - timeForDistance + this.controller.videoLatency + var finishTime = measure.ms + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + 3, measure.speed) + this.controller.videoLatency if(measure.visible && (!measure.branch || measure.branch.active) && ms >= startingTime && ms <= finishTime){ - var measureX = this.slotPos.x + this.msToPos(measure.ms - ms, measure.speed) + var measureX = this.slotPos.x + this.msToPos(measure.ms - ms + this.controller.videoLatency, measure.speed) this.ctx.strokeStyle = measure.branchFirst ? "#ff0" : "#bdbdbd" this.ctx.lineWidth = 3 this.ctx.beginPath() @@ -1267,8 +1513,8 @@ var speed = circle.speed var timeForDistance = this.posToMs(distanceForCircle + this.slotPos.size / 2, speed) - var startingTime = circle.ms - timeForDistance - var finishTime = circle.endTime + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + this.slotPos.size * 2, speed) + var startingTime = circle.ms - timeForDistance + this.controller.videoLatency + var finishTime = circle.endTime + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + this.slotPos.size * 2, speed) + this.controller.videoLatency if(circle.isPlayed <= 0 || circle.score === 0){ if((!circle.branch || circle.branch.active) && ms >= startingTime && ms <= finishTime && circle.isPlayed !== -1){ @@ -1350,7 +1596,7 @@ if(!circlePos){ circlePos = { - x: this.slotPos.x + this.msToPos(circleMs - ms, speed), + x: this.slotPos.x + this.msToPos(circleMs - ms + this.controller.videoLatency, speed), y: this.slotPos.y } } @@ -1388,10 +1634,10 @@ size = circleSize faceID = noteFace.small var h = size * 1.8 - if(circleMs < ms && ms <= endTime){ + if(circleMs + this.controller.audioLatency < ms && ms <= endTime + this.controller.audioLatency){ circlePos.x = this.slotPos.x - }else if(ms > endTime){ - circlePos.x = this.slotPos.x + this.msToPos(endTime - ms, speed) + }else if(ms > endTime + this.controller.audioLatency){ + circlePos.x = this.slotPos.x + this.msToPos(endTime - ms + this.controller.audioLatency, speed) } ctx.drawImage(assets.image["balloon"], circlePos.x + size - 4, @@ -1596,7 +1842,9 @@ } toggleGogoTime(circle){ this.gogoTime = circle.gogoTime - this.gogoTimeStarted = circle.ms + if(circle.gogoTime || this.gogoTimeStarted !== -Infinity){ + this.gogoTimeStarted = circle.ms + } if(this.gogoTime){ this.assets.fireworks.forEach(fireworksAsset => { @@ -1789,21 +2037,62 @@ if(typeof pos === "undefined"){ pos = this.state.pausePos } + var game = this.controller.game + var state = game.calibrationState + switch(state){ + case "audioHelp": + pos = pos === 0 ? 2 : 0 + break + case "videoHelp": + if(pos === 0){ + assets.sounds["se_don"].play() + game.calibrationReset("audio") + return + }else{ + pos = 0 + } + break + case "results": + if(pos === 0){ + assets.sounds["se_don"].play() + game.calibrationReset("video") + return + }else{ + var input = settings.getItem("latency") + var output = {} + var progress = game.calibrationProgress + for(var i in input){ + if(i === "audio" || i === "video"){ + output[i] = progress[i] + }else{ + output[i] = input[i] + } + } + settings.setItem("latency", output) + pos = 2 + } + break + } switch(pos){ case 1: - assets.sounds["se_don"].play() - this.controller.restartSong() + this.controller.playSound("se_don", 0, true) + if(state === "video"){ + game.calibrationReset(state) + }else{ + this.controller.restartSong() + } pageEvents.send("pause-restart") break case 2: - assets.sounds["se_don"].play() + this.controller.playSound("se_don", 0, true) this.controller.songSelection() pageEvents.send("pause-song-select") break default: - this.controller.togglePause() + this.controller.togglePause(false) break } + return true } onmousedown(event){ if(this.controller.game.paused){ @@ -1855,23 +2144,30 @@ x = x * pauseScale + 257 y = y * pauseScale - 328 } - if(104 <= y && y <= 575 && 465 <= x && x <= 465 + 110 * this.pauseOptions.length){ - return Math.floor((x - 465) / 110) + switch(this.controller.game.calibrationState){ + case "audioHelp": + case "videoHelp": + case "results": + if(554 - 90 * this.pauseOptions.length <= y && y <= 554 && 404 <= x && x <= 876){ + return Math.floor((y - 554 + 90 * this.pauseOptions.length) / 90) + } + break + default: + if(104 <= y && y <= 575 && 465 <= x && x <= 465 + 110 * this.pauseOptions.length){ + return Math.floor((x - 465) / 110) + } + break } return null } mouseIdle(){ var lastMouse = pageEvents.getMouse() - if(lastMouse && !this.cursorHidden){ + if(lastMouse && !this.cursorHidden && !this.state.hasPointer){ if(this.getMS() >= this.lastMousemove + 2000){ - this.cursor.style.top = lastMouse.clientY + "px" - this.cursor.style.left = lastMouse.clientX + "px" - this.cursor.style.pointerEvents = "auto" + this.canvas.style.cursor = "none" this.cursorHidden = true }else{ - this.cursor.style.top = "" - this.cursor.style.left = "" - this.cursor.style.pointerEvents = "" + this.canvas.style.cursor = "" } } } @@ -1890,12 +2186,13 @@ this.pauseCache.clean() this.branchCache.clean() + versionDiv.classList.remove("version-hide") + loader.screen.parentNode.appendChild(versionDiv) if(this.multiplayer !== 2){ if(this.touchEnabled){ pageEvents.remove(this.canvas, "touchstart") pageEvents.remove(this.touchPauseBtn, "touchend") this.gameDiv.classList.add("touch-results") - document.getElementById("version").classList.remove("version-hide") this.touchDrumDiv.parentNode.removeChild(this.touchDrumDiv) delete this.touchDrumDiv delete this.touchDrumImg @@ -1915,7 +2212,6 @@ pageEvents.mouseRemove(this) delete this.pauseMenu - delete this.cursor delete this.gameDiv delete this.canvas delete this.ctx diff --git a/public/src/views/about.html b/public/src/views/about.html index 3506eb3..37e0c74 100644 --- a/public/src/views/about.html +++ b/public/src/views/about.html @@ -7,8 +7,8 @@ -
diff --git a/public/src/views/game.html b/public/src/views/game.html index b9ba0ae..c9e3837 100644 --- a/public/src/views/game.html +++ b/public/src/views/game.html @@ -11,5 +11,4 @@
-
diff --git a/public/src/views/settings.html b/public/src/views/settings.html index fbac462..c1f1a97 100644 --- a/public/src/views/settings.html +++ b/public/src/views/settings.html @@ -20,5 +20,15 @@
+
+
+
+
+
+
+
+
+
+