diff --git a/public/assets/audio/calibration.wav b/public/assets/audio/calibration.wav new file mode 100644 index 0000000..f1459e1 Binary files /dev/null and b/public/assets/audio/calibration.wav differ 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 @@
+
+
+
+
+
+
+
+
+
+