diff --git a/app.py b/app.py index 5eb6969..7161c58 100644 --- a/app.py +++ b/app.py @@ -40,10 +40,10 @@ def get_config(): try: config = json.load(open('config.json', 'r')) except ValueError: - print 'WARNING: Invalid config.json, using default values' + print('WARNING: Invalid config.json, using default values') config = {} else: - print 'WARNING: No config.json found, using default values' + print('WARNING: No config.json found, using default values') config = {} if not config.get('songs_baseurl'): @@ -55,77 +55,6 @@ def get_config(): return config -def parse_osu(osu): - osu_lines = open(osu, 'r').read().replace('\x00', '').split('\n') - sections = {} - current_section = (None, []) - - for line in osu_lines: - line = line.strip() - secm = re.match('^\[(\w+)\]$', line) - if secm: - if current_section: - sections[current_section[0]] = current_section[1] - current_section = (secm.group(1), []) - else: - if current_section: - current_section[1].append(line) - else: - current_section = ('Default', [line]) - - if current_section: - sections[current_section[0]] = current_section[1] - - return sections - - -def get_osu_key(osu, section, key, default=None): - sec = osu[section] - for line in sec: - ok = line.split(':', 1)[0].strip() - ov = line.split(':', 1)[1].strip() - - if ok.lower() == key.lower(): - return ov - - return default - - -def get_preview(song_id, song_type): - preview = 0 - - if song_type == "tja": - if os.path.isfile('public/songs/%s/main.tja' % song_id): - preview = get_tja_preview('public/songs/%s/main.tja' % song_id) - else: - osus = [osu for osu in os.listdir('public/songs/%s' % song_id) if osu in ['easy.osu', 'normal.osu', 'hard.osu', 'oni.osu']] - if osus: - osud = parse_osu('public/songs/%s/%s' % (song_id, osus[0])) - preview = int(get_osu_key(osud, 'General', 'PreviewTime', 0)) - - return preview - - -def get_tja_preview(tja): - tja_lines = open(tja, 'r').read().replace('\x00', '').split('\n') - - for line in tja_lines: - line = line.strip() - if ':' in line: - name, value = line.split(':', 1) - if name.lower() == 'demostart': - value = value.strip() - try: - value = float(value) - except ValueError: - pass - else: - return int(value * 1000) - elif line.lower() == '#start': - break - return 0 - - def get_version(): version = {'commit': None, 'commit_short': '', 'version': None, 'url': DEFAULT_URL} if os.path.isfile('version.json'): @@ -168,7 +97,7 @@ def route_api_preview(): abort(400) song_type = song_row[0][12] - prev_path = make_preview(song_id, song_type) + prev_path = make_preview(song_id, song_type, song_row[0][15]) if not prev_path: return redirect(get_config()['songs_baseurl'] + '%s/main.ogg' % song_id) @@ -182,7 +111,6 @@ def route_api_songs(): raw_categories = query_db('select * from categories') categories = {} - def_category = {'title': None, 'title_en': None} for cat in raw_categories: categories[cat[0]] = cat[1] @@ -195,9 +123,9 @@ def route_api_songs(): for song in songs: song_id = song[0] song_type = song[12] - preview = get_preview(song_id, song_type) + preview = song[15] - category_out = categories[song[11]] if song[11] in categories else def_category + category_out = categories[song[11]] if song[11] in categories else "" song_skin_out = song_skins[song[14]] if song[14] in song_skins else None songs_out.append({ @@ -213,7 +141,8 @@ def route_api_songs(): 'category': category_out, 'type': song_type, 'offset': song[13], - 'song_skin': song_skin_out + 'song_skin': song_skin_out, + 'volume': song[16] }) return jsonify(songs_out) @@ -226,13 +155,12 @@ def route_api_config(): return jsonify(config) -def make_preview(song_id, song_type): +def make_preview(song_id, song_type, preview): song_path = 'public/songs/%s/main.ogg' % song_id prev_path = 'public/songs/%s/preview.mp3' % song_id if os.path.isfile(song_path) and not os.path.isfile(prev_path): - preview = get_preview(song_id, song_type) / 1000 - if not preview or preview <= 0.1: + if not preview or preview <= 0: print('Skipping #%s due to no preview' % song_id) return False diff --git a/public/assets/audio/bgm_settings.mp3 b/public/assets/audio/bgm_settings.mp3 new file mode 100644 index 0000000..e09d492 Binary files /dev/null and b/public/assets/audio/bgm_settings.mp3 differ diff --git a/public/assets/img/img.css b/public/assets/img/img.css index 7a9d682..2a79238 100644 --- a/public/assets/img/img.css +++ b/public/assets/img/img.css @@ -10,24 +10,16 @@ #loading-don{ background-image: url("dancing-don.gif"); } -#touch-drum-img{ - background-image: url("touch_drum.png"); -} #touch-full-btn{ background-image: url("touch_fullscreen.png"); } #touch-pause-btn{ background-image: url("touch_pause.png"); } -.song-stage-1{ - background-image: url("bg_stage_1.png"); - background-size: calc(100vh / 720 * 66); +.settings-outer{ + background-image: url("bg_settings.png"); } -.song-stage-2{ - background-image: url("bg_stage_2.png"); - background-size: calc(100vh / 720 * 254); -} -.song-stage-3{ - background-image: url("bg_stage_3.png"); - background-size: calc(100vh / 720 * 458); +#gamepad-bg, +#gamepad-buttons{ + background-image: url("settings_gamepad.png"); } diff --git a/public/assets/img/settings_gamepad.png b/public/assets/img/settings_gamepad.png new file mode 100644 index 0000000..c287f06 Binary files /dev/null and b/public/assets/img/settings_gamepad.png differ diff --git a/public/assets/img/vectors.json b/public/assets/img/vectors.json index 0e55a2a..536fb30 100644 --- a/public/assets/img/vectors.json +++ b/public/assets/img/vectors.json @@ -66,8 +66,5 @@ "", "logo5": -"", - -"globe": "" } diff --git a/public/src/css/debug.css b/public/src/css/debug.css index 7c838f0..f9e1478 100644 --- a/public/src/css/debug.css +++ b/public/src/css/debug.css @@ -44,7 +44,8 @@ box-sizing: border-box; } -#debug .input-slider{ +#debug .input-slider, +#debug .select{ display: flex; width: 100%; height: 30px; @@ -59,7 +60,8 @@ padding: 2px 4px; text-align: center; } -#debug .input-slider>span{ +#debug .input-slider>span, +#debug .select>span{ display: block; width: 10%; height: 100%; @@ -70,10 +72,19 @@ line-height: 2em; cursor: pointer; } -#debug .input-slider>span:hover{ +#debug .input-slider>span:hover, +#debug .select>span:hover{ opacity: 1; background: #333; } +#debug .select select{ + width: 90%; + height: 100%; + box-sizing: border-box; + font-size: 18px; + font-family: sans-serif; + padding: 2px 4px; +} #debug label{ display: block; @@ -111,6 +122,7 @@ margin-left: 3px; } -#debug .autoplay-label{ +#debug .autoplay-label, +#debug .branch-hide{ display: none; } diff --git a/public/src/css/game.css b/public/src/css/game.css index 2944e82..f8b7214 100644 --- a/public/src/css/game.css +++ b/public/src/css/game.css @@ -94,3 +94,6 @@ z-index: 2; transition: 1s background-color linear; } +.fix-animations *{ + animation: none !important; +} diff --git a/public/src/css/loader.css b/public/src/css/loader.css index 2899ce0..9e97ec1 100644 --- a/public/src/css/loader.css +++ b/public/src/css/loader.css @@ -15,7 +15,7 @@ body{ margin: 0; padding: 0; background-color: #000; - background-position: top center; + background-position: center; background-size: 30vh; } #screen.pattern-bg{ @@ -29,8 +29,8 @@ body{ width:90%; height:10%; border:1px solid black; - position: fixed; - top:50%; + position: absolute; + top:45%; left:5%; background: rgba(0,0,0,0.65); } diff --git a/public/src/css/main.css b/public/src/css/main.css index 2ec0cf7..c6e1f80 100644 --- a/public/src/css/main.css +++ b/public/src/css/main.css @@ -23,115 +23,6 @@ left: 0; z-index: -1; } -#tutorial-outer{ - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - position: absolute; - width: 100%; - height: 100%; -} -#tutorial{ - background: rgb(246, 234, 212); - color: black; - border: 0.25em black solid; - border-radius: 0.5em; - width: 800px; - padding: 1em; - margin: 1em; - font-size: 21px; - position: relative; -} -.touch-enabled #tutorial{ - font-size: 3vmin; -} -#tutorial-title{ - z-index: 1; - position: absolute; - color: white; - top: -0.7em; - font-size: 1.65em; -} -#tutorial-content{ - margin: 0.7em 0; - overflow-y: auto; - max-height: calc(100vh - 14em); -} -kbd{ - font-family: inherit; - padding: 0.1em 0.6em; - border: 1px solid #ccc; - font-size: 0.6em; - background-color: #f7f7f7; - color: #333; - box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; - border-radius: 3px; - display: inline-block; - text-shadow: 0 1px 0 #fff; - line-height: 1.4; - white-space: nowrap; -} -.taibtn{ - display: inline-block; - background: #f6ead4; - padding: 0.4em 0.4em; - border-radius: 0.5em; - border: 0.1em rgba(218, 205, 178, 1) solid; - cursor: pointer; - font-size: 1.4em; - box-sizing: border-box; - color: #555; - text-align: center; -} -#tutorial-end-button{ - float: right; - padding: 0.4em 1.5em; - font-weight: bold; - border-color: #000; - color: #000; -} -.taibtn:hover, -#tutorial-end-button:hover{ - position: relative; - z-index: 1; - color: #fff; - background: #ffb547; - border-color: #fff; -} -.taibtn::before{ - padding-left: inherit; -} -#about-link-btns{ - float: left; - display: flex; -} -#about-link-btns .taibtn{ - margin-right: 0.4em; -} -#diag-txt textarea, -#diag-txt iframe{ - width: 100%; - height: 5em; - font-size: inherit; - resize: none; - word-break: break-all; - margin-bottom: 1em; - background: #fff; - border: 1px solid #a9a9a9; - user-select: all; -} -.text-warn{ - color: #d00; -} -.link-btn a{ - color: inherit; - text-decoration: none; - pointer-events: none; -} -.nowrap{ - white-space: nowrap; -} #session-invite{ width: 100%; height: 1.9em; @@ -149,10 +40,10 @@ kbd{ } @keyframes bgscroll{ from{ - background-position: 0 top; + background-position: 50% top; } to{ - background-position: calc(-100vh / 720 * 512) top; + background-position: calc(50% - 100vh / 720 * 512) top; } } #song-select{ diff --git a/public/src/css/songbg.css b/public/src/css/songbg.css index 9374f93..5f6f2ef 100644 --- a/public/src/css/songbg.css +++ b/public/src/css/songbg.css @@ -26,6 +26,7 @@ height: calc(44 / 720 * 100vh); background-position: center bottom; background-repeat-y: no-repeat; + background-size: auto 100%; bottom: 0; } .portrait #songbg{ diff --git a/public/src/css/titlescreen.css b/public/src/css/titlescreen.css index 6a280a3..3ca80fc 100644 --- a/public/src/css/titlescreen.css +++ b/public/src/css/titlescreen.css @@ -38,53 +38,6 @@ -webkit-text-stroke: 0.25em #f00; filter: blur(0.3vmin); } -#lang{ - font-size: 3vmin; - position: absolute; - bottom: 0; - left: 0; - width: 7em; - height: 4em; - color: #000; - z-index: 5; -} -#lang:focus-within{ - outline: #4d90fe auto 5px; -} -#lang-dropdown{ - font-size: 1.7em; - font-family: Microsoft YaHei, sans-serif; - opacity: 0; - width: 100%; - height: 100%; - color: #000; - cursor: pointer; -} -#lang-id{ - position: absolute; - top: 0; - bottom: 0; - left: 2.6em; - font-family: TnT, Meiryo, sans-serif; - font-size: 1.5em; - font-weight: normal; - color: #fff; - line-height: 2.75em; - z-index: 0; -} -#lang-icon{ - position: absolute; - width: 2.8em; - height: 2.8em; - padding: 0.6em; - fill: currentColor; -} -#lang:hover #lang-icon{ - color: #f00; -} -#lang:hover #lang-id::before { - -webkit-text-stroke: 0.25em #f00; -} #title-disclaimer { text-align: center; position: absolute; diff --git a/public/src/css/view.css b/public/src/css/view.css new file mode 100644 index 0000000..0340d97 --- /dev/null +++ b/public/src/css/view.css @@ -0,0 +1,261 @@ +.view-outer{ + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + position: absolute; + width: 100%; + height: 100%; + background-position: center; +} +.view{ + background: rgb(246, 234, 212); + color: black; + border: 0.25em black solid; + border-radius: 0.5em; + width: 800px; + max-width: 40em; + padding: 1em; + margin: 1em; + font-size: 21px; + position: relative; +} +@media (max-width: 950px){ + .view-outer:not(.touch-enabled) .view{ + font-size: 3vmin; + } +} +@media (max-height: 650px){ + .view-outer:not(.touch-enabled) .view{ + font-size: 3vmin; + } +} +.touch-enabled .view{ + font-size: 3vmin; +} +.view-title{ + z-index: 1; + position: absolute; + color: white; + top: -0.7em; + font-size: 1.65em; +} +.view-content{ + margin: 0.7em 0; + overflow-y: auto; + max-height: calc(100vh - 14em); +} +kbd{ + font-family: inherit; + padding: 0.1em 0.6em; + border: 1px solid #ccc; + font-size: 0.6em; + background-color: #f7f7f7; + color: #333; + box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; + border-radius: 3px; + display: inline-block; + text-shadow: 0 1px 0 #fff; + line-height: 1.4; + white-space: nowrap; +} +.taibtn{ + display: inline-block; + background: #f6ead4; + padding: 0.4em 0.4em; + border-radius: 0.5em; + border: 0.1em rgba(218, 205, 178, 1) solid; + cursor: pointer; + font-size: 1.4em; + box-sizing: border-box; + color: #555; + text-align: center; +} +.view-end-button{ + float: right; + padding: 0.4em 1.5em; + font-weight: bold; + border-color: #000; + color: #000; + z-index: 1; +} +.taibtn:hover, +.taibtn.selected, +.view-end-button:hover, +.view-end-button.selected{ + position: relative; + color: #fff; + background: #ffb547; + border-color: #fff; +} +.taibtn::before, +.view-end-button::before{ + display: none; +} +.taibtn:hover::before, +.taibtn.selected::before, +.view-end-button:hover::before, +.view-end-button.selected::before{ + display: block +} +.taibtn::before{ + padding-left: inherit; +} +.left-buttons{ + float: left; + display: flex; +} +.left-buttons .taibtn{ + margin-right: 0.4em; +} +#diag-txt textarea, +#diag-txt iframe{ + width: 100%; + height: 5em; + font-size: inherit; + resize: none; + word-break: break-all; + margin-bottom: 1em; + background: #fff; + border: 1px solid #a9a9a9; + user-select: all; +} +.text-warn{ + color: #d00; +} +.link-btn a{ + color: inherit; + text-decoration: none; + pointer-events: none; +} +.nowrap{ + white-space: nowrap; +} +@keyframes border-pulse{ + 0%{border-color: #ff0} + 50%{border-color: rgba(255, 255, 0, 0)} + 100%{border-color: #ff0} +} +@keyframes border-pulse2{ + 0%{border-color: #e29e06} + 50%{border-color: rgba(226, 158, 6, 0)} + 100%{border-color: #e29e06} +} +.settings-outer{ + background-size: 50vh; +} +.setting-box{ + display: flex; + height: 2em; + margin-top: 1.2em; + border: 0.25em solid #000; + border-radius: 0.5em; + padding: 0.3em; + outline: none; + color: #000; + cursor: pointer; +} +.setting-box:first-child{ + margin-top: 0; +} +.settings-outer .view-content:not(:hover) .setting-box.selected, +.view-outer:not(.settings-outer) .setting-box.selected, +.setting-box:hover{ + background: #ffb547; + animation: 2s linear border-pulse infinite; +} +.bold-fonts .setting-box{ + line-height: 1em; +} +.setting-name{ + position: relative; + width: 50%; + padding: 0.3em; + font-size: 1.3em; + box-sizing: border-box; + white-space: nowrap; + 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; + z-index: 0; +} +.setting-name::before{ + padding-left: 0.3em; +} +.setting-value{ + display: flex; + background: #fff; + width: 50%; + border-radius: 0.2em; + padding: 0.5em; + box-sizing: border-box; +} +.setting-value.selected{ + width: calc(50% + 0.2em); + margin: -0.1em; + border: 0.2em solid #e29e06; + padding: 0.4em; + animation: 2s linear border-pulse2 infinite; +} +.setting-value>div{ + padding: 0 0.4em; + overflow: hidden; + text-overflow: ellipsis; +} +.shadow-outer{ + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1; +} +#settings-gamepad{ + 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); +} +#settings-gamepad .setting-box{ + height: auto; +} +#gamepad-bg{ + position: relative; + width: 550px; + height: 317px; + max-height: none; + background-repeat: no-repeat; + text-align: center; + font-size: 1.4em; + cursor: pointer; +} +#gamepad-buttons{ + position: absolute; + left: 141px; + top: 120px; + width: 282px; + height: 131px; + background-position: 0 -318px; + background-repeat: no-repeat; + pointer-events: none; +} +#gamepad-value{ + position: relative; + margin-top: 1em; +} +#gamepad-value::before{ + left: auto; +} diff --git a/public/src/js/about.js b/public/src/js/about.js index 235a8de..104d8a1 100644 --- a/public/src/js/about.js +++ b/public/src/js/about.js @@ -4,20 +4,20 @@ loader.changePage("about", true) cancelTouch = false - this.endButton = document.getElementById("tutorial-end-button") + this.endButton = this.getElement("view-end-button") this.diagTxt = document.getElementById("diag-txt") this.version = document.getElementById("version-link").href - this.tutorialOuter = document.getElementById("tutorial-outer") + this.tutorialOuter = this.getElement("view-outer") if(touchEnabled){ this.tutorialOuter.classList.add("touch-enabled") } this.linkIssues = document.getElementById("link-issues") this.linkEmail = document.getElementById("link-email") - var tutorialTitle = document.getElementById("tutorial-title") + var tutorialTitle = this.getElement("view-title") tutorialTitle.innerText = strings.aboutSimulator tutorialTitle.setAttribute("alt", strings.aboutSimulator) - var tutorialContent = document.getElementById("tutorial-content") + var tutorialContent = this.getElement("view-content") strings.about.bugReporting.forEach(string => { tutorialContent.appendChild(document.createTextNode(string)) tutorialContent.appendChild(document.createElement("br")) @@ -34,20 +34,62 @@ pageEvents.add(this.linkIssues, ["click", "touchend"], this.linkButton.bind(this)) pageEvents.add(this.linkEmail, ["click", "touchend"], this.linkButton.bind(this)) - pageEvents.once(this.endButton, ["mousedown", "touchstart"]).then(this.onEnd.bind(this)) - pageEvents.keyOnce(this, 13, "down").then(this.onEnd.bind(this)) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items = [this.linkIssues, this.linkEmail, this.endButton] + this.selected = 2 + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + back: ["escape"] + }, this.keyPressed.bind(this)) this.gamepad = new Gamepad({ - "confirm": ["start", "b", "ls", "rs"] - }, this.onEnd.bind(this)) + "confirm": ["b", "ls", "rs"], + "previous": ["u", "l", "lb", "lt", "lsu", "lsl"], + "next": ["d", "r", "rb", "rt", "lsd", "lsr"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) - this.addDiag() + pageEvents.send("about", this.addDiag()) + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + keyPressed(pressed, name){ + if(!pressed){ + return + } + var selected = this.items[this.selected] + if(name === "confirm"){ + if(selected === this.endButton){ + this.onEnd() + }else{ + this.getLink(selected).click() + pageEvents.send("about-link", selected) + assets.sounds["se_don"].play() + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + }else if(name === "back"){ + this.onEnd() + } + } + mod(length, index){ + return ((index % length) + length) % length } onEnd(event){ var touched = false - if(event && event.type === "touchstart"){ - event.preventDefault() - touched = true + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } } this.clean() assets.sounds["se_don"].play() @@ -143,17 +185,24 @@ } } - var issueBody = strings.issueTemplate + "\n\n\n\n" + diag + var issueBody = strings.about.issueTemplate + "\n\n\n\n" + diag this.getLink(this.linkEmail).href += "?body=" + encodeURIComponent(issueBody.replace(/\n/g, "
\r\n")) + + return diag } getLink(target){ return target.getElementsByTagName("a")[0] } linkButton(event){ - this.getLink(event.currentTarget).click() + if(event.target === event.currentTarget){ + this.getLink(event.currentTarget).click() + pageEvents.send("about-link", event.currentTarget) + assets.sounds["se_don"].play() + } } clean(){ cancelTouch = true + this.keyboard.clean() this.gamepad.clean() pageEvents.remove(this.linkIssues, ["click", "touchend"]) pageEvents.remove(this.linkEmail, ["click", "touchend"]) @@ -161,7 +210,7 @@ if(this.textarea){ pageEvents.remove(this.textarea, ["focus", "blur"]) } - pageEvents.keyRemove(this, 13) + pageEvents.keyRemove(this, "all") delete this.endButton delete this.diagTxt delete this.version diff --git a/public/src/js/assets.js b/public/src/js/assets.js index 4f938a1..e363842 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -7,6 +7,7 @@ var assets = { "scoresheet.js", "songselect.js", "keyboard.js", + "gameinput.js", "game.js", "controller.js", "circle.js", @@ -27,7 +28,8 @@ var assets = { "debug.js", "session.js", "importsongs.js", - "logo.js" + "logo.js", + "settings.js" ], "css": [ "main.css", @@ -35,7 +37,8 @@ var assets = { "loadsong.css", "game.css", "debug.css", - "songbg.css" + "songbg.css", + "view.css" ], "assetsCss": [ "fonts/fonts.css", @@ -71,18 +74,15 @@ var assets = { "bg_score_p2.png", "bg_settings.png", "bg_pause.png", - "bg_stage_1.png", - "bg_stage_2.png", - "bg_stage_3.png", "badge_auto.png", - "touch_drum.png", "touch_pause.png", "touch_fullscreen.png", "mimizu.png", "results_flowers.png", "results_mikoshi.png", "results_tetsuohana.png", - "results_tetsuohana2.png" + "results_tetsuohana2.png", + "settings_gamepad.png" ], "audioSfx": [ "se_cancel.wav", @@ -154,7 +154,8 @@ var assets = { "audioMusic": [ "bgm_songsel.mp3", "bgm_result.mp3", - "bgm_setsume.mp3" + "bgm_setsume.mp3", + "bgm_settings.mp3" ], "fonts": [ "Kozuka", @@ -168,7 +169,8 @@ var assets = { "tutorial.html", "about.html", "debug.html", - "session.html" + "session.html", + "settings.html" ], "songs": [], diff --git a/public/src/js/canvascache.js b/public/src/js/canvascache.js index 56a1f7d..a731bcb 100644 --- a/public/src/js/canvascache.js +++ b/public/src/js/canvascache.js @@ -29,33 +29,58 @@ class CanvasCache{ return } var saved = false + var time = Date.now() if(!img){ var w = config.w var h = config.h - this.x += this.lastW + 1 + this.x += this.lastW + (this.lastW ? 1 : 0) if(this.x + w > this.w){ this.x = 0 this.y += this.lastH + 1 } - this.lastW = w - this.lastH = Math.max(this.lastH, h) - img = { - x: this.x, - y: this.y, - w: w, - h: h + if(this.y + h > this.h){ + var clear = true + var oldest = {time: time} + this.map.forEach((oldImg, id) => { + if(oldImg.time < oldest.time){ + oldest.id = id + oldest.time = oldImg.time + } + }) + var oldImg = this.map.get(oldest.id) + this.map.delete(oldest.id) + img = { + x: oldImg.x, + y: oldImg.y, + w: w, + h: h + } + }else{ + var clear = false + this.lastW = w + this.lastH = Math.max(this.lastH, h) + img = { + x: this.x, + y: this.y, + w: w, + h: h + } } - this.map.set(config.id, img) saved = true this.ctx.save() this.ctx.translate(img.x |0, img.y |0) + if(clear){ + this.ctx.clearRect(0, 0, (img.w |0) + 1, (img.h |0) + 1) + } this.ctx.beginPath() this.ctx.rect(0, 0, img.w |0, img.h |0) this.ctx.clip() + this.map.set(config.id, img) callback(this.ctx) } + img.time = time if(setOnly){ this.ctx.restore() return @@ -81,6 +106,10 @@ class CanvasCache{ this.ctx.clearRect(0, 0, this.w, this.h) } clean(){ + if(!this.canvas){ + return + } + this.resize(1, 1, 1) delete this.map delete this.ctx delete this.canvas diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index 472730c..0e77fbb 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -48,9 +48,9 @@ this.regex = { comma: /[,.]/, ideographicComma: /[、。]/, - apostrophe: /['']/, + apostrophe: /[''’]/, degree: /[゚°]/, - brackets: /[\((\))\[\]「」『』【】]/, + brackets: /[\((\))\[\]「」『』【】::;;]/, tilde: /[\--~~〜_]/, tall: /[bbddffgghhj-lj-ltt♪]/, i: /[ii]/, @@ -268,13 +268,16 @@ easeOut(pos){ return Math.sin(Math.PI / 2 * pos) } + easeOutBack(pos){ + return Math.sin(Math.PI / 1.74 * pos) * 1.03 + } easeInOut(pos){ return (Math.cos(Math.PI * pos) - 1) / -2 } verticalText(config){ var ctx = config.ctx - var inputText = config.text + var inputText = config.text.toString() var mul = config.fontSize / 40 var ura = false var r = this.regex @@ -572,7 +575,7 @@ } if(symbol.ura){ ctx.font = (30 * mul) + "px Meiryo, sans-serif" - ctx.textBaseline = "center" + ctx.textBaseline = "middle" ctx.beginPath() ctx.arc(currentX, currentY + (17 * mul), (18 * mul), 0, Math.PI * 2) if(action === "stroke"){ @@ -581,7 +584,7 @@ }else if(action === "fill"){ ctx.strokeStyle = config.fill ctx.lineWidth = 2.5 * mul - ctx.fillText(symbol.text, currentX, currentY) + ctx.fillText(symbol.text, currentX, currentY + (17 * mul)) } ctx.stroke() }else{ @@ -598,7 +601,7 @@ layeredText(config, layers){ var ctx = config.ctx - var inputText = config.text + var inputText = config.text.toString() var mul = config.fontSize / 40 var ura = false var r = this.regex @@ -622,8 +625,6 @@ drawn.push({text: symbol, x: -2, y: 0, w: 20, scale: [0.6, 0.5]}) }else if(symbol === " "){ drawn.push({text: symbol, x: 0, y: 0, w: 10}) - }else if(symbol === "'"){ - drawn.push({text: ",", x: 0, y: -15, w: 7, scale: [1, 0.7]}) }else if(symbol === '"'){ drawn.push({text: symbol, x: 2, y: 0, w: 10}) }else if(symbol === "∀"){ @@ -634,6 +635,8 @@ } }else if(symbol === "."){ drawn.push({text: symbol, x: -9, y: 0, w: 37}) + }else if(r.apostrophe.test(symbol)){ + drawn.push({text: ",", x: 0, y: -15, w: 7, scale: [1, 0.7]}) }else if(r.comma.test(symbol)){ // Comma, full stop if(bold){ @@ -788,7 +791,7 @@ } if(symbol.ura){ ctx.font = (30 * mul) + "px Meiryo, sans-serif" - ctx.textBaseline = "center" + ctx.textBaseline = "middle" ctx.beginPath() ctx.arc(currentX, currentY + (17 * mul), (18 * mul), 0, Math.PI * 2) if(action === "strokeText"){ @@ -797,7 +800,7 @@ }else if(action === "fillText"){ ctx.strokeStyle = layer.fill ctx.lineWidth = 2.5 * mul - ctx.fillText(symbol.text, currentX, currentY) + ctx.fillText(symbol.text, currentX, currentY + (17 * mul)) } ctx.stroke() }else{ @@ -1167,6 +1170,7 @@ var firstTop = config.multiplayer ? 0 : 30 var secondTop = config.multiplayer ? 0 : 8 + config.percentage = Math.max(0, Math.min(1, config.percentage)) var cleared = config.percentage - 1 / 50 >= config.clear var gaugeW = 14 * 50 diff --git a/public/src/js/circle.js b/public/src/js/circle.js index 6d3c650..261629e 100644 --- a/public/src/js/circle.js +++ b/public/src/js/circle.js @@ -1,6 +1,5 @@ class Circle{ constructor(config){ - // id, ms, type, text, speed, endTime, requiredHits this.id = config.id this.ms = config.start this.originalMS = this.ms @@ -23,38 +22,13 @@ class Circle{ this.gogoChecked = false this.beatMS = config.beatMS this.fixedPos = config.fixedPos - } - getMS(){ - return this.ms - } - getEndTime(){ - return this.endTime - } - getType(){ - return this.type - } - getLastFrame(){ - return this.lastFrame + this.branch = config.branch + this.section = config.section } animate(ms){ this.animating = true this.animT = ms } - isAnimated(){ - return this.animating - } - getAnimT(){ - return this.animT - } - getPlayed(){ - return this.isPlayed - } - isAnimationFinished(){ - return this.animationEnded - } - endAnimation(){ - this.animationEnded = true - } played(score, big){ this.score = score this.isPlayed = score <= 0 ? score - 1 : (big ? 2 : 1) @@ -65,16 +39,4 @@ class Circle{ this.timesKa++ } } - getScore(){ - return this.score - } - getID(){ - return this.id - } - getText(){ - return this.text - } - getSpeed(){ - return this.speed - } } \ No newline at end of file diff --git a/public/src/js/controller.js b/public/src/js/controller.js index ef7dca3..cf253b4 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -21,13 +21,14 @@ class Controller{ 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 Keyboard(this) + this.keyboard = new GameInput(this) this.playedSounds = {} } @@ -35,6 +36,9 @@ class Controller{ if(syncWith){ this.syncWith = syncWith } + if(this.multiplayer !== 2){ + snd.musicGain.setVolumeMul(this.volume) + } this.game.run() this.view.run() if(this.multiplayer === 1){ @@ -58,11 +62,19 @@ class Controller{ this.viewLoop() if(this.multiplayer !== 2){ this.gameInterval = setInterval(this.gameLoop.bind(this), 1000 / 60) + pageEvents.send("game-start", { + selectedSong: this.selectedSong, + autoPlayEnabled: this.autoPlayEnabled, + multiplayer: this.multiplayer, + touchEnabled: this.touchEnabled + }) } } stopMainLoop(){ this.mainLoopRunning = false - this.mainAsset.stop() + if(this.mainAsset){ + this.mainAsset.stop() + } if(this.multiplayer !== 2){ clearInterval(this.gameInterval) } @@ -153,8 +165,26 @@ class Controller{ if(this.multiplayer){ new LoadSong(this.selectedSong, false, true, this.touchEnabled) }else{ - var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) - taikoGame.run() + 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{ + resolve() + } + }).then(() => { + var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) + taikoGame.run() + }) } } playSound(id, time){ @@ -180,11 +210,8 @@ class Controller{ getKeys(){ return this.keyboard.getKeys() } - setKey(keyCode, down, ms){ - return this.keyboard.setKey(keyCode, down, ms) - } - getBindings(){ - return this.keyboard.getBindings() + setKey(pressed, name, ms){ + return this.keyboard.setKey(pressed, name, ms) } getElapsedTime(){ return this.game.elapsedTime @@ -214,7 +241,7 @@ class Controller{ if(this.multiplayer){ p2.play(circle, this.mekadon) }else{ - this.mekadon.play(circle) + return this.mekadon.play(circle) } } clean(){ @@ -224,6 +251,7 @@ class Controller{ this.stopMainLoop() this.keyboard.clean() this.view.clean() + snd.buffer.loadSettings() if(!this.multiplayer){ debugObj.controller = null diff --git a/public/src/js/debug.js b/public/src/js/debug.js index ca2cf51..a6fa9a7 100644 --- a/public/src/js/debug.js +++ b/public/src/js/debug.js @@ -8,15 +8,20 @@ class Debug{ this.debugDiv.innerHTML = assets.pages["debug"] document.body.appendChild(this.debugDiv) - this.titleDiv = this.debugDiv.getElementsByClassName("title")[0] - this.minimiseDiv = this.debugDiv.getElementsByClassName("minimise")[0] - this.offsetDiv = this.debugDiv.getElementsByClassName("offset")[0] - this.measureNumDiv = this.debugDiv.getElementsByClassName("measure-num")[0] - this.restartCheckbox = this.debugDiv.getElementsByClassName("change-restart")[0] - this.autoplayLabel = this.debugDiv.getElementsByClassName("autoplay-label")[0] - this.autoplayCheckbox = this.debugDiv.getElementsByClassName("autoplay")[0] - this.restartBtn = this.debugDiv.getElementsByClassName("restart-btn")[0] - this.exitBtn = this.debugDiv.getElementsByClassName("exit-btn")[0] + this.titleDiv = this.byClass("title") + this.minimiseDiv = this.byClass("minimise") + this.offsetDiv = this.byClass("offset") + this.measureNumDiv = this.byClass("measure-num") + this.branchHideDiv = this.byClass("branch-hide") + this.branchSelectDiv = this.byClass("branch-select") + this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0] + this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0] + this.volumeDiv = this.byClass("music-volume") + this.restartCheckbox = this.byClass("change-restart") + this.autoplayLabel = this.byClass("autoplay-label") + this.autoplayCheckbox = this.byClass("autoplay") + this.restartBtn = this.byClass("restart-btn") + this.exitBtn = this.byClass("exit-btn") this.moving = false pageEvents.add(window, ["mousedown", "mouseup", "blur"], this.stopMove.bind(this)) @@ -26,6 +31,8 @@ class Debug{ pageEvents.add(this.restartBtn, "click", this.restartSong.bind(this)) pageEvents.add(this.exitBtn, "click", this.clean.bind(this)) pageEvents.add(this.autoplayCheckbox, "change", this.toggleAutoplay.bind(this)) + pageEvents.add(this.branchSelect, "change", this.branchChange.bind(this)) + pageEvents.add(this.branchResetBtn, "click", this.branchReset.bind(this)) this.offsetSlider = new InputSlider(this.offsetDiv, -60, 60, 3) this.offsetSlider.onchange(this.offsetChange.bind(this)) @@ -34,9 +41,17 @@ class Debug{ this.measureNumSlider.onchange(this.measureNumChange.bind(this)) this.measureNumSlider.set(0) + this.volumeSlider = new InputSlider(this.volumeDiv, 0, 3, 2) + this.volumeSlider.onchange(this.volumeChange.bind(this)) + this.volumeSlider.set(1) + this.moveTo(100, 100) this.restore() this.updateStatus() + pageEvents.send("debug") + } + byClass(name){ + return this.debugDiv.getElementsByClassName(name)[0] } startMove(event){ if(event.which === 1){ @@ -87,20 +102,30 @@ class Debug{ } updateStatus(){ if(debugObj.controller && !this.controller){ + this.controller = debugObj.controller + this.restartBtn.style.display = "block" this.autoplayLabel.style.display = "block" + if(this.controller.parsedSongData.branches){ + this.branchHideDiv.style.display = "block" + } - this.controller = debugObj.controller var selectedSong = this.controller.selectedSong this.defaultOffset = selectedSong.offset || 0 if(this.songFolder === selectedSong.folder){ this.offsetChange(this.offsetSlider.get(), true) + this.branchChange(null, true) + this.volumeChange(this.volumeSlider.get(), true) }else{ this.songFolder = selectedSong.folder this.offsetSlider.set(this.defaultOffset) + this.branchReset(null, true) + this.volumeSlider.set(this.controller.volume) } - var measures = this.controller.parsedSongData.measures + var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => { + return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01 + }) this.measureNumSlider.setMinMax(0, measures.length - 1) if(this.measureNum && measures.length > this.measureNum){ var measureMS = measures[this.measureNum].ms @@ -116,6 +141,7 @@ class Debug{ if(circles[i].endTime >= measureMS){ break } + game.skipNote(circles[i]) } if(game.mainMusicPlaying){ game.mainMusicPlaying = false @@ -127,6 +153,7 @@ class Debug{ if(this.controller && !debugObj.controller){ this.restartBtn.style.display = "" this.autoplayLabel.style.display = "" + this.branchHideDiv.style.display = "" this.controller = null } } @@ -141,6 +168,11 @@ class Debug{ songData.measures.forEach(measure => { measure.ms = measure.originalMS + offset }) + if(songData.branches){ + songData.branches.forEach(branch => { + branch.ms = branch.originalMS + offset + }) + } if(this.restartCheckbox.checked && !noRestart){ this.restartSong() } @@ -152,6 +184,14 @@ class Debug{ this.restartSong() } } + volumeChange(value, noRestart){ + if(this.controller){ + snd.musicGain.setVolumeMul(value) + } + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } restartSong(){ if(this.controller){ this.controller.restartSong() @@ -162,29 +202,57 @@ class Debug{ this.controller.autoPlayEnabled = this.autoplayCheckbox.checked if(!this.controller.autoPlayEnabled){ var keyboard = debugObj.controller.keyboard - var kbd = keyboard.getBindings() - keyboard.setKey(kbd.don_l, false) - keyboard.setKey(kbd.don_r, false) - keyboard.setKey(kbd.ka_l, false) - keyboard.setKey(kbd.ka_r, false) + keyboard.setKey(false, "don_l") + keyboard.setKey(false, "don_r") + keyboard.setKey(false, "ka_l") + keyboard.setKey(false, "ka_r") } } } + branchChange(event, noRestart){ + if(this.controller){ + var game = this.controller.game + var name = this.branchSelect.value + game.branch = name === "auto" ? false : name + game.branchSet = name === "auto" + if(noRestart){ + game.branchStatic = true + } + var selectedOption = this.branchSelect.selectedOptions[0] + this.branchSelect.style.background = selectedOption.style.background + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } + } + branchReset(event, noRestart){ + this.branchSelect.value = "auto" + this.branchChange(null, noRestart) + } clean(){ this.offsetSlider.clean() + this.measureNumSlider.clean() pageEvents.remove(window, ["mousedown", "mouseup", "blur"]) pageEvents.mouseRemove(this) + pageEvents.remove(this.titleDiv, "mousedown") pageEvents.remove(this.title, "mousedown") pageEvents.remove(this.minimiseDiv, "click") pageEvents.remove(this.restartBtn, "click") pageEvents.remove(this.exitBtn, "click") pageEvents.remove(this.autoplayCheckbox, "change") + pageEvents.remove(this.branchSelect, "change") + pageEvents.remove(this.branchResetBtn, "click") delete this.titleDiv delete this.minimiseDiv delete this.offsetDiv delete this.measureNumDiv + delete this.branchHideDiv + delete this.branchSelectDiv + delete this.branchSelect + delete this.branchResetBtn + delete this.volumeDiv delete this.restartCheckbox delete this.autoplayLabel delete this.autoplayCheckbox diff --git a/public/src/js/game.js b/public/src/js/game.js index 2df56d0..63a92b7 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -19,8 +19,8 @@ class Game{ difficulty: this.rules.difficulty } this.HPGain = 100 / this.songData.circles.filter(circle => { - var type = circle.getType() - return type === "don" || type === "ka" || type === "daiDon" || type === "daiKa" + var type = circle.type + return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active) }).length this.paused = false this.started = false @@ -28,6 +28,9 @@ class Game{ this.musicFadeOut = 0 this.fadeOutStarted = false this.currentTimingPoint = 0 + this.branchNames = ["normal", "advanced", "master"] + this.resetSection() + this.gameLagSync = !this.controller.touchEnabled && !(/Firefox/.test(navigator.userAgent)) assets.songs.forEach(song => { if(song.id == selectedSong.folder){ @@ -69,17 +72,19 @@ class Game{ } updateCirclesStatus(){ var nextSet = false + var ms = this.elapsedTime var circles = this.songData.circles var startIndex = this.currentCircle === 0 ? 0 : this.currentCircle - 1 - for(var i = startIndex; i < circles.length && i < this.currentCircle + 2; i++){ + var index = 0 + + for(var i = startIndex; i < circles.length; i++){ var circle = circles[i] - if(!circle.getPlayed()){ - var ms = this.elapsedTime - var type = circle.getType() + if(circle && (!circle.branch || circle.branch.active) && !circle.isPlayed){ + var type = circle.type var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" - var endTime = circle.getEndTime() + (drumrollNotes ? 0 : this.rules.bad) + var endTime = circle.endTime + (drumrollNotes ? 0 : this.rules.bad) - if(ms >= circle.getMS()){ + if(ms >= circle.ms){ if(drumrollNotes && !circle.rendaPlayed && ms < endTime){ circle.rendaPlayed = true if(this.rules.difficulty === "easy"){ @@ -97,11 +102,14 @@ class Game{ if(ms > endTime){ if(!this.controller.autoPlayEnabled){ if(drumrollNotes){ + if(circle.section && circle.timesHit === 0){ + this.resetSection() + } circle.played(-1, false) this.updateCurrentCircle() if(this.controller.multiplayer === 1){ var value = { - pace: (ms - circle.getMS()) / circle.timesHit + pace: (ms - circle.ms) / circle.timesHit } if(type === "drumroll" || type === "daiDrumroll"){ value.kaAmount = circle.timesKa / circle.timesHit @@ -109,58 +117,178 @@ class Game{ p2.send("drumroll", value) } }else{ - var currentScore = 0 - circle.played(-1, type === "daiDon" || type === "daiKa") - this.controller.displayScore(currentScore, true) + this.skipNote(circle) this.updateCurrentCircle() - this.updateCombo(currentScore) - this.updateGlobalScore(currentScore, 1) - if(this.controller.multiplayer === 1){ - p2.send("note", { - score: -1 - }) - } } } }else if(!this.controller.autoPlayEnabled && !nextSet){ nextSet = true this.currentCircle = i } + if(index++ > 1){ + break + } } } + + var branches = this.songData.branches + if(branches){ + var force = this.controller.multiplayer === 2 ? p2 : this + var measures = this.songData.measures + if(this.controller.multiplayer === 2 || force.branch){ + if(!force.branchSet){ + force.branchSet = true + if(branches.length){ + this.setBranch(branches[0], force.branch) + } + var view = this.controller.view + var currentMeasure = view.branch + for(var i = measures.length; i--;){ + var measure = measures[i] + if(measure.nextBranch && measure.ms <= ms){ + currentMeasure = measure.nextBranch.active + } + } + if(view.branch !== currentMeasure){ + if(!this.branchStatic){ + view.branchAnimate = { + ms: ms, + fromBranch: view.branch + } + } + this.branchStatic = false + view.branch = currentMeasure + } + } + } + for(var i = 0; i < measures.length; i++){ + var measure = measures[i] + if(measure.ms > ms){ + break + }else if(measure.nextBranch && !measure.gameChecked){ + measure.gameChecked = true + var branch = measure.nextBranch + if(branch.type){ + var accuracy = 0 + if(branch.type === "drumroll"){ + if(force.branch){ + var accuracy = Math.max(0, branch.requirement[force.branch]) + }else{ + var accuracy = this.sectionDrumroll + } + }else if(this.sectionNotes.length !== 0){ + if(force.branch){ + var accuracy = Math.max(0, Math.min(100, branch.requirement[force.branch])) + }else{ + var accuracy = this.sectionNotes.reduce((a, b) => a + b) / this.sectionNotes.length * 100 + } + } + if(accuracy >= branch.requirement.master){ + this.setBranch(branch, "master") + }else if(accuracy >= branch.requirement.advanced){ + this.setBranch(branch, "advanced") + }else{ + this.setBranch(branch, "normal") + } + }else if(this.controller.multiplayer === 1){ + p2.send("branch", "normal") + } + } + } + } + } + fixNoteStream(keysDon){ + var circleIsNote = circle => { + var type = circle.type + return type === "don" || type === "ka" || type === "daiDon" || type === "daiKa" + } + var correctNote = circle => { + var type = circle.type + return keysDon ? (type === "don" || type === "daiDon") : (type === "ka" || type === "daiKa") + } + var ms = this.elapsedTime + var circles = this.songData.circles + + for(var i = this.currentCircle + 1; i < circles.length; i++){ + var circle = circles[i] + var relative = ms - circle.ms + if(!circle.branch || circle.branch.active){ + if((!circleIsNote(circle) || relative < -this.rules.bad)){ + break + }else if(Math.abs(relative) < this.rules.ok && correctNote(circle)){ + for(var j = this.currentCircle; j < i; j++){ + var circle = circles[j] + if(circle && !circle.branch || circle.branch.active){ + this.skipNote(circles[j]) + } + } + this.currentCircle = i + return circles[i] + } + } + } + } + skipNote(circle){ + if(circle.section){ + this.resetSection() + } + circle.played(-1, circle.type === "daiDon" || circle.type === "daiKa") + this.sectionNotes.push(0) + this.controller.displayScore(0, true) + this.updateCombo(0) + this.updateGlobalScore(0, 1) + if(this.controller.multiplayer === 1){ + p2.send("note", { + score: -1 + }) + } } checkPlays(){ var circles = this.songData.circles var circle = circles[this.currentCircle] - if(circle && this.controller.autoPlayEnabled){ - return this.controller.autoPlay(circle) + if(this.controller.autoPlayEnabled){ + while(circle && this.controller.autoPlay(circle)){ + circle = circles[this.currentCircle] + } + return } var keys = this.controller.getKeys() - var kbd = this.controller.getBindings() - var don_l = keys[kbd["don_l"]] && !this.controller.isWaiting(kbd["don_l"], "score") - var don_r = keys[kbd["don_r"]] && !this.controller.isWaiting(kbd["don_r"], "score") - var ka_l = keys[kbd["ka_l"]] && !this.controller.isWaiting(kbd["ka_l"], "score") - var ka_r = keys[kbd["ka_r"]] && !this.controller.isWaiting(kbd["ka_r"], "score") + var don_l = keys["don_l"] && !this.controller.isWaiting("don_l", "score") + var don_r = keys["don_r"] && !this.controller.isWaiting("don_r", "score") + var ka_l = keys["ka_l"] && !this.controller.isWaiting("ka_l", "score") + var ka_r = keys["ka_r"] && !this.controller.isWaiting("ka_r", "score") - if(don_l && don_r){ - this.checkKey([kbd["don_l"], kbd["don_r"]], circle, "daiDon") - }else if(don_l){ - this.checkKey([kbd["don_l"]], circle, "don") - }else if(don_r){ - this.checkKey([kbd["don_r"]], circle, "don") + var checkDon = () => { + if(don_l && don_r){ + this.checkKey(["don_l", "don_r"], circle, "daiDon") + }else if(don_l){ + this.checkKey(["don_l"], circle, "don") + }else if(don_r){ + this.checkKey(["don_r"], circle, "don") + } } - if(ka_l && ka_r){ - this.checkKey([kbd["ka_l"], kbd["ka_r"]], circle, "daiKa") - }else if(ka_l){ - this.checkKey([kbd["ka_l"]], circle, "ka") - }else if(ka_r){ - this.checkKey([kbd["ka_r"]], circle, "ka") + var checkKa = () => { + if(ka_l && ka_r){ + this.checkKey(["ka_l", "ka_r"], circle, "daiKa") + }else if(ka_l){ + this.checkKey(["ka_l"], circle, "ka") + }else if(ka_r){ + this.checkKey(["ka_r"], circle, "ka") + } + } + var keyTime = this.controller.getKeyTime() + if(keyTime["don"] >= keyTime["ka"]){ + checkDon() + checkKa() + }else{ + checkKa() + checkDon() } } checkKey(keyCodes, circle, check){ - if(circle && !circle.getPlayed()){ + if(circle && !circle.isPlayed){ if(!this.checkScore(circle, check)){ return } @@ -171,7 +299,7 @@ class Game{ } checkScore(circle, check){ var ms = this.elapsedTime - var type = circle.getType() + var type = circle.type var keysDon = check === "don" || check === "daiDon" var keysKa = check === "ka" || check === "daiKa" @@ -182,7 +310,14 @@ class Game{ var keyTime = this.controller.getKeyTime() var currentTime = keysDon ? keyTime["don"] : keyTime["ka"] - var relative = currentTime - circle.getMS() + var relative = currentTime - circle.ms + + if(relative >= this.rules.ok){ + var fixedNote = this.fixNoteStream(keysDon) + if(fixedNote){ + return this.checkScore(fixedNote, check) + } + } if(typeDon || typeKa){ if(-this.rules.bad >= relative || relative >= this.rules.bad){ @@ -213,17 +348,26 @@ class Game{ circle.played(score, score === 0 ? typeDai : keyDai) this.controller.displayScore(score, false, typeDai && keyDai) }else{ + var keyTime = this.controller.getKeyTime() + var keyTimeRelative = Math.abs(keyTime.don - keyTime.ka) + if(Math.abs(relative) >= (keyTimeRelative <= 25 ? this.rules.bad : this.rules.good)){ + return true + } circle.played(-1, typeDai) this.controller.displayScore(score, true, false) } this.updateCombo(score) this.updateGlobalScore(score, typeDai && keyDai ? 2 : 1, circle.gogoTime) this.updateCurrentCircle() - if(this.controller.multiplayer == 1){ + if(circle.section){ + this.resetSection() + } + this.sectionNotes.push(score === 450 ? 1 : (score === 230 ? 0.5 : 0)) + if(this.controller.multiplayer === 1){ var value = { score: score, - ms: circle.getMS() - currentTime, - dai: typeDai ? keyDai ? 2 : 1 : 0 + ms: circle.ms - currentTime, + dai: typeDai ? (keyDai ? 2 : 1) : 0 } if((!keysDon || !typeDon) && (!keysKa || !typeKa)){ value.reverse = true @@ -231,12 +375,12 @@ class Game{ p2.send("note", value) } }else{ - if(circle.getMS() > currentTime || currentTime > circle.getEndTime()){ + if(circle.ms > currentTime || currentTime > circle.endTime){ return true } if(keysDon && type === "balloon"){ this.checkBalloon(circle) - if(check === "daiDon" && !circle.getPlayed()){ + if(check === "daiDon" && !circle.isPlayed){ this.checkBalloon(circle) } }else if((keysDon || keysKa) && (type === "drumroll" || type === "daiDrumroll")){ @@ -256,24 +400,28 @@ class Game{ circle.played(score) if(this.controller.multiplayer == 1){ p2.send("drumroll", { - pace: (this.elapsedTime - circle.getMS()) / circle.timesHit + pace: (this.elapsedTime - circle.ms) / circle.timesHit }) } }else{ var score = 300 circle.hit() } - this.globalScore.drumroll ++ + this.globalScore.drumroll++ + this.sectionDrumroll++ this.globalScore.points += score this.view.setDarkBg(false) } checkDrumroll(circle, keysKa){ var ms = this.elapsedTime - var dai = circle.getType() === "daiDrumroll" + var dai = circle.type === "daiDrumroll" var score = 100 + if(circle.section && circle.timesHit === 0){ + this.resetSection() + } circle.hit(keysKa) var keyTime = this.controller.getKeyTime() - if(circle.getType() === "drumroll"){ + if(circle.type === "drumroll"){ var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka" }else{ var sound = keyTime["don"] > keyTime["ka"] ? "daiDon" : "daiKa" @@ -291,6 +439,7 @@ class Game{ circleAnim.animate(ms) this.view.drumroll.push(circleAnim) this.globalScore.drumroll++ + this.sectionDrumroll++ this.globalScore.points += score * (dai ? 2 : 1) this.view.setDarkBg(false) } @@ -298,11 +447,11 @@ class Game{ var ms = this.elapsedTime if(!this.lastCircle){ var circles = this.songData.circles - this.lastCircle = circles[circles.length - 1].getEndTime() + this.lastCircle = circles[circles.length - 1].endTime if(this.controller.multiplayer){ var syncWith = this.controller.syncWith var syncCircles = syncWith.game.songData.circles - var syncLastCircle = syncCircles[syncCircles.length - 1].getEndTime() + var syncLastCircle = syncCircles[syncCircles.length - 1].endTime if(syncLastCircle > this.lastCircle){ this.lastCircle = syncLastCircle } @@ -319,7 +468,8 @@ class Game{ var started = this.fadeOutStarted if(started){ var ms = this.elapsedTime - var musicDuration = this.controller.mainAsset.duration * 1000 - this.controller.offset + var duration = this.mainAsset ? this.mainAsset.duration : 0 + var musicDuration = duration * 1000 - this.controller.offset if(this.musicFadeOut === 0){ if(this.controller.multiplayer === 1){ p2.send("gameresults", this.getGlobalScore()) @@ -345,7 +495,7 @@ class Game{ playMainMusic(){ var ms = this.elapsedTime + this.controller.offset if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms < this.fadeOutStarted + 1600)){ - if(this.controller.multiplayer !== 2){ + if(this.controller.multiplayer !== 2 && this.mainAsset){ this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000)) } this.mainMusicPlaying = true @@ -356,12 +506,15 @@ class Game{ assets.sounds["se_pause"].play() this.paused = true this.latestDate = Date.now() - this.mainAsset.stop() + if(this.mainAsset){ + this.mainAsset.stop() + } this.mainMusicPlaying = false this.view.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() this.paused = false @@ -370,6 +523,7 @@ class Game{ this.sndTime = currentDate - snd.buffer.getTime() * 1000 this.view.gameDiv.classList.remove("game-paused") this.view.pointer() + pageEvents.send("unpause", currentDate - this.latestDate) } } isPaused(){ @@ -385,12 +539,13 @@ class Game{ this.sndTime = this.startDate - snd.buffer.getTime() * 1000 }else if(ms < 0 || ms >= 0 && this.started){ var currentDate = Date.now() - if(!this.controller.touchEnabled){ + if(this.gameLagSync){ var sndTime = currentDate - snd.buffer.getTime() * 1000 var lag = sndTime - this.sndTime if(Math.abs(lag) >= 50){ this.startDate += lag this.sndTime = sndTime + pageEvents.send("game-lag", lag) } } this.elapsedTime = currentDate - this.startDate @@ -407,7 +562,10 @@ class Game{ return this.songData.circles } updateCurrentCircle(){ - this.currentCircle++ + var circles = this.songData.circles + do{ + var circle = circles[++this.currentCircle] + }while(circle && circle.branch && !circle.branch.active) } getCurrentCircle(){ return this.currentCircle @@ -461,4 +619,59 @@ class Game{ } this.globalScore.points += Math.floor(score * multiplier / 10) * 10 } + setBranch(currentBranch, activeName){ + var pastActive = currentBranch.active + var ms = currentBranch.ms + for(var i = 0; i < this.songData.branches.length; i++){ + var branch = this.songData.branches[i] + if(branch.ms >= ms){ + var relevantName = activeName + var req = branch.requirement + var noNormal = req.advanced <= 0 + var noAdvanced = req.master <= 0 || req.advanced >= req.master || branch.type === "accuracy" && req.advanced > 100 + var noMaster = branch.type === "accuracy" && req.master > 100 + if(relevantName === "normal" && noNormal){ + relevantName = noAdvanced ? "master" : "advanced" + } + if(relevantName === "advanced" && noAdvanced){ + relevantName = noMaster ? "normal" : "master" + } + if(relevantName === "master" && noMaster){ + relevantName = noAdvanced ? "normal" : "advanced" + } + for(var j in this.branchNames){ + var name = this.branchNames[j] + if(name in branch){ + branch[name].active = name === relevantName + } + } + if(branch === currentBranch){ + activeName = relevantName + } + branch.active = relevantName + } + } + var circles = this.songData.circles + var circle = circles[this.currentCircle] + if(!circle || circle.branch === currentBranch[pastActive]){ + var ms = this.elapsedTime + var closestCircle = circles.findIndex(circle => { + return (!circle.branch || circle.branch.active) && circle.endTime >= ms + }) + if(closestCircle !== -1){ + this.currentCircle = closestCircle + } + } + this.HPGain = 100 / this.songData.circles.filter(circle => { + var type = circle.type + return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active) + }).length + if(this.controller.multiplayer === 1){ + p2.send("branch", activeName) + } + } + resetSection(){ + this.sectionNotes = [] + this.sectionDrumroll = 0 + } } diff --git a/public/src/js/gameinput.js b/public/src/js/gameinput.js new file mode 100644 index 0000000..4707652 --- /dev/null +++ b/public/src/js/gameinput.js @@ -0,0 +1,247 @@ +class GameInput{ + constructor(controller){ + this.controller = controller + this.game = this.controller.game + + this.keyboard = new Keyboard({ + ka_l: ["ka_l"], + don_l: ["don_l"], + don_r: ["don_r"], + ka_r: ["ka_r"], + pause: ["q", "esc"], + back: ["backspace"], + previous: ["left", "up"], + next: ["right", "down"], + confirm: ["enter", "space"] + }, this.keyPress.bind(this)) + this.keys = {} + this.waitKeyupScore = {} + this.waitKeyupSound = {} + this.waitKeyupMenu = {} + this.keyTime = { + "don": -Infinity, + "ka": -Infinity + } + this.keyboardEvents = 0 + + var layout = settings.getItem("gamepadLayout") + if(layout === "b"){ + var gameBtn = { + don_l: ["d", "r", "ls"], + don_r: ["a", "x", "rs"], + ka_l: ["u", "l", "lb", "lt"], + ka_r: ["b", "y", "rb", "rt"] + } + }else if(layout === "c"){ + var gameBtn = { + don_l: ["d", "l", "ls"], + don_r: ["a", "b", "rs"], + ka_l: ["u", "r", "lb", "lt"], + ka_r: ["x", "y", "rb", "rt"] + } + }else{ + var gameBtn = { + don_l: ["u", "d", "l", "r", "ls"], + don_r: ["a", "b", "x", "y", "rs"], + ka_l: ["lb", "lt"], + ka_r: ["rb", "rt"] + } + } + this.gamepad = new Gamepad(gameBtn) + this.gamepadInterval = setInterval(this.gamepadKeys.bind(this), 1000 / 60 / 2) + + this.gamepadMenu = new Gamepad({ + cancel: ["a"], + confirm: ["b", "ls", "rs"], + previous: ["u", "l", "lb", "lt", "lsu", "lsl"], + next: ["d", "r", "rb", "rt", "lsd", "lsr"], + pause: ["start"] + }) + + if(controller.multiplayer === 1){ + pageEvents.add(window, "beforeunload", event => { + if(p2.otherConnected){ + pageEvents.send("p2-abandoned", event) + } + }) + } + } + keyPress(pressed, name){ + if(!this.controller.autoPlayEnabled || this.game.isPaused() || name !== "don_l" && name !== "don_r" && name !== "ka_l" && name !== "ka_r"){ + this.setKey(pressed, name, this.game.getAccurateTime()) + } + } + checkGameKeys(){ + if(this.controller.autoPlayEnabled){ + this.checkKeySound("don_l", "don") + this.checkKeySound("don_r", "don") + this.checkKeySound("ka_l", "ka") + this.checkKeySound("ka_r", "ka") + } + } + gamepadKeys(){ + if(!this.game.isPaused() && !this.controller.autoPlayEnabled){ + this.gamepad.play((pressed, name) => { + if(pressed){ + if(this.keys[name]){ + this.setKey(false, name) + } + this.setKey(true, name, this.game.getAccurateTime()) + }else{ + this.setKey(false, name) + } + }) + } + } + checkMenuKeys(){ + if(!this.controller.multiplayer && !this.locked){ + var moveMenu = 0 + var ms = this.game.getAccurateTime() + this.gamepadMenu.play((pressed, name) => { + if(pressed){ + if(this.game.isPaused()){ + if(name === "cancel"){ + this.locked = true + return setTimeout(() => { + this.controller.togglePause() + this.locked = false + }, 200) + } + } + if(this.keys[name]){ + this.setKey(false, name) + } + this.setKey(true, name, ms) + }else{ + this.setKey(false, name) + } + }) + this.checkKey("pause", "menu", () => { + this.controller.togglePause() + for(var key in this.keyTime){ + this.keys[key] = null + this.keyTime[key] = -Infinity + } + }) + var moveMenuMinus = () => { + moveMenu = -1 + } + var moveMenuPlus = () => { + moveMenu = 1 + } + var moveMenuConfirm = () => { + if(this.game.isPaused()){ + this.locked = true + setTimeout(() => { + this.controller.view.pauseConfirm() + this.locked = false + }, 200) + } + } + this.checkKey("previous", "menu", moveMenuMinus) + this.checkKey("ka_l", "menu", moveMenuMinus) + this.checkKey("next", "menu", moveMenuPlus) + this.checkKey("ka_r", "menu", moveMenuPlus) + this.checkKey("confirm", "menu", moveMenuConfirm) + this.checkKey("don_l", "menu", moveMenuConfirm) + this.checkKey("don_r", "menu", moveMenuConfirm) + if(moveMenu && this.game.isPaused()){ + assets.sounds["se_ka"].play() + this.controller.view.pauseMove(moveMenu) + } + } + if(this.controller.multiplayer !== 2){ + this.checkKey("back", "menu", () => { + if(this.controller.multiplayer === 1 && p2.otherConnected){ + p2.send("gameend") + pageEvents.send("p2-abandoned") + } + this.controller.togglePause() + this.controller.songSelection() + }) + } + } + checkKey(name, type, callback){ + if(this.keys[name] && !this.isWaiting(name, type)){ + this.waitForKeyup(name, type) + callback() + } + } + checkKeySound(name, sound){ + this.checkKey(name, "sound", () => { + var circles = this.controller.getCircles() + var circle = circles[this.controller.getCurrentCircle()] + var currentTime = this.keyTime[name] + this.keyTime[sound] = currentTime + if(circle && !circle.isPlayed){ + if(circle.type === "balloon"){ + if(sound === "don" && circle.requiredHits - circle.timesHit <= 1){ + this.controller.playSound("se_balloon") + return + } + } + } + this.controller.playSound("neiro_1_" + sound) + }) + } + getKeys(){ + return this.keys + } + setKey(pressed, name, ms){ + if(pressed){ + this.keys[name] = true + this.waitKeyupScore[name] = false + this.waitKeyupSound[name] = false + this.waitKeyupMenu[name] = false + if(this.game.isPaused()){ + return + } + this.keyTime[name] = ms + if(name == "don_l" || name == "don_r"){ + this.checkKeySound(name, "don") + this.keyboardEvents++ + }else if(name == "ka_l" || name == "ka_r"){ + this.checkKeySound(name, "ka") + this.keyboardEvents++ + } + }else{ + this.keys[name] = false + this.waitKeyupScore[name] = false + this.waitKeyupSound[name] = false + this.waitKeyupMenu[name] = false + } + } + isWaiting(name, type){ + if(type === "score"){ + return this.waitKeyupScore[name] + }else if(type === "sound"){ + return this.waitKeyupSound[name] + }else if(type === "menu"){ + return this.waitKeyupMenu[name] + } + } + waitForKeyup(name, type){ + if(!this.keys[name]){ + return + } + if(type === "score"){ + this.waitKeyupScore[name] = true + }else if(type === "sound"){ + this.waitKeyupSound[name] = true + }else if(type === "menu"){ + this.waitKeyupMenu[name] = true + } + } + getKeyTime(){ + return this.keyTime + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + this.gamepadMenu.clean() + clearInterval(this.gamepadInterval) + if(this.controller.multiplayer === 1){ + pageEvents.remove(window, "beforeunload") + } + } +} diff --git a/public/src/js/gamepad.js b/public/src/js/gamepad.js index b6255c0..a4b3327 100644 --- a/public/src/js/gamepad.js +++ b/public/src/js/gamepad.js @@ -1,6 +1,7 @@ class Gamepad{ constructor(bindings, callback){ this.bindings = bindings + this.callback = !!callback this.b = { "a": 0, "b": 1, @@ -25,6 +26,7 @@ class Gamepad{ "lsl": "lsl" } this.btn = {} + this.gamepadEvents = 0 if(callback){ this.interval = setInterval(() => { this.play(callback) @@ -86,6 +88,9 @@ class Gamepad{ for(var name in bindings[bind]){ var bindName = bindings[bind][name] this.checkButton(gamepads, this.b[bindName], bind, callback, force[bindName]) + if(!this.b){ + return + } } } break @@ -123,6 +128,7 @@ class Gamepad{ if(pressed){ callback(true, keyCode) + this.gamepadEvents++ }else if(!button){ if(released){ this.toRelease[keyCode + "released"] = true @@ -134,6 +140,11 @@ class Gamepad{ } } clean(){ - clearInterval(this.interval) + if(this.callback){ + clearInterval(this.interval) + } + delete this.bindings + delete this.b + delete this.btn } } diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index bf669ea..112b675 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -29,6 +29,8 @@ this.otherFiles = {} this.songs = [] this.stylesheet = [] + this.songTitle = {} + this.uraRegex = /\s*[\((]裏[\))]$/ this.courseTypes = { "easy": 0, "normal": 1, @@ -82,7 +84,7 @@ file: file, index: i }) - }else if(name === "genre.ini" || name === "box.def"){ + }else if(name === "genre.ini" || name === "box.def" || name === "songtitle.txt"){ var level = (file.webkitRelativePath.match(/\//g) || []).length metaFiles.push({ file: file, @@ -154,6 +156,20 @@ break } } + }else if(name === "songtitle.txt"){ + var lastTitle + for(var i = 0; i < data.length; i++){ + var line = data[i].trim() + if(line){ + var lang = line.slice(0, 2) + if(line.charAt(2) !== " " || !(lang in allStrings)){ + this.songTitle[line] = {} + lastTitle = line + }else if(lastTitle){ + this.songTitle[lastTitle][lang] = line.slice(3).trim() + } + } + } } if(category){ var metaPath = file.webkitRelativePath.toLowerCase().slice(0, file.name.length * -1) @@ -168,7 +184,11 @@ this.osuFiles.forEach(filesLoop) } }).catch(() => {}) - reader.readAsText(file, "sjis") + if(name === "songtitle.txt"){ + reader.readAsText(file) + }else{ + reader.readAsText(file, "sjis") + } return promise } @@ -183,26 +203,27 @@ var songObj = { id: index + 1, type: "tja", - chart: data, - stars: [] + chart: file, + stars: [], + music: "muted" } + var titleLang = {} + var subtitleLang = {} var dir = file.webkitRelativePath.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) var hasCategory = false for(var diff in tja.metadata){ var meta = tja.metadata[diff] - songObj.title = songObj.title_en = meta.title || file.name.slice(0, file.name.lastIndexOf(".")) + songObj.title = meta.title || file.name.slice(0, file.name.lastIndexOf(".")) var subtitle = meta.subtitle || "" - if(subtitle.startsWith("--")){ - subtitle = subtitle.slice(2) - } - songObj.subtitle = songObj.subtitle_en = subtitle - songObj.preview = meta.demostart ? Math.floor(meta.demostart * 1000) : 0 - if(meta.level){ - songObj.stars[this.courseTypes[diff]] = meta.level + if(subtitle.startsWith("--") || subtitle.startsWith("++")){ + subtitle = subtitle.slice(2).trim() } + songObj.subtitle = subtitle + songObj.preview = meta.demostart || 0 + songObj.stars[this.courseTypes[diff]] = (meta.level || "0") + (meta.branch ? " B" : "") if(meta.wave){ - songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] + songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music } if(meta.genre){ songObj.category = this.categories[meta.genre.toLowerCase()] || meta.genre @@ -210,11 +231,44 @@ if(meta.taikowebskin){ songObj.song_skin = this.getSkin(dir, meta.taikowebskin) } + for(var id in allStrings){ + var songTitle = songObj.title + var ura = "" + if(songTitle){ + var uraPos = songTitle.search(this.uraRegex) + if(uraPos !== -1){ + ura = songTitle.slice(uraPos) + songTitle = songTitle.slice(0, uraPos) + } + } + if(meta["title" + id]){ + titleLang[id] = meta["title" + id] + }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ + titleLang[id] = this.songTitle[songTitle][id] + ura + } + if(meta["subtitle" + id]){ + subtitleLang[id] = meta["subtitle" + id] + } + } + } + var titleLangArray = [] + for(var id in titleLang){ + titleLangArray.push(id + " " + titleLang[id]) + } + if(titleLangArray.length !== 0){ + songObj.title_lang = titleLangArray.join("\n") + } + var subtitleLangArray = [] + for(var id in subtitleLang){ + subtitleLangArray.push(id + " " + subtitleLang[id]) + } + if(subtitleLangArray.length !== 0){ + songObj.subtitle_lang = subtitleLangArray.join("\n") } if(!songObj.category){ songObj.category = category || this.getCategory(file) } - if(songObj.music && songObj.stars.filter(star => star).length !== 0){ + if(songObj.stars.length !== 0){ this.songs[index] = songObj } }).catch(() => {}) @@ -235,12 +289,12 @@ var songObj = { id: index + 1, type: "osu", - chart: data, + chart: file, subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, - subtitle_en: osu.metadata.Artist || osu.metadata.ArtistUnicode, - preview: osu.generalInfo.PreviewTime, + subtitle_lang: osu.metadata.Artist || osu.metadata.ArtistUnicode, + preview: osu.generalInfo.PreviewTime / 1000, stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1], - music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] + music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted" } var filename = file.name.slice(0, file.name.lastIndexOf(".")) var title = osu.metadata.TitleUnicode || osu.metadata.Title @@ -251,13 +305,11 @@ suffix = " " + matches[0] } songObj.title = title + suffix - songObj.title_en = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix + songObj.title_lang = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix }else{ songObj.title = filename } - if(songObj.music){ - this.songs[index] = songObj - } + this.songs[index] = songObj songObj.category = category || this.getCategory(file) }).catch(() => {}) reader.readAsText(file) @@ -386,6 +438,7 @@ document.head.appendChild(style) } if(this.songs.length){ + var length = this.songs.length assets.songs = this.songs assets.customSongs = true assets.customSelected = 0 @@ -395,6 +448,7 @@ loader.screen.removeChild(this.loaderDiv) this.clean() new SongSelect("browse", false, this.songSelect.touchEnabled) + pageEvents.send("import-songs", length) }, 500) }else{ loader.screen.removeChild(this.loaderDiv) diff --git a/public/src/js/keyboard.js b/public/src/js/keyboard.js index 6d938ac..949673b 100644 --- a/public/src/js/keyboard.js +++ b/public/src/js/keyboard.js @@ -1,247 +1,105 @@ class Keyboard{ - constructor(controller){ - this.controller = controller - this.game = this.controller.game - - this.kbd = { - "don_l": 70, // F - "don_r": 74, // J - "ka_l": 68, // D - "ka_r": 75, // K - "pause": 81, // Q - "back": 8, // Backspace - "previous": 37, // Left - "next": 39, // Right - "confirm": 13 // Enter + constructor(bindings, callback){ + this.bindings = bindings + this.callback = callback + this.wildcard = false + this.substitute = { + "up": "arrowup", + "right": "arrowright", + "down": "arrowdown", + "left": "arrowleft", + "space": " ", + "esc": "escape", + "ctrl": "control", + "altgr": "altgraph" } - this.kbdAlias = { - "pause": [27], // Esc - "previous": [38], // Up - "next": [40], // Down - "confirm": [32] // Space - } - this.keys = {} - this.waitKeyupScore = {} - this.waitKeyupSound = {} - this.waitKeyupMenu = {} - this.keyTime = { - "don": -Infinity, - "ka": -Infinity - } - - var gameBtn = {} - gameBtn[this.kbd["don_l"]] = ["u", "d", "l", "r", "ls"] - gameBtn[this.kbd["don_r"]] = ["a", "b", "x", "y", "rs"] - gameBtn[this.kbd["ka_l"]] = ["lb", "lt"] - gameBtn[this.kbd["ka_r"]] = ["rb", "rt"] - this.gamepad = new Gamepad(gameBtn) - this.gamepadInterval = setInterval(this.gamepadKeys.bind(this), 1000 / 60 / 2) - - var menuBtn = { - "cancel": ["a"], - } - menuBtn[this.kbd["confirm"]] = ["b", "ls", "rs"] - menuBtn[this.kbd["previous"]] = ["u", "l", "lb", "lt", "lsu", "lsl"], - menuBtn[this.kbd["next"]] = ["d", "r", "rb", "rt", "lsd", "lsr"] - menuBtn[this.kbd["pause"]] = ["start"] - this.gamepadMenu = new Gamepad(menuBtn) - - this.kbdSearch = {} - for(var name in this.kbdAlias){ - var list = this.kbdAlias[name] - for(var i in list){ - this.kbdSearch[list[i]] = this.kbd[name] + this.btn = {} + this.update() + pageEvents.keyAdd(this, "all", "both", this.keyEvent.bind(this)) + pageEvents.blurAdd(this, this.blurEvent.bind(this)) + } + update(){ + var kbdSettings = settings.getItem("keyboardSettings") + var drumKeys = {} + for(var name in kbdSettings){ + var keys = kbdSettings[name] + for(var i in keys){ + drumKeys[keys[i]] = name } } - for(var name in this.kbd){ - this.kbdSearch[this.kbd[name]] = this.kbd[name] - } - - pageEvents.keyAdd(this, "all", "both", event => { - if(event.keyCode === 8){ - // Disable back navigation when pressing backspace - event.preventDefault() - } - var key = this.kbdSearch[event.keyCode] - if(key && !event.repeat && this.buttonEnabled(key)){ - var ms = this.game.getAccurateTime() - this.setKey(key, event.type === "keydown", ms) - } - }) - } - getBindings(){ - return this.kbd - } - buttonEnabled(keyCode){ - if(this.controller.autoPlayEnabled){ - switch(keyCode){ - case this.kbd["don_l"]: - case this.kbd["don_r"]: - case this.kbd["ka_l"]: - case this.kbd["ka_r"]: - return false - } - } - return true - } - checkGameKeys(){ - if(this.controller.autoPlayEnabled){ - this.checkKeySound(this.kbd["don_l"], "don") - this.checkKeySound(this.kbd["don_r"], "don") - this.checkKeySound(this.kbd["ka_l"], "ka") - this.checkKeySound(this.kbd["ka_r"], "ka") - } - } - gamepadKeys(){ - if(!this.game.isPaused() && !this.controller.autoPlayEnabled){ - this.gamepad.play((pressed, keyCode) => { - if(pressed){ - if(this.keys[keyCode]){ - this.setKey(keyCode, false) - } - this.setKey(keyCode, true, this.game.getAccurateTime()) - }else{ - this.setKey(keyCode, false) + this.kbd = {} + for(var name in this.bindings){ + var keys = this.bindings[name] + for(var i in keys){ + var key = keys[i] + if(key in drumKeys){ + continue } - }) - } - } - checkMenuKeys(){ - if(!this.controller.multiplayer && !this.locked){ - var moveMenu = 0 - var ms = this.game.getAccurateTime() - this.gamepadMenu.play((pressed, keyCode) => { - if(pressed){ - if(this.game.isPaused()){ - if(keyCode === "cancel"){ - this.locked = true - return setTimeout(() => { - this.controller.togglePause() - this.locked = false - }, 200) + if(key in kbdSettings){ + var keyArray = kbdSettings[key] + for(var j in keyArray){ + key = keyArray[j] + if(!(key in this.kbd)){ + this.kbd[key] = name } } - if(this.keys[keyCode]){ - this.setKey(keyCode, false) - } - this.setKey(keyCode, true, ms) }else{ - this.setKey(keyCode, false) - } - }) - this.checkKey(this.kbd["pause"], "menu", () => { - this.controller.togglePause() - for(var key in this.keyTime){ - this.keys[key] = null - this.keyTime[key] = -Infinity - } - }) - var moveMenuMinus = () => { - moveMenu = -1 - } - var moveMenuPlus = () => { - moveMenu = 1 - } - var moveMenuConfirm = () => { - if(this.game.isPaused()){ - this.locked = true - setTimeout(() => { - this.controller.view.pauseConfirm() - this.locked = false - }, 200) + if(key in this.substitute){ + key = this.substitute[key] + } + if(!(key in this.kbd)){ + if(key === "wildcard"){ + this.wildcard = true + } + this.kbd[key] = name + } } } - this.checkKey(this.kbd["previous"], "menu", moveMenuMinus) - this.checkKey(this.kbd["ka_l"], "menu", moveMenuMinus) - this.checkKey(this.kbd["next"], "menu", moveMenuPlus) - this.checkKey(this.kbd["ka_r"], "menu", moveMenuPlus) - this.checkKey(this.kbd["confirm"], "menu", moveMenuConfirm) - this.checkKey(this.kbd["don_l"], "menu", moveMenuConfirm) - this.checkKey(this.kbd["don_r"], "menu", moveMenuConfirm) - if(moveMenu && this.game.isPaused()){ - assets.sounds["se_ka"].play() - this.controller.view.pauseMove(moveMenu) - } - } - if(this.controller.multiplayer !== 2){ - this.checkKey(this.kbd["back"], "menu", () => { - if(this.controller.multiplayer === 1){ - p2.send("gameend") - } - this.controller.togglePause() - this.controller.songSelection() - }) } } - checkKey(keyCode, type, callback){ - if(this.keys[keyCode] && !this.isWaiting(keyCode, type)){ - this.waitForKeyup(keyCode, type) - callback() + keyEvent(event){ + var key = event.key.toLowerCase() + if(key === "escape" || key === "backspace" || key === "tab"){ + event.preventDefault() } - } - checkKeySound(keyCode, sound){ - this.checkKey(keyCode, "sound", () => { - var circles = this.controller.getCircles() - var circle = circles[this.controller.getCurrentCircle()] - if( - sound === "don" - && circle - && !circle.getPlayed() - && circle.getType() === "balloon" - && circle.requiredHits - circle.timesHit <= 1 - ){ - this.controller.playSound("se_balloon") + if(!event.repeat){ + var pressed = event.type === "keydown" + if(pressed){ + this.btn[key] = true }else{ - this.controller.playSound("neiro_1_" + sound) + delete this.btn[key] + if(key in this.kbd){ + for(var i in this.btn){ + if(this.kbd[i] === this.kbd[key]){ + return + } + } + } } - this.keyTime[sound] = this.keyTime[keyCode] - }) - } - getKeys(){ - return this.keys - } - setKey(keyCode, down, ms){ - if(down){ - this.keys[keyCode] = true - if(this.game.isPaused()){ - return + if(key in this.kbd){ + this.callback(pressed, this.kbd[key], event) + }else if(this.wildcard){ + this.callback(pressed, this.kbd["wildcard"], event) } - this.keyTime[keyCode] = ms - if(keyCode == this.kbd.don_l || keyCode == this.kbd.don_r){ - this.checkKeySound(keyCode, "don") - }else if(keyCode == this.kbd.ka_l || keyCode == this.kbd.ka_r){ - this.checkKeySound(keyCode, "ka") - } - }else{ - this.keys[keyCode] = false - this.waitKeyupScore[keyCode] = false - this.waitKeyupSound[keyCode] = false - this.waitKeyupMenu[keyCode] = false } } - isWaiting(keyCode, type){ - if(type === "score"){ - return this.waitKeyupScore[keyCode] - }else if(type === "sound"){ - return this.waitKeyupSound[keyCode] - }else if(type === "menu"){ - return this.waitKeyupMenu[keyCode] + blurEvent(){ + for(var key in this.btn){ + if(this.btn[key]){ + delete this.btn[key] + var name = this.kbd[key] || (this.wildcard ? "wildcard" : false) + if(name){ + this.callback(false, name) + } + } } } - waitForKeyup(keyCode, type){ - if(type === "score"){ - this.waitKeyupScore[keyCode] = true - }else if(type === "sound"){ - this.waitKeyupSound[keyCode] = true - }else if(type === "menu"){ - this.waitKeyupMenu[keyCode] = true - } - } - getKeyTime(){ - return this.keyTime - } clean(){ pageEvents.keyRemove(this, "all") - clearInterval(this.gamepadInterval) + pageEvents.blurRemove(this) + delete this.bindings + delete this.callback + delete this.kbd + delete this.btn } } diff --git a/public/src/js/loader.js b/public/src/js/loader.js index a2ce20d..a3ba450 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -25,6 +25,12 @@ class Loader{ var queryString = gameConfig._version.commit_short ? "?" + gameConfig._version.commit_short : "" + if(gameConfig.custom_js){ + var script = document.createElement("script") + this.addPromise(pageEvents.load(script)) + script.src = gameConfig.custom_js + queryString + document.head.appendChild(script) + } assets.js.forEach(name => { var script = document.createElement("script") this.addPromise(pageEvents.load(script)) @@ -32,7 +38,15 @@ class Loader{ document.head.appendChild(script) }) - this.addPromise(new Promise(resolve => { + this.addPromise(new Promise((resolve, reject) => { + if( + versionLink.href !== gameConfig._version.url && + gameConfig._version.commit && + versionLink.href.indexOf(gameConfig._version.commit) === -1 + ){ + // Version in the config does not match version on the page + reject() + } var cssCount = document.styleSheets.length + assets.css.length assets.css.forEach(name => { var stylesheet = document.createElement("link") @@ -112,6 +126,7 @@ class Loader{ 0.5 ) snd.sfxLoudGain.setVolume(1.2) + snd.buffer.saveSettings() this.afterJSCount = 0 @@ -147,17 +162,29 @@ class Loader{ } })) + var readyEvent = "normal" + var songId + var hashLower = location.hash.toLowerCase() p2 = new P2Connection() - if(location.hash.length === 6){ + if(hashLower.startsWith("#song=")){ + var number = parseInt(location.hash.slice(6)) + if(number > 0){ + songId = number + readyEvent = "song-id" + } + }else if(location.hash.length === 6){ p2.hashLock = true this.addPromise(new Promise(resolve => { p2.open() pageEvents.add(p2, "message", response => { if(response.type === "session"){ + pageEvents.send("session-start", "invited") + readyEvent = "session-start" resolve() }else if(response.type === "gameend"){ p2.hash("") p2.hashLock = false + readyEvent = "session-expired" resolve() } }) @@ -176,13 +203,17 @@ class Loader{ p2.hash("") } + settings = new Settings() + pageEvents.setKbd() + Promise.all(this.promises).then(() => { this.canvasTest.drawAllImages().then(result => { perf.allImg = result perf.load = Date.now() - this.startTime this.canvasTest.clean() this.clean() - this.callback() + this.callback(songId) + pageEvents.send("ready", readyEvent) }) }, this.errorMsg.bind(this)) @@ -192,7 +223,7 @@ class Loader{ } addPromise(promise){ this.promises.push(promise) - promise.then(this.assetLoaded.bind(this)) + promise.then(this.assetLoaded.bind(this), this.errorMsg.bind(this)) } loadSound(name, gain){ var id = this.getFilename(name) @@ -205,6 +236,7 @@ class Loader{ } errorMsg(error){ console.error(error) + pageEvents.send("loader-error", error) this.error = true this.loaderPercentage.appendChild(document.createElement("br")) this.loaderPercentage.appendChild(document.createTextNode("An error occurred, please refresh")) @@ -237,7 +269,9 @@ class Loader{ } clean(){ var fontDetectDiv = document.getElementById("fontdetectHelper") - fontDetectDiv.parentNode.removeChild(fontDetectDiv) + if(fontDetectDiv){ + fontDetectDiv.parentNode.removeChild(fontDetectDiv) + } delete this.loaderPercentage delete this.loaderProgress delete this.promises diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index 273217f..d70b799 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -4,6 +4,15 @@ class LoadSong{ this.autoPlayEnabled = autoPlayEnabled this.multiplayer = multiplayer this.touchEnabled = touchEnabled + var resolution = settings.getItem("resolution") + this.imgScale = 1 + if(resolution === "medium"){ + this.imgScale = 0.75 + }else if(resolution === "low"){ + this.imgScale = 0.5 + }else if(resolution === "lowest"){ + this.imgScale = 0.25 + } loader.changePage("loadsong", true) var loadingText = document.getElementById("loading-text") @@ -15,6 +24,12 @@ class LoadSong{ cancel.setAttribute("alt", strings.cancel) } this.run() + pageEvents.send("load-song", { + selectedSong: selectedSong, + autoPlayEnabled: autoPlayEnabled, + multiplayer: multiplayer, + touchEnabled: touchEnabled + }) } run(){ var song = this.selectedSong @@ -51,9 +66,10 @@ class LoadSong{ } if(type === "don"){ song.donBg = null - } - if(type === "song"){ + }else if(type === "song"){ song.songBg = null + }else if(type === "stage"){ + song.songStage = null } } } @@ -65,19 +81,14 @@ class LoadSong{ continue } let img = document.createElement("img") - if(!songObj.music && this.touchEnabled && imgLoad[i].type === "song"){ + let force = imgLoad[i].type === "song" && this.touchEnabled + if(!songObj.music && (this.imgScale !== 1 || force)){ img.crossOrigin = "Anonymous" } let promise = pageEvents.load(img) - if(imgLoad[i].type === "song"){ - promises.push(promise.then(() => { - return this.scaleImg(img, filename, prefix) - })) - }else{ - promises.push(promise.then(() => { - assets.image[prefix + filename] = img - })) - } + promises.push(promise.then(() => { + return this.scaleImg(img, filename, prefix, force) + })) if(songObj.music){ img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) }else{ @@ -91,32 +102,53 @@ class LoadSong{ if(songObj.sound){ songObj.sound.gain = snd.musicGain resolve() - }else if(songObj.music){ + }else if(!songObj.music){ + snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { + songObj.sound = sound + resolve() + }, reject) + }else if(songObj.music !== "muted"){ snd.musicGain.load(songObj.music, true).then(sound => { songObj.sound = sound resolve() }, reject) }else{ - snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.ogg").then(sound => { - songObj.sound = sound - resolve() - }, reject) + resolve() } })) if(songObj.chart){ - this.songData = 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 => { this.songData = data.replace(/\0/g, "").split("\n") })) } + if(this.touchEnabled && !assets.image["touch_drum"]){ + let img = document.createElement("img") + if(this.imgScale !== 1){ + img.crossOrigin = "Anonymous" + } + promises.push(pageEvents.load(img).then(() => { + return this.scaleImg(img, "touch_drum", "") + })) + img.src = gameConfig.assets_baseurl + "img/touch_drum.png" + } Promise.all(promises).then(() => { this.setupMultiplayer() }, error => { - console.error(error) if(Array.isArray(error) && error[1] instanceof HTMLElement){ error = error[0] + ": " + error[1].outerHTML } + console.error(error) + pageEvents.send("load-song-error", error) errorMessage(new Error(error).stack) alert("An error occurred, please refresh") }) @@ -134,23 +166,23 @@ class LoadSong{ filenames.push("bg_don2_" + this.selectedSong.donBg) } } + if(this.selectedSong.songStage !== null){ + filenames.push("bg_stage_" + this.selectedSong.songStage) + } for(var i = 0; i < filenames.length; i++){ - for(var letter = 0; letter < 2; letter++){ - let filenameAb = filenames[i] + (letter === 0 ? "a" : "b") + var filename = filenames[i] + var stage = filename.startsWith("bg_stage_") + for(var letter = 0; letter < (stage ? 1 : 2); letter++){ + let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b")) if(!(filenameAb in assets.image)){ let img = document.createElement("img") - if(filenameAb.startsWith("bg_song_")){ - if(this.touchEnabled){ - img.crossOrigin = "Anonymous" - } - promises.push(pageEvents.load(img).then(() => { - return this.scaleImg(img, filenameAb, "") - })) - }else{ - promises.push(pageEvents.load(img).then(() => { - assets.image[filenameAb] = img - })) + let force = filenameAb.startsWith("bg_song_") && this.touchEnabled + if(this.imgScale !== 1 || force){ + img.crossOrigin = "Anonymous" } + promises.push(pageEvents.load(img).then(() => { + return this.scaleImg(img, filenameAb, "", force) + })) img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png" } } @@ -158,12 +190,16 @@ class LoadSong{ Promise.all(promises).then(resolve, reject) }) } - scaleImg(img, filename, prefix){ + scaleImg(img, filename, prefix, force){ return new Promise((resolve, reject) => { - if(this.touchEnabled){ + var scale = this.imgScale + if(force && scale > 0.5){ + scale = 0.5 + } + if(scale !== 1){ var canvas = document.createElement("canvas") - var w = Math.floor(img.width / 2) - var h = Math.floor(img.height / 2) + var w = Math.floor(img.width * scale) + var h = Math.floor(img.height * scale) canvas.width = w canvas.height = h var ctx = canvas.getContext("2d") @@ -243,6 +279,7 @@ class LoadSong{ var taikoGame1 = new Controller(song, this.songData, false, 1, this.touchEnabled) var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled) taikoGame1.run(taikoGame2) + pageEvents.send("load-song-player2", this.selectedSong2) }else if(event.type === "left" || event.type === "gameend"){ this.clean() new SongSelect(false, false, this.touchEnabled) @@ -264,6 +301,7 @@ class LoadSong{ }else{ if(!repeat){ assets.sounds["v_sanka"].play() + pageEvents.send("load-song-unfocused") } setTimeout(() => { this.startMultiplayer(true) @@ -281,6 +319,7 @@ class LoadSong{ p2.send("leave") assets.sounds["se_don"].play() this.cancelButton.style.pointerEvents = "none" + pageEvents.send("load-song-cancel") } clean(){ pageEvents.remove(p2, "message") diff --git a/public/src/js/logo.js b/public/src/js/logo.js index 44f98d7..e77f992 100644 --- a/public/src/js/logo.js +++ b/public/src/js/logo.js @@ -2,7 +2,7 @@ constructor(){ this.canvas = document.getElementById("logo") this.ctx = this.canvas.getContext("2d") - this.pathSvg = failedTests.indexOf("Path2D SVG") === -1 + this.pathSvg = failedTests.indexOf("Path2D SVG") === -1 && vectors.logo1 this.symbolFont = "TnT, Meiryo, sans-serif" this.symbols = [{ x: 315, y: 18, xAlt: 15, scale: true, text: "ブ", diff --git a/public/src/js/main.js b/public/src/js/main.js index 1ba7a91..e9067f1 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -82,6 +82,7 @@ var perf = { } var strings var vectors +var settings pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ @@ -90,8 +91,12 @@ pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { }) var versionDiv = document.getElementById("version") var versionLink = document.getElementById("version-link") -pageEvents.add(versionDiv, ["click", "touchend"], () => { - versionLink.click() +versionLink.tabIndex = -1 +pageEvents.add(versionDiv, ["click", "touchend"], event => { + if(event.target === versionDiv){ + versionLink.click() + pageEvents.send("version-link") + } }) resizeRoot() setInterval(resizeRoot, 100) @@ -103,7 +108,9 @@ pageEvents.keyAdd(debugObj, "all", "down", event => { }else if(debugObj.state === "minimised"){ debugObj.debug.restore() }else{ - debugObj.debug = new Debug() + try{ + debugObj.debug = new Debug() + }catch(e){} } } if(event.keyCode === 82 && debugObj.debug && debugObj.controller){ @@ -112,7 +119,7 @@ pageEvents.keyAdd(debugObj, "all", "down", event => { } }) -var loader = new Loader(() => { - new Titlescreen() +var loader = new Loader(songId => { + new Titlescreen(songId) }) diff --git a/public/src/js/mekadon.js b/public/src/js/mekadon.js index cca80e2..a58247e 100644 --- a/public/src/js/mekadon.js +++ b/public/src/js/mekadon.js @@ -6,22 +6,25 @@ class Mekadon{ this.lastHit = -Infinity } play(circle){ - var type = circle.getType() - if((type === "balloon" || type === "drumroll" || type === "daiDrumroll") && this.getMS() > circle.getEndTime()){ + var type = circle.type + if((type === "balloon" || type === "drumroll" || type === "daiDrumroll") && this.getMS() > circle.endTime){ + if(circle.section && circle.timesHit === 0){ + this.game.resetSection() + } circle.played(-1, false) this.game.updateCurrentCircle() } - type = circle.getType() + type = circle.type if(type === "balloon"){ - this.playDrumrollAt(circle, 0, 30) + return this.playDrumrollAt(circle, 0, 30) }else if(type === "drumroll" || type === "daiDrumroll"){ - this.playDrumrollAt(circle, 0, 60) + return this.playDrumrollAt(circle, 0, 60) }else{ - this.playAt(circle, 0, 450) + return this.playAt(circle, 0, 450) } } playAt(circle, ms, score, dai, reverse){ - var currentMs = circle.getMS() - this.getMS() + var currentMs = circle.ms - this.getMS() if(ms > currentMs - 10){ return this.playNow(circle, score, dai, reverse) } @@ -32,22 +35,22 @@ class Mekadon{ if(kaAmount > 0){ score = Math.random() > kaAmount ? 1 : 2 } - this.playAt(circle, ms, score) + return this.playAt(circle, ms, score) } } miss(circle){ - var currentMs = circle.getMS() - this.getMS() + var currentMs = circle.ms - this.getMS() if(0 >= currentMs - 10){ this.controller.displayScore(0, true) this.game.updateCurrentCircle() this.game.updateCombo(0) this.game.updateGlobalScore(0, 1, circle.gogoTime) + this.game.sectionNotes.push(0) return true } } playNow(circle, score, dai, reverse){ - var kbd = this.controller.getBindings() - var type = circle.getType() + var type = circle.type var keyDai = false var playDai = !dai || dai === 2 var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" @@ -55,7 +58,7 @@ class Mekadon{ if(drumrollNotes){ var ms = this.getMS() }else{ - var ms = circle.getMS() + var ms = circle.ms } if(reverse){ @@ -65,25 +68,25 @@ class Mekadon{ type = "don" } } - if(type == "daiDon" && playDai){ - this.setKey(kbd["don_l"], ms) - this.setKey(kbd["don_r"], ms) + if(type === "daiDon" && playDai){ + this.setKey("don_l", ms) + this.setKey("don_r", ms) this.lr = false keyDai = true - }else if(type == "don" || type == "daiDon" || drumrollNotes && score !== 2){ - this.setKey(this.lr ? kbd["don_l"] : kbd["don_r"], ms) + }else if(type === "don" || type === "daiDon" || drumrollNotes && score !== 2){ + this.setKey(this.lr ? "don_l" : "don_r", ms) this.lr = !this.lr - }else if(type == "daiKa" && playDai){ - this.setKey(kbd["ka_l"], ms) - this.setKey(kbd["ka_r"], ms) + }else if(type === "daiKa" && playDai){ + this.setKey("ka_l", ms) + this.setKey("ka_r", ms) this.lr = false keyDai = true - }else if(type == "ka" || type == "daiKa" || drumrollNotes){ - this.setKey(this.lr ? kbd["ka_l"] : kbd["ka_r"], ms) + }else if(type === "ka" || type === "daiKa" || drumrollNotes){ + this.setKey(this.lr ? "ka_l" : "ka_r", ms) this.lr = !this.lr } if(type === "balloon"){ - if(circle.requiredHits == 1){ + if(circle.requiredHits === 1){ assets.sounds["se_balloon"].play() } this.game.checkBalloon(circle) @@ -95,6 +98,10 @@ class Mekadon{ this.game.updateGlobalScore(score, keyDai ? 2 : 1, circle.gogoTime) this.game.updateCurrentCircle() circle.played(score, keyDai) + if(circle.section){ + this.game.resetSection() + } + this.game.sectionNotes.push(score === 450 ? 1 : (score === 230 ? 0.5 : 0)) } this.lastHit = ms return true @@ -102,8 +109,7 @@ class Mekadon{ getMS(){ return this.controller.getElapsedTime() } - setKey(keyCode, ms){ - this.controller.setKey(keyCode, false) - this.controller.setKey(keyCode, true, ms) + setKey(name, ms){ + this.controller.setKey(true, name, ms) } } diff --git a/public/src/js/p2.js b/public/src/js/p2.js index b635cb1..a0c266d 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -58,6 +58,7 @@ class P2Connection{ this.open() } }, 500) + pageEvents.send("p2-disconnected") } var addedType = this.allEvents.get("close") if(addedType){ @@ -108,9 +109,15 @@ class P2Connection{ this.dai = 2 this.kaAmount = 0 this.results = false + this.branch = "normal" break case "gameend": this.otherConnected = false + if(this.session){ + pageEvents.send("session-end") + }else if(!this.results){ + pageEvents.send("p2-game-end") + } this.session = false if(this.hashLock){ this.hash("") @@ -135,6 +142,10 @@ class P2Connection{ this.kaAmount = response.value.kaAmount } break + case "branch": + this.branch = response.value + this.branchSet = false + break case "session": this.clearMessage("users") this.otherConnected = true @@ -155,10 +166,10 @@ class P2Connection{ } play(circle, mekadon){ if(this.otherConnected || this.notes.length > 0){ - var type = circle.getType() + var type = circle.type var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" - if(drumrollNotes && mekadon.getMS() > circle.getEndTime()){ + if(drumrollNotes && mekadon.getMS() > circle.endTime){ circle.played(-1, false) mekadon.game.updateCurrentCircle() } @@ -171,7 +182,7 @@ class P2Connection{ var note = this.notes[0] if(note.score >= 0){ var dai = 1 - if(circle.getType() === "daiDon" || circle.getType() === "daiKa"){ + if(circle.type === "daiDon" || circle.type === "daiKa"){ dai = this.dai } if(mekadon.playAt(circle, note.ms, note.score, dai, note.reverse)){ diff --git a/public/src/js/pageevents.js b/public/src/js/pageevents.js index 789275e..c8c0af7 100644 --- a/public/src/js/pageevents.js +++ b/public/src/js/pageevents.js @@ -3,10 +3,13 @@ class PageEvents{ this.allEvents = new Map() this.keyListeners = new Map() this.mouseListeners = new Map() + this.blurListeners = new Map() this.lastKeyEvent = -Infinity this.add(window, "keydown", this.keyEvent.bind(this)) this.add(window, "keyup", this.keyEvent.bind(this)) this.add(window, "mousemove", this.mouseEvent.bind(this)) + this.add(window, "blur", this.blurEvent.bind(this)) + this.kbd = [] } add(target, type, callback){ if(Array.isArray(type)){ @@ -81,8 +84,9 @@ class PageEvents{ }) } keyEvent(event){ - if ([68, 70, 74, 75].indexOf(event.keyCode) > -1) { // D, F, J, K + if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){ this.lastKeyEvent = Date.now() + event.preventDefault() } this.keyListeners.forEach(addedKeyCode => { this.checkListener(addedKeyCode.get("all"), event) @@ -140,7 +144,29 @@ class PageEvents{ mouseRemove(target){ this.mouseListeners.delete(target) } + blurEvent(event){ + this.blurListeners.forEach(callback => callback(event)) + } + blurAdd(target, callback){ + this.blurListeners.set(target, callback) + } + blurRemove(target){ + this.blurListeners.delete(target) + } getMouse(){ return this.lastMouse } + send(name, detail){ + dispatchEvent(new CustomEvent(name, {detail: detail})) + } + setKbd(){ + this.kbd = [] + var kbdSettings = settings.getItem("keyboardSettings") + for(var name in kbdSettings){ + var keys = kbdSettings[name] + for(var i in keys){ + this.kbd.push(keys[i]) + } + } + } } diff --git a/public/src/js/parseosu.js b/public/src/js/parseosu.js index 336bc70..b8d94e2 100644 --- a/public/src/js/parseosu.js +++ b/public/src/js/parseosu.js @@ -181,7 +181,8 @@ class ParseOsu{ measures.push({ ms: ms, originalMS: ms, - speed: speed + speed: speed, + visible: true }) } } diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index 0055b99..2b10f9b 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -10,18 +10,20 @@ this.difficulty = difficulty this.offset = (offset || 0) * -1000 this.soundOffset = 0 - this.noteTypes = [ - {name: false, txt: false}, - {name: "don", txt: strings.note.don}, - {name: "ka", txt: strings.note.ka}, - {name: "daiDon", txt: strings.note.daiDon}, - {name: "daiKa", txt: strings.note.daiKa}, - {name: "drumroll", txt: strings.note.drumroll}, - {name: "daiDrumroll", txt: strings.note.daiDrumroll}, - {name: "balloon", txt: strings.note.balloon}, - {name: false, txt: false}, - {name: "balloon", txt: strings.note.balloon} - ] + this.noteTypes = { + "0": {name: false, txt: false}, + "1": {name: "don", txt: strings.note.don}, + "2": {name: "ka", txt: strings.note.ka}, + "3": {name: "daiDon", txt: strings.note.daiDon}, + "4": {name: "daiKa", txt: strings.note.daiKa}, + "5": {name: "drumroll", txt: strings.note.drumroll}, + "6": {name: "daiDrumroll", txt: strings.note.daiDrumroll}, + "7": {name: "balloon", txt: strings.note.balloon}, + "8": {name: false, txt: false}, + "9": {name: "balloon", txt: strings.note.balloon}, + "A": {name: "daiDon", txt: strings.note.daiDon}, + "B": {name: "daiKa", txt: strings.note.daiKa} + } this.courseTypes = { "0": "easy", "1": "normal", @@ -44,7 +46,7 @@ var hasSong = false var courses = {} var currentCourse = {} - var courseName = this.difficulty + var courseName = "oni" for(var lineNum = 0; lineNum < this.data.length; lineNum++){ var line = this.data[lineNum] @@ -55,11 +57,13 @@ inSong = true if(!hasSong){ + if(!(courseName in courses)){ + courses[courseName] = {} + } for(var name in currentCourse){ - if(!(courseName in courses)){ - courses[courseName] = {} + if(name !== "branch"){ + courses[courseName][name] = currentCourse[name] } - courses[courseName][name] = currentCourse[name] } courses[courseName].start = lineNum + 1 courses[courseName].end = this.data.length @@ -70,6 +74,8 @@ hasSong = true courses[courseName].end = lineNum } + }else if(name.startsWith("branchstart") && inSong){ + courses[courseName].branch = true } }else if(!inSong){ @@ -114,10 +120,7 @@ parseCircles(){ var meta = this.metadata[this.difficulty] var ms = (meta.offset || 0) * -1000 + this.offset - var bpm = meta.bpm || 0 - if(bpm <= 0){ - bpm = 1 - } + var bpm = Math.abs(meta.bpm) || 120 var scroll = 1 var measure = 4 this.beatInfo.beatInterval = 60000 / bpm @@ -128,29 +131,36 @@ var balloons = meta.balloon || [] var lastDrumroll = false + var branch = false - var branchType - var branchPreference = "m" + var branchObj = {} + var currentBranch = false + var branchSettings = {} + var branchFirstMeasure = false + var sectionBegin = true var currentMeasure = [] var firstNote = true var circles = [] var circleID = 0 + var regexAZ = /[A-Z]/ var pushMeasure = () => { - if(barLine){ - var note = currentMeasure[0] - if(note){ - var speed = note.bpm * note.scroll / 60 - }else{ - var speed = bpm * scroll / 60 - } - this.measures.push({ - ms: ms, - originalMS: ms, - speed: speed - }) + var note = currentMeasure[0] + if(note){ + var speed = note.bpm * note.scroll / 60 + }else{ + var speed = bpm * scroll / 60 } + this.measures.push({ + ms: ms, + originalMS: ms, + speed: speed, + visible: barLine, + branch: currentBranch, + branchFirst: branchFirstMeasure + }) + branchFirstMeasure = false if(currentMeasure.length){ for(var i = 0; i < currentMeasure.length; i++){ var note = currentMeasure[i] @@ -182,7 +192,9 @@ gogoTime: note.gogo, endTime: note.endTime, requiredHits: note.requiredHits, - beatMS: 60000 / note.bpm + beatMS: 60000 / note.bpm, + branch: currentBranch, + section: note.section }) if(lastDrumroll === note){ lastDrumroll = circleObj @@ -204,61 +216,115 @@ var line = line.slice(1).toLowerCase() var [name, value] = this.split(line, " ") - if(!branch || branch && branchType === branchPreference){ - switch(name){ - case "gogostart": - gogo = true - break - case "gogoend": - gogo = false - break - case "bpmchange": - bpm = parseFloat(value) - break - case "scroll": - scroll = parseFloat(value) - break - case "measure": - var [numerator, denominator] = value.split("/") - measure = numerator / denominator * 4 - break - case "delay": - ms += (parseFloat(value) || 0) * 1000 - break - case "barlineon": - barLine = true - break - case "barlineoff": - barLine = false - break - } - } switch(name){ + case "gogostart": + gogo = true + break + case "gogoend": + gogo = false + break + case "bpmchange": + bpm = parseFloat(value) || bpm + break + case "scroll": + scroll = Math.abs(parseFloat(value)) || scroll + break + case "measure": + var [numerator, denominator] = value.split("/") + measure = numerator / denominator * 4 || measure + break + case "delay": + ms += (parseFloat(value) || 0) * 1000 + break + case "barlineon": + barLine = true + break + case "barlineoff": + barLine = false + break case "branchstart": branch = true - branchType = "" + currentBranch = false + branchFirstMeasure = true + branchSettings = { + ms: ms, + gogo: gogo, + bpm: bpm, + scroll: scroll, + sectionBegin: sectionBegin + } value = value.split(",") - var forkType = value[0].toLowerCase() - if(forkType === "r" || parseFloat(value[2]) <= 100){ - branchPreference = "m" - }else if(parseFloat(value[1]) <= 100){ - branchPreference = "e" + if(!this.branches){ + this.branches = [] + } + var req = { + advanced: parseFloat(value[1]) || 0, + master: parseFloat(value[2]) || 0 + } + if(req.advanced > 0){ + var active = req.master > 0 ? "normal" : "master" }else{ - branchPreference = "n" + var active = req.master > 0 ? "advanced" : "master" + } + branchObj = { + ms: ms, + originalMS: ms, + active: active, + type: value[0].trim().toLowerCase() === "r" ? "drumroll" : "accuracy", + requirement: req + } + this.branches.push(branchObj) + if(this.measures.length === 1 && branchObj.type === "drumroll"){ + for(var i = circles.length; i--;){ + var circle = circles[i] + if(circle.endTime && circle.type === "drumroll" || circle.type === "daiDrumroll" || circle.type === "balloon"){ + this.measures.push({ + ms: circle.endTime, + originalMS: circle.endTime, + speed: circle.bpm * circle.scroll / 60, + visible: false, + branch: circle.branch + }) + break + } + } + } + if(this.measures.length !== 0){ + this.measures[this.measures.length - 1].nextBranch = branchObj } break case "branchend": - case "section": branch = false + currentBranch = false + break + case "section": + sectionBegin = true + if(branch && !currentBranch){ + branchSettings.sectionBegin = true + } break case "n": case "e": case "m": - branchType = name + if(!branch){ + break + } + ms = branchSettings.ms + gogo = branchSettings.gogo + bpm = branchSettings.bpm + scroll = branchSettings.scroll + sectionBegin = branchSettings.sectionBegin + branchFirstMeasure = true + var branchName = name === "m" ? "master" : (name === "e" ? "advanced" : "normal") + currentBranch = { + name: branchName, + active: branchName === branchObj.active + } + branchObj[branchName] = currentBranch break } - }else if(!branch || branch && branchType === branchPreference){ + }else{ - var string = line.split("") + var string = line.toUpperCase().split("") for(let symbol of string){ @@ -271,15 +337,17 @@ scroll: scroll }) break - case "1": case "2": case "3": case "4": + case "1": case "2": case "3": case "4": case "A": case "B": var type = this.noteTypes[symbol] var circleObj = { type: type.name, txt: type.txt, gogo: gogo, bpm: bpm, - scroll: scroll + scroll: scroll, + section: sectionBegin } + sectionBegin = false if(lastDrumroll){ circleObj.endDrumroll = lastDrumroll lastDrumroll = false @@ -293,15 +361,19 @@ txt: type.txt, gogo: gogo, bpm: bpm, - scroll: scroll + scroll: scroll, + section: sectionBegin } + sectionBegin = false if(lastDrumroll){ if(symbol === "9"){ currentMeasure.push({ endDrumroll: lastDrumroll, bpm: bpm, - scroll: scroll + scroll: scroll, + section: sectionBegin }) + sectionBegin = false lastDrumroll = false }else{ currentMeasure.push({ @@ -327,8 +399,10 @@ currentMeasure.push({ endDrumroll: lastDrumroll, bpm: bpm, - scroll: scroll + scroll: scroll, + section: sectionBegin }) + sectionBegin = false lastDrumroll = false }else{ currentMeasure.push({ @@ -342,7 +416,14 @@ currentMeasure = [] break default: - error = true + if(regexAZ.test(symbol)){ + currentMeasure.push({ + bpm: bpm, + scroll: scroll + }) + }else{ + error = true + } break } @@ -359,6 +440,11 @@ lastDrumroll.originalEndTime = ms } + if(this.branches){ + circles.sort((a, b) => a.ms > b.ms ? 1 : -1) + this.measures.sort((a, b) => a.ms > b.ms ? 1 : -1) + circles.forEach((circle, i) => circle.id = i + 1) + } return circles } } diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index 35f93c1..24fde5d 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -31,9 +31,12 @@ class Scoresheet{ this.draw = new CanvasDraw() this.canvasCache = new CanvasCache() + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "esc", "don_l", "don_r"] + }, this.keyDown.bind(this)) this.gamepad = new Gamepad({ - "13": ["a", "b", "start", "ls", "rs"] - }) + confirm: ["a", "b", "start", "ls", "rs"] + }, this.keyDown.bind(this)) this.difficulty = { "easy": 0, @@ -60,24 +63,20 @@ class Scoresheet{ } }) } + pageEvents.send("scoresheet", { + selectedSong: controller.selectedSong, + autoPlayEnabled: controller.autoPlayEnabled, + multiplayer: multiplayer, + touchEnabled: touchEnabled, + results: this.results, + p2results: multiplayer ? p2.results : null, + keyboardEvents: controller.keyboard.keyboardEvents, + gamepadEvents: controller.keyboard.gamepad.gamepadEvents, + touchEvents: controller.view.touchEvents + }) } - keyDown(event, code){ - if(!code){ - if(event.repeat){ - return - } - code = event.keyCode - } - var key = { - confirm: code == 13 || code == 32 || code == 70 || code == 74, - // Enter, Space, F, J - cancel: code == 27 || code == 8 - // Esc, Backspace - } - if(key.cancel && event){ - event.preventDefault() - } - if(key.confirm || key.cancel){ + keyDown(pressed){ + if(pressed && this.redrawing){ this.toNext() } } @@ -126,7 +125,6 @@ class Scoresheet{ this.winW = null this.winH = null - pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this)) pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this)) if(!this.multiplayer){ @@ -166,12 +164,6 @@ class Scoresheet{ } var ms = this.getMS() - this.gamepad.play((pressed, keyCode) => { - if(pressed){ - this.keyDown(false, keyCode) - } - }) - if(!this.redrawRunning){ return } @@ -182,6 +174,14 @@ class Scoresheet{ var winW = innerWidth var winH = lastHeight this.pixelRatio = window.devicePixelRatio || 1 + var resolution = settings.getItem("resolution") + if(resolution === "medium"){ + this.pixelRatio *= 0.75 + }else if(resolution === "low"){ + this.pixelRatio *= 0.5 + }else if(resolution === "lowest"){ + this.pixelRatio *= 0.25 + } winW *= this.pixelRatio winH *= this.pixelRatio var ratioX = winW / 1280 @@ -842,12 +842,13 @@ class Scoresheet{ } clean(){ + this.keyboard.clean() + this.gamepad.clean() this.draw.clean() this.canvasCache.clean() assets.sounds["bgm_result"].stop() - snd.musicGain.fadeIn() + snd.buffer.loadSettings() this.redrawRunning = false - pageEvents.keyRemove(this, "all") pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) if(this.multiplayer !== 2 && this.touchEnabled){ pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") diff --git a/public/src/js/session.js b/public/src/js/session.js index fb967c9..0f08c67 100644 --- a/public/src/js/session.js +++ b/public/src/js/session.js @@ -2,13 +2,13 @@ class Session{ constructor(touchEnabled){ this.touchEnabled = touchEnabled loader.changePage("session", true) - this.endButton = document.getElementById("tutorial-end-button") + this.endButton = this.getElement("view-end-button") if(touchEnabled){ - document.getElementById("tutorial-outer").classList.add("touch-enabled") + this.getElement("view-outer").classList.add("touch-enabled") } this.sessionInvite = document.getElementById("session-invite") - var tutorialTitle = document.getElementById("tutorial-title") + var tutorialTitle = this.getElement("view-title") tutorialTitle.innerText = strings.session.multiplayerSession tutorialTitle.setAttribute("alt", strings.session.multiplayerSession) this.sessionInvite.parentNode.insertBefore(document.createTextNode(strings.session.linkTutorial), this.sessionInvite) @@ -16,11 +16,12 @@ class Session{ this.endButton.setAttribute("alt", strings.session.cancel) pageEvents.add(window, ["mousedown", "touchstart"], this.mouseDown.bind(this)) - pageEvents.keyOnce(this, 27, "down").then(this.onEnd.bind(this)) - + this.keyboard = new Keyboard({ + confirm: ["esc"] + }, this.keyPress.bind(this)) this.gamepad = new Gamepad({ - "confirm": ["start", "b", "ls", "rs"] - }, this.onEnd.bind(this)) + confirm: ["start", "b", "ls", "rs"] + }, this.keyPress.bind(this)) p2.hashLock = true pageEvents.add(p2, "message", response => { @@ -29,12 +30,20 @@ class Session{ p2.hash(response.value) }else if(response.type === "songsel"){ p2.clearMessage("users") - this.onEnd(false, true) + this.onEnd(true) + pageEvents.send("session-start", "host") } }) p2.send("invite") + pageEvents.send("session") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] } mouseDown(event){ + if(event.type === "mousedown" && event.which !== 1){ + return + } if(event.target === this.sessionInvite){ this.sessionInvite.focus() }else{ @@ -45,17 +54,20 @@ class Session{ this.onEnd() } } - onEnd(event, fromP2){ + keyPress(pressed){ + if(pressed){ + this.onEnd() + } + } + onEnd(fromP2){ if(!p2.session){ p2.send("leave") p2.hash("") p2.hashLock = false + pageEvents.send("session-cancel") }else if(!fromP2){ return p2.send("songsel") } - if(event && event.type === "keydown"){ - event.preventDefault() - } this.clean() assets.sounds["se_don"].play() setTimeout(() => { @@ -63,9 +75,9 @@ class Session{ }, 500) } clean(){ + this.keyboard.clean() this.gamepad.clean() pageEvents.remove(window, ["mousedown", "touchstart"]) - pageEvents.keyRemove(this, 27) pageEvents.remove(p2, "message") delete this.endButton delete this.sessionInvite diff --git a/public/src/js/settings.js b/public/src/js/settings.js new file mode 100644 index 0000000..3c78e6f --- /dev/null +++ b/public/src/js/settings.js @@ -0,0 +1,528 @@ +class Settings{ + constructor(){ + var ios = /iPhone|iPad/.test(navigator.userAgent) + var phone = /Android|iPhone|iPad/.test(navigator.userAgent) + + this.items = { + language: { + type: "language", + options: ["ja", "en", "cn", "tw", "ko"], + default: this.getLang() + }, + resolution: { + type: "select", + options: ["high", "medium", "low", "lowest"], + default: phone ? "medium" : "high" + }, + touchAnimation: { + type: "toggle", + default: !ios, + touch: true + }, + keyboardSettings: { + type: "keyboard", + default: { + ka_l: ["d"], + don_l: ["f"], + don_r: ["j"], + ka_r: ["k"] + }, + touch: false + }, + gamepadLayout: { + type: "gamepad", + options: ["a", "b", "c"], + default: "a", + gamepad: true + } + } + + this.storage = {} + try{ + var storage = JSON.parse(localStorage.getItem("settings") || "{}") + for(var i in this.items){ + var current = this.items[i] + if(current.type === "language"){ + this.storage[i] = localStorage.getItem("lang") + if(current.options.indexOf(this.storage[i]) === -1){ + this.storage[i] = null + } + }else if(i in storage){ + if((current.type === "select" || current.type === "gamepad") && current.options.indexOf(storage[i]) === -1){ + this.storage[i] = null + }else if(current.type === "keyboard"){ + var obj = {} + for(var j in current.default){ + if(storage[i] && storage[i][j] && storage[i][j][0]){ + obj[j] = storage[i][j] + }else{ + obj = null + break + } + } + this.storage[i] = obj + }else{ + this.storage[i] = storage[i] + } + }else{ + this.storage[i] = null + } + } + }catch(e){ + for(var i in this.items){ + this.storage[i] = null + } + } + } + getItem(name){ + var value = this.storage[name] + return value === null ? this.items[name].default : value + } + setItem(name, value){ + this.storage[name] = value + try{ + if(name === "language"){ + if(value){ + localStorage.setItem("lang", value) + }else{ + localStorage.removeItem("lang") + } + }else{ + var language = this.storage.language + delete this.storage.language + localStorage.setItem("settings", JSON.stringify(this.storage)) + this.storage.language = language + } + }catch(e){} + } + getLang(){ + if("languages" in navigator){ + var userLang = navigator.languages.slice() + userLang.unshift(navigator.language) + for(var i in userLang){ + for(var j in allStrings){ + if(allStrings[j].regex.test(userLang[i])){ + return j + } + } + } + } + return "ja" + } + setLang(lang, noEvent){ + strings = lang + var boldFonts = strings.font === "Microsoft YaHei, sans-serif" + loader.screen.style.fontFamily = strings.font + loader.screen.style.fontWeight = boldFonts ? "bold" : "" + loader.screen.classList[boldFonts ? "add" : "remove"]("bold-fonts") + if(!noEvent){ + pageEvents.send("language-change", lang.id) + } + } +} + +class SettingsView{ + constructor(touchEnabled, tutorial, songId){ + this.touchEnabled = touchEnabled + this.tutorial = tutorial + this.songId = songId + + loader.changePage("settings", tutorial) + assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992) + this.defaultButton = document.getElementById("settings-default") + if(touchEnabled){ + this.getElement("view-outer").classList.add("touch-enabled") + } + var gamepadEnabled = false + if("getGamepads" in navigator){ + var gamepads = navigator.getGamepads() + for(var i = 0; i < gamepads.length; i++){ + if(gamepads[i]){ + gamepadEnabled = true + break + } + } + } + this.mode = "settings" + + this.keyboard = new Keyboard({ + "confirm": ["enter", "space", "don_l", "don_r"], + "up": ["up"], + "previous": ["left", "ka_l"], + "next": ["right", "down", "ka_r"], + "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"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) + + this.viewTitle = this.getElement("view-title") + this.endButton = this.getElement("view-end-button") + this.resolution = settings.getItem("resolution") + + var content = this.getElement("view-content") + this.items = [] + this.selected = 0 + for(let i in settings.items){ + var current = settings.items[i] + if( + !touchEnabled && current.touch === true || + touchEnabled && current.touch === false || + !gamepadEnabled && current.gamepad === true + ){ + continue + } + 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[i].name + nameDiv.innerText = name + nameDiv.setAttribute("alt", 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){ + settingBox.classList.add("selected") + } + this.addTouch(settingBox, event => this.setValue(i)) + this.items.push({ + id: i, + settingBox: settingBox, + nameDiv: nameDiv, + valueDiv: valueDiv + }) + } + this.items.push({ + id: "default", + settingBox: this.defaultButton + }) + this.addTouch(this.defaultButton, this.defaultSettings.bind(this)) + this.items.push({ + id: "back", + settingBox: this.endButton + }) + this.addTouch(this.endButton, this.onEnd.bind(this)) + + this.gamepadSettings = document.getElementById("settings-gamepad") + this.addTouch(this.gamepadSettings, event => { + if(event.target === event.currentTarget){ + this.gamepadBack() + } + }) + this.gamepadTitle = this.gamepadSettings.getElementsByClassName("view-title")[0] + this.gamepadEndButton = this.gamepadSettings.getElementsByClassName("view-end-button")[0] + this.addTouch(this.gamepadEndButton, event => this.gamepadBack(true)) + this.gamepadBox = this.gamepadSettings.getElementsByClassName("setting-box")[0] + this.addTouch(this.gamepadBox, event => this.gamepadSet(1)) + this.gamepadButtons = document.getElementById("gamepad-buttons") + this.gamepadValue = document.getElementById("gamepad-value") + + this.setStrings() + + pageEvents.send("settings") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + addTouch(element, callback){ + pageEvents.add(element, ["mousedown", "touchstart"], event => { + if(event.type === "touchstart"){ + event.preventDefault() + this.touched = true + }else if(event.which !== 1){ + return + }else{ + this.touched = false + } + callback(event) + }) + } + removeTouch(element){ + pageEvents.remove(element, ["mousedown", "touchstart"]) + } + getValue(name, valueDiv){ + var current = settings.items[name] + var value = settings.getItem(name) + if(current.type === "language"){ + value = allStrings[value].name + " (" + value + ")" + }else if(current.type === "select" || current.type === "gamepad"){ + value = strings.settings[name][value] + }else if(current.type === "toggle"){ + value = value ? strings.settings.on : strings.settings.off + }else if(current.type === "keyboard"){ + valueDiv.innerHTML = "" + for(var i in value){ + var keyDiv = document.createElement("div") + keyDiv.style.color = i === "ka_l" || i === "ka_r" ? "#009aa5" : "#ef2c10" + var key = value[i][0] + for(var j in this.keyboard.substitute){ + if(this.keyboard.substitute[j] === key){ + key = j + break + } + } + keyDiv.innerText = key.toUpperCase() + valueDiv.appendChild(keyDiv) + } + return + } + valueDiv.innerText = value + } + setValue(name){ + var current = settings.items[name] + var value = settings.getItem(name) + var selectedIndex = this.items.findIndex(item => item.id === name) + var selected = this.items[selectedIndex] + if(this.mode !== "settings"){ + if(this.selected === selectedIndex){ + this.keyboardBack(selected) + } + return + } + if(this.selected !== selectedIndex){ + this.items[this.selected].settingBox.classList.remove("selected") + this.selected = selectedIndex + selected.settingBox.classList.add("selected") + } + if(current.type === "language" || current.type === "select"){ + value = current.options[this.mod(current.options.length, current.options.indexOf(value) + 1)] + }else if(current.type === "toggle"){ + value = !value + }else if(current.type === "keyboard"){ + this.mode = "keyboard" + selected.settingBox.style.animation = "none" + selected.valueDiv.classList.add("selected") + this.keyboardKeys = {} + this.keyboardSet() + assets.sounds["se_don"].play() + return + }else if(current.type === "gamepad"){ + this.mode = "gamepad" + this.gamepadSelected = current.options.indexOf(value) + this.gamepadSet() + assets.sounds["se_don"].play() + return + } + settings.setItem(name, value) + this.getValue(name, this.items[this.selected].valueDiv) + assets.sounds["se_ka"].play() + if(current.type === "language"){ + this.setLang(allStrings[value]) + } + } + keyPressed(pressed, name, event){ + if(!pressed){ + return + } + this.touched = false + var selected = this.items[this.selected] + if(this.mode === "settings"){ + if(name === "confirm"){ + if(selected.id === "back"){ + this.onEnd() + }else if(selected.id === "default"){ + this.defaultSettings() + }else{ + this.setValue(selected.id) + } + }else if(name === "up" || name === "previous" || name === "next"){ + 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") + selected = this.items[this.selected] + selected.settingBox.classList.add("selected") + selected.settingBox.scrollIntoView() + assets.sounds["se_ka"].play() + }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 === "back"){ + this.gamepadBack() + } + }else if(this.mode === "keyboard"){ + if(name === "back"){ + this.keyboardBack(selected) + assets.sounds["se_cancel"].play() + }else{ + event.preventDefault() + var currentKey = event.key.toLowerCase() + for(var i in this.keyboardKeys){ + if(this.keyboardKeys[i][0] === currentKey || !currentKey){ + return + } + } + var current = this.keyboardCurrent + assets.sounds[current === "ka_l" || current === "ka_r" ? "se_ka" : "se_don"].play() + this.keyboardKeys[current] = [currentKey] + this.keyboardSet() + } + } + } + keyboardSet(){ + var selected = this.items[this.selected] + var current = settings.items[selected.id] + selected.valueDiv.innerHTML = "" + for(var i in current.default){ + var keyDiv = document.createElement("div") + keyDiv.style.color = i === "ka_l" || i === "ka_r" ? "#009aa5" : "#ef2c10" + if(this.keyboardKeys[i]){ + var key = this.keyboardKeys[i][0] + for(var j in this.keyboard.substitute){ + if(this.keyboard.substitute[j] === key){ + key = j + break + } + } + keyDiv.innerText = key.toUpperCase() + selected.valueDiv.appendChild(keyDiv) + }else{ + keyDiv.innerText = "[" + strings.settings[selected.id][i] + "]" + selected.valueDiv.appendChild(keyDiv) + this.keyboardCurrent = i + return + } + } + settings.setItem(selected.id, this.keyboardKeys) + this.keyboardBack(selected) + this.keyboard.update() + pageEvents.setKbd() + } + keyboardBack(selected){ + this.mode = "settings" + selected.settingBox.style.animation = "" + selected.valueDiv.classList.remove("selected") + this.getValue(selected.id, selected.valueDiv) + } + gamepadSet(diff){ + if(this.mode !== "gamepad"){ + return + } + var selected = this.items[this.selected] + var current = settings.items[selected.id] + if(diff){ + this.gamepadSelected = this.mod(current.options.length, this.gamepadSelected + diff) + assets.sounds["se_ka"].play() + } + 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" + } + gamepadBack(confirm){ + if(this.mode !== "gamepad"){ + return + } + var selected = this.items[this.selected] + 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.gamepadSettings.style.display = "" + this.mode = "settings" + } + defaultSettings(){ + if(this.mode === "keyboard"){ + this.keyboardBack(this.items[this.selected]) + } + for(var i in settings.items){ + settings.setItem(i, null) + } + this.setLang(allStrings[settings.getItem("language")]) + this.keyboard.update() + pageEvents.setKbd() + assets.sounds["se_don"].play() + } + onEnd(){ + this.clean() + assets.sounds["se_don"].play() + setTimeout(() => { + if(this.tutorial && !this.touched){ + new Tutorial(false, this.songId) + }else{ + try{ + localStorage.setItem("tutorial", "true") + }catch(e){} + new SongSelect(this.tutorial ? false : "settings", false, this.touched, this.songId) + } + }, 500) + } + setLang(lang){ + settings.setLang(lang) + if(failedTests.length !== 0){ + showUnsupported(strings) + } + for(var i in this.items){ + 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.getValue(item.id, item.valueDiv) + } + } + 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) + } + mod(length, index){ + return ((index % length) + length) % length + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + assets.sounds["bgm_settings"].stop() + for(var i in this.items){ + this.removeTouch(this.items[i].settingBox) + } + if(this.defaultButton){ + delete this.defaultButton + } + this.removeTouch(this.gamepadSettings) + this.removeTouch(this.gamepadEndButton) + this.removeTouch(this.gamepadBox) + delete this.tutorialTitle + delete this.endButton + delete this.items + delete this.gamepadSettings + delete this.gamepadTitle + delete this.gamepadEndButton + delete this.gamepadBox + delete this.gamepadButtons + delete this.gamepadValue + 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_")){ + URL.revokeObjectURL(assets.image[i].src) + delete assets.image[i] + } + } + } + } +} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 6147122..fd8a399 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -1,5 +1,5 @@ class SongSelect{ - constructor(fromTutorial, fadeIn, touchEnabled){ + constructor(fromTutorial, fadeIn, touchEnabled, songId){ this.touchEnabled = touchEnabled loader.changePage("songselect", false) @@ -25,21 +25,27 @@ class SongSelect{ }, "tutorial": { sort: 7, - background: "#9afbe1", - border: ["#d6ffff", "#6bae9c"], - outline: "#31ae94" + background: "#29e8aa", + border: ["#86ffbd", "#009a8c"], + outline: "#08a28c" }, "about": { sort: 7, - background: "#91cfff", - border: ["#dff0ff", "#6890b2"], - outline: "#217abb" + background: "#a2d0e7", + border: ["#c6dfff", "#4485d9"], + outline: "#2390d9" + }, + "settings": { + sort: 7, + background: "#ce93fa", + border: ["#dec4fd", "#a543ef"], + outline: "#a741ef" }, "browse": { sort: 7, - background: "#9791ff", - border: ["#e2dfff", "#6d68b2"], - outline: "#5350ba" + background: "#fab5d3", + border: ["#ffe7ef", "#d36aa2"], + outline: "#d36aa2" }, "J-POP": { sort: 0, @@ -107,7 +113,8 @@ class SongSelect{ type: song.type, offset: song.offset, songSkin: song.song_skin || {}, - music: song.music + music: song.music, + volume: song.volume }) } this.songs.sort((a, b) => { @@ -148,6 +155,12 @@ class SongSelect{ action: "about", category: strings.random }) + this.songs.push({ + title: strings.gameSettings, + skin: this.songSkin.settings, + action: "settings", + category: strings.random + }) if("webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent))){ this.browse = document.getElementById("browse") pageEvents.add(this.browse, "change", this.browseChange.bind(this)) @@ -213,23 +226,32 @@ class SongSelect{ this.selectedDiff = 0 assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) - if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage)){ + if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){ fromTutorial = touchEnabled ? "about" : "tutorial" } if(p2.session){ fromTutorial = false } + var songIdIndex = -1 if(fromTutorial){ this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial) this.playBgm(true) }else{ - if(assets.customSongs){ + if(songId){ + songIdIndex = this.songs.findIndex(song => song.id === songId) + if(songIdIndex === -1){ + this.clearHash() + } + } + if(songIdIndex !== -1){ + this.selectedSong = songIdIndex + }else if(assets.customSongs){ this.selectedSong = assets.customSelected }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) } - assets.sounds["v_songsel"].play() + assets.sounds[songIdIndex !== -1 ? "v_diffsel" : "v_songsel"].play() snd.musicGain.fadeOut() this.playBgm(false) } @@ -247,7 +269,7 @@ class SongSelect{ var skipStart = fromTutorial || p2.session this.state = { - screen: fadeIn ? "titleFadeIn" : (skipStart ? "song" : "title"), + screen: songIdIndex !== -1 ? "difficulty" : (fadeIn ? "titleFadeIn" : (skipStart ? "song" : "title")), screenMS: this.getMS(), move: 0, moveMS: 0, @@ -267,23 +289,33 @@ class SongSelect{ this.startPreview(true) this.pressedKeys = {} + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + back: ["escape"], + left: ["left", "ka_l"], + right: ["right", "ka_r"], + up: ["up"], + down: ["down"], + session: ["backspace"], + ctrl: ["ctrl"], + shift: ["shift"] + }, this.keyPress.bind(this)) this.gamepad = new Gamepad({ - "13": ["b", "start", "ls", "rs"], - "27": ["a"], - "37": ["l", "lb", "lt", "lsl"], - "39": ["r", "rb", "rt", "lsr"], - "38": ["u", "lsu"], - "40": ["d", "lsd"], - "8": ["back"], - "ctrl": ["y"], - "shift": ["x"] - }) + confirm: ["b", "start", "ls", "rs"], + back: ["a"], + left: ["l", "lb", "lt", "lsl"], + right: ["r", "rb", "rt", "lsr"], + up: ["u", "lsu"], + down: ["d", "lsd"], + session: ["back"], + ctrl: ["y"], + shift: ["x"] + }, this.keyPress.bind(this)) if(!assets.customSongs){ this.startP2() } - pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this)) pageEvents.add(loader.screen, "mousemove", this.mouseMove.bind(this)) pageEvents.add(loader.screen, "mouseleave", () => { this.state.moveHover = null @@ -302,73 +334,54 @@ class SongSelect{ this.redrawRunning = true this.redrawBind = this.redraw.bind(this) this.redraw() + pageEvents.send("song-select") + pageEvents.send("song-select-move", this.songs[this.selectedSong]) + if(songIdIndex !== -1){ + pageEvents.send("song-select-difficulty", this.songs[this.selectedSong]) + } } - keyDown(event, code){ - if(code){ - var modifiers = { - shift: this.pressedKeys["shift"], - ctrl: this.pressedKeys["ctrl"] + keyPress(pressed, name, event){ + if(pressed){ + if(!this.pressedKeys[name]){ + this.pressedKeys[name] = this.getMS() + 300 } }else{ - code = event.keyCode - var modifiers = { - shift: event.shiftKey, - ctrl: event.ctrlKey - } - } - if(code === "ctrl" || code === "shift" || !this.redrawRunning){ + this.pressedKeys[name] = 0 return } - - var key = { - confirm: code == 13 || code == 32 || code == 70 || code == 74, - // Enter, Space, F, J - cancel: code == 27, - // Esc - session: code == 8, - // Backspace - left: code == 37 || code == 68, - // Left, D - right: code == 39 || code == 75, - // Right, K - up: code == 38, - // Up - down: code == 40 - // Down - } - if(key.cancel && event){ - event.preventDefault() + if(name === "ctrl" || name === "shift" || !this.redrawRunning){ + return } if(this.state.screen === "song"){ - if(key.confirm){ + if(name === "confirm"){ this.toSelectDifficulty() - }else if(key.cancel){ + }else if(name === "back"){ this.toTitleScreen() - }else if(key.session){ + }else if(name === "session"){ this.toSession() - }else if(key.left){ + }else if(name === "left"){ this.moveToSong(-1) - }else if(key.right){ + }else if(name === "right"){ this.moveToSong(1) } }else if(this.state.screen === "difficulty"){ - if(key.confirm){ + if(name === "confirm"){ if(this.selectedDiff === 0){ this.toSongSelect() }else if(this.selectedDiff === 1){ this.toOptions(1) }else{ - this.toLoadSong(this.selectedDiff - this.diffOptions.length, modifiers.shift, modifiers.ctrl) + this.toLoadSong(this.selectedDiff - this.diffOptions.length, this.pressedKeys["shift"], this.pressedKeys["ctrl"]) } - }else if(key.cancel || key.session){ + }else if(name === "back" || name === "session"){ this.toSongSelect() - }else if(key.left){ + }else if(name === "left"){ this.moveToDiff(-1) - }else if(key.right){ + }else if(name === "right"){ this.moveToDiff(1) - }else if(this.selectedDiff === 1 && (key.up || key.down)){ - this.toOptions(key.up ? -1 : 1) + }else if(this.selectedDiff === 1 && (name === "up" || name === "down")){ + this.toOptions(name === "up" ? -1 : 1) } } } @@ -508,7 +521,7 @@ class SongSelect{ }else if(550 < x && x < 1050 && 95 < y && y < 524){ var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length var currentSong = this.songs[this.selectedSong] - if(this.state.ura && moveBy === this.diffOptions + 3 || currentSong.stars[moveBy - this.diffOptions.length]){ + if(this.state.ura && moveBy === this.diffOptions.length + 3 || currentSong.stars[moveBy - this.diffOptions.length]){ return moveBy } } @@ -590,6 +603,7 @@ class SongSelect{ assets.sounds["se_don"].play() assets.sounds["v_songsel"].stop() assets.sounds["v_diffsel"].play(0.3) + pageEvents.send("song-select-difficulty", currentSong) }else if(currentSong.action === "back"){ this.clean() this.toTitleScreen() @@ -603,10 +617,13 @@ class SongSelect{ setTimeout(() => { this.moveToSong(moveBy) }, 200) + pageEvents.send("song-select-random") }else if(currentSong.action === "tutorial"){ this.toTutorial() }else if(currentSong.action === "about"){ this.toAbout() + }else if(currentSong.action === "settings"){ + this.toSettings() }else if(currentSong.action === "browse"){ this.toBrowse() } @@ -630,6 +647,8 @@ class SongSelect{ assets.sounds["v_diffsel"].stop() assets.sounds["se_cancel"].play() } + this.clearHash() + pageEvents.send("song-select-back") } toLoadSong(difficulty, shift, ctrl, touch){ this.clean() @@ -703,6 +722,13 @@ class SongSelect{ new About(this.touchEnabled) }, 500) } + toSettings(){ + assets.sounds["se_don"].play() + this.clean() + setTimeout(() => { + new SettingsView(this.touchEnabled) + }, 500) + } toSession(){ if(p2.socket.readyState !== 1 || assets.customSongs){ return @@ -728,6 +754,7 @@ class SongSelect{ setTimeout(() => { new SongSelect("browse", false, this.touchEnabled) }, 500) + pageEvents.send("import-songs-default") }else{ this.browse.click() } @@ -740,20 +767,10 @@ class SongSelect{ requestAnimationFrame(this.redrawBind) var ms = this.getMS() - this.gamepad.play((pressed, keyCode) => { - if(pressed){ - if(!this.pressedKeys[keyCode]){ - this.pressedKeys[keyCode] = ms + 300 - this.keyDown(false, keyCode) - } - }else{ - this.pressedKeys[keyCode] = 0 - } - }) for(var key in this.pressedKeys){ if(this.pressedKeys[key]){ if(ms >= this.pressedKeys[key] + 50){ - this.keyDown(false, key) + this.keyPress(true, key) this.pressedKeys[key] = ms } } @@ -770,6 +787,14 @@ class SongSelect{ winW = winH / 9 * 32 } this.pixelRatio = window.devicePixelRatio || 1 + var resolution = settings.getItem("resolution") + if(resolution === "medium"){ + this.pixelRatio *= 0.75 + }else if(resolution === "low"){ + this.pixelRatio *= 0.5 + }else if(resolution === "lowest"){ + this.pixelRatio *= 0.25 + } winW *= this.pixelRatio winH *= this.pixelRatio var ratioX = winW / 1280 @@ -783,9 +808,11 @@ class SongSelect{ this.canvas.style.height = (winH / this.pixelRatio) + "px" var borders = (this.songAsset.border + this.songAsset.innerBorder) * 2 + var songsLength = Math.ceil(winW / ratio / (this.songAsset.width + this.songAsset.marginLeft)) + 1 + this.songTitleCache.resize( - (this.songAsset.width - borders + 1) * Math.ceil(this.songs.length / 3), - (this.songAsset.height - borders + 1) * 3, + (this.songAsset.width - borders + 1) * songsLength, + this.songAsset.height - borders + 1, ratio + 0.2 ) @@ -807,7 +834,7 @@ class SongSelect{ categories++ } }) - this.categoryCache.resize(280, (this.songAsset.marginTop + 1) * categories , ratio + 0.5) + this.categoryCache.resize(280, this.songAsset.marginTop + 1 , ratio + 0.5) this.difficultyCache.resize((44 + 56 + 2) * 5, 135 + 10, ratio + 0.5) @@ -952,7 +979,11 @@ class SongSelect{ var elapsed = ms - this.state.moveMS if(this.state.move && ms > this.state.moveMS + resize2 - scrollDelay){ assets.sounds["se_ka"].play() + var previousSelectedSong = this.selectedSong this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move) + if(previousSelectedSong !== this.selectedSong){ + pageEvents.send("song-select-move", this.songs[this.selectedSong]) + } this.state.move = 0 this.state.locked = 2 @@ -1299,27 +1330,47 @@ class SongSelect{ outlineSize: currentUra ? this.songAsset.letterBorder : 0 }) }) - var songStars = currentUra ? currentSong.stars[4] : currentSong.stars[i] - for(var j = 0; j < 10; j++){ - if(songSel){ - var yPos = _y + 113 + j * 17 - }else{ - var yPos = _y + 178 + j * 19.5 - } - if(10 - j > songStars){ - ctx.fillStyle = currentUra ? "#187085" : (songSel ? "#e97526" : "#e7e7e7") - ctx.beginPath() - ctx.arc(_x, yPos, songSel ? 4.5 : 5, 0, Math.PI * 2) - ctx.fill() - }else{ - this.draw.diffStar({ - ctx: ctx, - songSel: songSel, - ura: currentUra, - x: _x, - y: yPos, - ratio: ratio - }) + var songStarsArray = (currentUra ? currentSong.stars[4] : currentSong.stars[i]).toString().split(" ") + var songStars = songStarsArray[0] + var songBranch = songStarsArray[1] === "B" + var elapsedMS = this.state.screenMS > this.state.moveMS || !songSel ? this.state.screenMS : this.state.moveMS + var fade = ((ms - elapsedMS) % 2000) / 2000 + if(songBranch && fade > 0.25 && fade < 0.75){ + this.draw.verticalText({ + ctx: ctx, + text: strings.songBranch, + x: _x, + y: _y + (songSel ? 110 : 185), + width: songSel ? 44 : 56, + height: songSel ? 160 : 170, + fill: songSel && !currentUra ? "#c85200" : "#fff", + fontSize: songSel ? 25 : 27, + fontFamily: songSel ? "Meiryo, Microsoft YaHei, sans-serif" : this.font, + outline: songSel ? false : "#f22666", + outlineSize: songSel ? 0 : this.songAsset.letterBorder + }) + }else{ + for(var j = 0; j < 10; j++){ + if(songSel){ + var yPos = _y + 113 + j * 17 + }else{ + var yPos = _y + 178 + j * 19.5 + } + if(10 - j > songStars){ + ctx.fillStyle = currentUra ? "#187085" : (songSel ? "#e97526" : "#e7e7e7") + ctx.beginPath() + ctx.arc(_x, yPos, songSel ? 4.5 : 5, 0, Math.PI * 2) + ctx.fill() + }else{ + this.draw.diffStar({ + ctx: ctx, + songSel: songSel, + ura: currentUra, + x: _x, + y: yPos, + ratio: ratio + }) + } } } var currentDiff = this.selectedDiff - this.diffOptions.length @@ -1687,55 +1738,45 @@ class SongSelect{ if("id" in currentSong){ var startLoad = this.getMS() if(loadOnly){ - var resolveLoading - this.previewLoading = currentSong.id + var currentId = null }else{ + var currentId = this.previewId this.previewing = this.selectedSong - if(this.previewLoading === currentSong.id){ - this.previewLoading = null - return - } } - var currentId = this.previewId var songObj = this.previewList.find(song => song && song.id === id) if(songObj){ if(!loadOnly){ this.preview = songObj.preview_sound this.preview.gain = snd.previewGain - this.previewLoaded(startLoad, songObj.preview_time) + this.previewLoaded(startLoad, songObj.preview_time, currentSong.volume) } }else{ songObj = {id: id} - var previewFilename = prvTime > 0.1 ? "/preview.mp3" : "/main.ogg" + var previewFilename = prvTime > 0 ? "/preview.mp3" : "/main.ogg" var loadPreview = previewFilename => { return snd.previewGain.load(gameConfig.songs_baseurl + id + previewFilename) } new Promise((resolve, reject) => { - if(currentSong.music){ - songObj.preview_time = prvTime - snd.previewGain.load(currentSong.music, true).then(resolve, reject) - }else{ + if(!currentSong.music){ songObj.preview_time = 0 loadPreview(previewFilename).catch(() => { songObj.preview_time = prvTime return loadPreview("/main.ogg") }).then(resolve, reject) + }else if(currentSong.music !== "muted"){ + songObj.preview_time = prvTime + snd.previewGain.load(currentSong.music, true).then(resolve, reject) } }).then(sound => { if(currentId === this.previewId){ songObj.preview_sound = sound - if(!loadOnly || !this.previewLoading){ - this.previewing = this.selectedSong - this.preview = sound - this.previewLoaded(startLoad, songObj.preview_time) - } - if(loadOnly){ - this.previewLoading = null - } + this.preview = sound + this.previewLoaded(startLoad, songObj.preview_time, currentSong.volume) + var oldPreview = this.previewList.shift() if(oldPreview){ oldPreview.preview_sound.clean() @@ -1748,12 +1789,13 @@ class SongSelect{ } } } - previewLoaded(startLoad, prvTime){ + previewLoaded(startLoad, prvTime, volume){ var endLoad = this.getMS() var difference = endLoad - startLoad var minDelay = 300 var delay = minDelay - Math.min(minDelay, difference) - this.preview.playLoop(delay / 1000, false, prvTime / 1000) + snd.previewGain.setVolumeMul(volume || 1) + this.preview.playLoop(delay / 1000, false, prvTime) } endPreview(){ this.previewId++ @@ -1880,22 +1922,32 @@ class SongSelect{ return title } + clearHash(){ + if(location.hash.toLowerCase().startsWith("#song=")){ + p2.hash("") + } + } + getMS(){ return Date.now() } clean(){ + this.keyboard.clean() + this.gamepad.clean() + this.clearHash() this.draw.clean() this.songTitleCache.clean() this.selectTextCache.clean() this.categoryCache.clean() this.difficultyCache.clean() this.sessionCache.clean() + this.currentSongCache.clean() assets.sounds["bgm_songsel"].stop() if(!this.bgmEnabled){ snd.musicGain.fadeIn() setTimeout(() => { - snd.musicGain.fadeIn() + snd.buffer.loadSettings() }, 500) } this.redrawRunning = false @@ -1905,7 +1957,6 @@ class SongSelect{ song.preview_sound.clean() } }) - pageEvents.keyRemove(this, "all") pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"]) pageEvents.remove(this.canvas, "touchend") pageEvents.remove(p2, "message") diff --git a/public/src/js/soundbuffer.js b/public/src/js/soundbuffer.js index 0262a6b..3e6f45e 100644 --- a/public/src/js/soundbuffer.js +++ b/public/src/js/soundbuffer.js @@ -3,6 +3,7 @@ var AudioContext = window.AudioContext || window.webkitAudioContext this.context = new AudioContext() pageEvents.add(window, ["click", "touchend"], this.pageClicked.bind(this)) + this.gainList = [] } load(url, local, gain){ if(local){ @@ -27,7 +28,9 @@ }) } createGain(channel){ - return new SoundGain(this, channel) + var gain = new SoundGain(this, channel) + this.gainList.push(gain) + return gain } setCrossfade(gain1, gain2, median){ if(!Array.isArray(gain1)){ @@ -60,6 +63,18 @@ this.context.resume() } } + saveSettings(){ + for(var i = 0; i < this.gainList.length; i++){ + var gain = this.gainList[i] + gain.defaultVol = gain.volume + } + } + loadSettings(){ + for(var i = 0; i < this.gainList.length; i++){ + var gain = this.gainList[i] + gain.setVolume(gain.defaultVol) + } + } } class SoundGain{ constructor(soundBuffer, channel){ @@ -85,8 +100,11 @@ class SoundGain{ this.gainNode.gain.value = amount * amount this.volume = amount } + setVolumeMul(amount){ + this.setVolume(amount * this.defaultVol) + } setCrossfade(amount){ - this.setVolume(Math.pow(Math.sin(Math.PI / 2 * amount), 1 / 4)) + this.setVolume(Math.sqrt(Math.sin(Math.PI / 2 * amount))) } fadeIn(duration, time, absolute){ this.fadeVolume(0, this.volume * this.volume, duration, time, absolute) diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 510a36c..eb77015 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -1,7 +1,7 @@ function StringsJa(){ this.id = "ja" this.name = "日本語" - this.regex = /^ja$/ + this.regex = /^ja$|^ja-/ this.font = "TnT, Meiryo, sans-serif" this.taikoWeb = "たいこウェブ" @@ -24,6 +24,7 @@ this.randomSong = "ランダムに曲をえらぶ" this.howToPlay = "あそびかた説明" this.aboutSimulator = "このシミュレータについて" + this.gameSettings = "ゲーム設定" this.browse = "参照する…" this.defaultSongList = "デフォルト曲リスト" this.songOptions = "演奏オプション" @@ -34,6 +35,7 @@ this.normal = "ふつう" this.hard = "むずかしい" this.oni = "おに" + this.songBranch = "譜面分岐あり" this.sessionStart = "オンラインセッションを開始する!" this.sessionEnd = "オンラインセッションを終了する" this.loading = "ロード中..." @@ -53,6 +55,11 @@ this.good = "良" this.ok = "可" this.bad = "不可" + this.branch = { + "normal": "普通譜面", + "advanced": "玄人譜面", + "master": "達人譜面" + } this.pauseOptions = [ "演奏をつづける", "はじめからやりなおす", @@ -65,16 +72,16 @@ this.tutorial = { basics: [ - "Hit the drum when the notes reach the taiko!", - "For red notes, hit the face of the drum (%s or %s)...", - "...and for blue notes, hit the rim! (%s or %s)", - "USB controllers are also supported!" + "流れてくる音符がワクに重なったらバチで太鼓をたたこう!", + "赤い音符は面をたたこう(%sまたは%s)", + "青い音符はフチをたたこう(%sまたは%s)", + "USBコントローラがサポートされています!" ], - otherControls: "Other controls", + otherControls: "他のコントロール", otherTutorial: [ - "%s \u2014 pause game", - "%s while selecting difficulty \u2014 enable autoplay mode", - "%s while selecting difficulty \u2014 enable 2P mode" + "%sはゲームを一時停止します", + "むずかしさをえらぶしながら%sキーを押しながらオートモードを有効", + "むずかしさをえらぶしながら%sキーを押しながらネットプレイモードを有効" ], ok: "OK" } @@ -92,6 +99,38 @@ linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", cancel: "キャンセル" } + this.settings = { + language: { + name: "言語" + }, + resolution: { + name: "ゲームの解像度", + high: "高", + medium: "中", + low: "低", + lowest: "最低" + }, + touchAnimation: { + name: "タッチアニメーション" + }, + keyboardSettings: { + name: "キーボード設定", + ka_l: "ふち(左)", + don_l: "面(左)", + don_r: "面(右)", + ka_r: "ふち(右)" + }, + gamepadLayout: { + name: "そうさタイプ設定", + a: "タイプA", + b: "タイプB", + c: "タイプC" + }, + on: "オン", + off: "オフ", + default: "既定値にリセット", + ok: "OK" + } this.browserSupport = { browserWarning: "サポートされていないブラウザを実行しています (%s)", details: "詳しく", @@ -125,6 +164,7 @@ function StringsEn(){ this.randomSong = "Random Song" this.howToPlay = "How to Play" this.aboutSimulator = "About Simulator" + this.gameSettings = "Game Settings" this.browse = "Browse…" this.defaultSongList = "Default Song List" this.songOptions = "Song Options" @@ -135,6 +175,7 @@ function StringsEn(){ this.normal = "Normal" this.hard = "Hard" this.oni = "Extreme" + this.songBranch = "Diverge Notes" this.sessionStart = "Begin an Online Session!" this.sessionEnd = "End Online Session" this.loading = "Loading..." @@ -154,6 +195,11 @@ function StringsEn(){ this.good = "GOOD" this.ok = "OK" this.bad = "BAD" + this.branch = { + "normal": "Normal", + "advanced": "Professional", + "master": "Master" + } this.pauseOptions = [ "Continue", "Retry", @@ -166,8 +212,8 @@ function StringsEn(){ this.tutorial = { basics: [ - "Hit the drum when the notes reach the taiko!", - "For red notes, hit the face of the drum (%s or %s)...", + "When a note overlaps the frame, that is your cue to hit the drum!", + "For red notes, hit the surface of the drum (%s or %s)...", "...and for blue notes, hit the rim! (%s or %s)", "USB controllers are also supported!" ], @@ -193,6 +239,38 @@ function StringsEn(){ linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", cancel: "Cancel" } + this.settings = { + language: { + name: "Language" + }, + resolution: { + name: "Game Resolution", + high: "High", + medium: "Medium", + low: "Low", + lowest: "Lowest" + }, + touchAnimation: { + name: "Touch Animation" + }, + keyboardSettings: { + name: "Keyboard Settings", + ka_l: "Left Rim", + don_l: "Left Surface", + don_r: "Right Surface", + ka_r: "Right Rim" + }, + gamepadLayout: { + name: "Gamepad Layout", + a: "Type A", + b: "Type B", + c: "Type C" + }, + on: "On", + off: "Off", + default: "Reset to Defaults", + ok: "OK" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -226,6 +304,7 @@ function StringsCn(){ this.randomSong = "随机选曲" this.howToPlay = "操作说明" this.aboutSimulator = "关于模拟器" + this.gameSettings = "游戏设定" this.browse = "浏览…" this.defaultSongList = "默认歌曲列表" this.songOptions = "选项" @@ -236,6 +315,7 @@ function StringsCn(){ this.normal = "普通" this.hard = "困难" this.oni = "魔王" + this.songBranch = "有谱面分歧" this.sessionStart = "开始在线会话!" this.sessionEnd = "结束在线会话" this.loading = "加载中..." @@ -255,6 +335,11 @@ function StringsCn(){ this.good = "良" this.ok = "可" this.bad = "不可" + this.branch = { + "normal": "一般谱面", + "advanced": "进阶谱面", + "master": "达人谱面" + } this.pauseOptions = [ "继续演奏", "从头开始", @@ -267,16 +352,16 @@ function StringsCn(){ this.tutorial = { basics: [ - "Hit the drum when the notes reach the taiko!", - "For red notes, hit the face of the drum (%s or %s)...", - "...and for blue notes, hit the rim! (%s or %s)", - "USB controllers are also supported!" + "当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧", + "遇到红色音符要敲打鼓面(%s或%s)", + "遇到蓝色音符则敲打鼓边(%s或%s)", + "USB控制器也支持!" ], - otherControls: "Other controls", + otherControls: "其他控制", otherTutorial: [ - "%s \u2014 pause game", - "%s while selecting difficulty \u2014 enable autoplay mode", - "%s while selecting difficulty \u2014 enable 2P mode" + "%s暂停游戏", + "选择难度时按住%s以启用自动模式", + "选择难度时按住%s以启用网络对战模式" ], ok: "确定" } @@ -294,6 +379,38 @@ function StringsCn(){ linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", cancel: "取消" } + this.settings = { + language: { + name: "语言" + }, + resolution: { + name: "游戏分辨率", + high: "高", + medium: "中", + low: "低", + lowest: "最低" + }, + touchAnimation: { + name: "触摸动画" + }, + keyboardSettings: { + name: "键盘设置", + ka_l: "边缘(左)", + don_l: "表面(左)", + don_r: "表面(右)", + ka_r: "边缘(右)" + }, + gamepadLayout: { + name: "操作类型设定", + a: "类型A", + b: "类型B", + c: "类型C" + }, + on: "开", + off: "关", + default: "重置为默认值", + ok: "确定" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -327,6 +444,7 @@ function StringsTw(){ this.randomSong = "隨機選曲" this.howToPlay = "操作說明" this.aboutSimulator = "關於模擬器" + this.gameSettings = "遊戲設定" this.browse = "開啟檔案…" this.defaultSongList = "默認歌曲列表" this.songOptions = "選項" @@ -337,6 +455,7 @@ function StringsTw(){ this.normal = "普通" this.hard = "困難" this.oni = "魔王" + this.songBranch = "有譜面分歧" this.sessionStart = "開始多人模式!" this.sessionEnd = "結束多人模式" this.loading = "讀取中..." @@ -356,6 +475,11 @@ function StringsTw(){ this.good = "良" this.ok = "可" this.bad = "不可" + this.branch = { + "normal": "一般譜面", + "advanced": "進階譜面", + "master": "達人譜面" + } this.pauseOptions = [ "繼續演奏", "從頭開始", @@ -368,16 +492,16 @@ function StringsTw(){ this.tutorial = { basics: [ - "Hit the drum when the notes reach the taiko!", - "For red notes, hit the face of the drum (%s or %s)...", - "...and for blue notes, hit the rim! (%s or %s)", - "USB controllers are also supported!" + "當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧", + "遇到紅色音符要敲打鼓面(%s或%s)", + "遇到藍色音符則敲打鼓邊(%s或%s)", + "USB控制器也支持!" ], - otherControls: "Other controls", + otherControls: "其他控制", otherTutorial: [ - "%s \u2014 pause game", - "%s while selecting difficulty \u2014 enable autoplay mode", - "%s while selecting difficulty \u2014 enable 2P mode" + "%s暫停遊戲", + "選擇難度時按住%s以啟用自動模式", + "選擇難度時按住%s以啟用網上對打模式" ], ok: "確定" } @@ -395,6 +519,38 @@ function StringsTw(){ linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", cancel: "取消" } + this.settings = { + language: { + name: "語系" + }, + resolution: { + name: "遊戲分辨率", + high: "高", + medium: "中", + low: "低", + lowest: "最低" + }, + touchAnimation: { + name: "觸摸動畫" + }, + keyboardSettings: { + name: "鍵盤設置", + ka_l: "邊緣(左)", + don_l: "表面(左)", + don_r: "表面(右)", + ka_r: "邊緣(右)" + }, + gamepadLayout: { + name: "操作類型設定", + a: "類型A", + b: "類型B", + c: "類型C" + }, + on: "開", + off: "關", + default: "重置為默認值", + ok: "確定" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", @@ -428,6 +584,7 @@ function StringsKo(){ this.randomSong = "랜덤" this.howToPlay = "지도 시간" this.aboutSimulator = "게임 정보" + this.gameSettings = "게임 설정" this.browse = "찾아보기…" this.defaultSongList = "기본 노래 목록" this.songOptions = "옵션" @@ -438,6 +595,7 @@ function StringsKo(){ this.normal = "보통" this.hard = "어려움" this.oni = "귀신" + this.songBranch = "악보 분기 있습니다" this.sessionStart = "온라인 세션 시작!" this.sessionEnd = "온라인 세션 끝내기" this.loading = "로딩 중..." @@ -457,6 +615,11 @@ function StringsKo(){ this.good = "얼쑤" this.ok = "좋다" this.bad = "에구" + this.branch = { + "normal": "보통 악보", + "advanced": "현인 악보", + "master": "달인 악보" + } this.pauseOptions = [ "연주 계속하기", "처음부터 다시", @@ -469,16 +632,16 @@ function StringsKo(){ this.tutorial = { basics: [ - "Hit the drum when the notes reach the taiko!", - "For red notes, hit the face of the drum (%s or %s)...", - "...and for blue notes, hit the rim! (%s or %s)", - "USB controllers are also supported!" + "이동하는 음표가 테두리와 겹쳐졌을 때 북채로 태고를 두드리자!", + "빨간 음표는 면을 두드리자 (%s 또는 %s)", + "파란 음표는 테를 두드리자 (%s 또는 %s)", + "USB 컨트롤러도 지원됩니다!" ], - otherControls: "Other controls", + otherControls: "기타 컨트롤", otherTutorial: [ - "%s \u2014 pause game", - "%s while selecting difficulty \u2014 enable autoplay mode", - "%s while selecting difficulty \u2014 enable 2P mode" + "%s \u2014 게임을 일시 중지합니다", + "난이도 선택 동안 %s 홀드 \u2014 오토 모드 활성화", + "난이도 선택 동안 %s 홀드 \u2014 넷 플레이 모드 활성화" ], ok: "확인" } @@ -496,6 +659,38 @@ function StringsKo(){ linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", cancel: "취소" } + this.settings = { + language: { + name: "언어" + }, + resolution: { + name: "게임 해상도", + high: "높은", + medium: "중간", + low: "저", + lowest: "최저" + }, + touchAnimation: { + name: "터치 애니메이션" + }, + keyboardSettings: { + name: "키보드 설정", + ka_l: "가장자리 (왼쪽)", + don_l: "표면 (왼쪽)", + don_r: "표면 (오른쪽)", + ka_r: "가장자리 (오른쪽)" + }, + gamepadLayout: { + name: "조작 타입 설정", + a: "타입 A", + b: "타입 B", + c: "타입 C" + }, + on: "온", + off: "오프", + default: "기본값으로 재설정", + ok: "확인" + } this.browserSupport = { browserWarning: "You are running an unsupported browser (%s)", details: "Details...", diff --git a/public/src/js/titlescreen.js b/public/src/js/titlescreen.js index 9f7c4b5..6068a93 100644 --- a/public/src/js/titlescreen.js +++ b/public/src/js/titlescreen.js @@ -1,146 +1,102 @@ class Titlescreen{ - constructor(){ - loader.changePage("titlescreen", false) + constructor(songId){ + this.songId = songId - this.titleScreen = document.getElementById("title-screen") - this.proceed = document.getElementById("title-proceed") - this.langDropdown = document.getElementById("lang-dropdown") - this.langId = document.getElementById("lang-id") - this.disclaimerText = document.getElementById("title-disclaimer-text") - this.disclaimerCopyright = document.getElementById("title-disclaimer-copyright") - document.getElementById("globe-path").setAttribute("d", vectors.globe) - - this.logo = new Logo() - this.lang = this.getLang() - this.setLang(allStrings[this.lang]) - this.addLangs() + if(!songId){ + loader.changePage("titlescreen", false) + + this.titleScreen = document.getElementById("title-screen") + this.proceed = document.getElementById("title-proceed") + this.disclaimerText = document.getElementById("title-disclaimer-text") + this.disclaimerCopyright = document.getElementById("title-disclaimer-copyright") + this.logo = new Logo() + } + this.setLang(allStrings[settings.getItem("language")]) - pageEvents.keyAdd(this, "all", "down", this.keyDown.bind(this)) - pageEvents.add(this.titleScreen, ["mousedown", "touchstart"], this.onPressed.bind(this)) - pageEvents.add(this.langDropdown, "change", this.langChange.bind(this)) - - assets.sounds["v_title"].play() - this.gamepad = new Gamepad({ - "13": ["a", "b", "x", "y", "start", "ls", "rs"] - }, pressed => { - if(pressed){ - this.onPressed() + if(songId){ + if(localStorage.getItem("tutorial") === "true"){ + new SongSelect(false, false, this.touched, this.songId) + }else{ + new SettingsView(false, true, this.songId) } - }) - if(p2.session){ - pageEvents.add(p2, "message", response => { - if(response.type === "songsel"){ - this.goNext(true) + }else{ + pageEvents.add(this.titleScreen, ["mousedown", "touchstart"], event => { + if(event.type === "touchstart"){ + event.preventDefault() + this.touched = true + }else if(event.type === "mousedown" && event.which !== 1){ + return } + this.onPressed(true) }) + + assets.sounds["v_title"].play() + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"] + }, this.onPressed.bind(this)) + this.gamepad = new Gamepad({ + confirm: ["a", "b", "x", "y", "start", "ls", "rs"] + }, this.onPressed.bind(this)) + if(p2.session){ + pageEvents.add(p2, "message", response => { + if(response.type === "songsel"){ + this.goNext(true) + } + }) + } + pageEvents.send("title-screen") } } - keyDown(event, code){ - if(event && event.target === this.langDropdown){ - return + onPressed(pressed, name){ + if(pressed){ + this.titleScreen.style.cursor = "auto" + this.clean() + assets.sounds["se_don"].play() + this.goNext() } - if(!code){ - code = event.keyCode - } - if(code == 13 || code == 32 || code == 70 || code == 74){ - // Enter, Space, F, J - this.onPressed() - } - } - onPressed(event){ - if(event){ - if(event.type === "touchstart"){ - event.preventDefault() - this.touched = true - }else if(event.type === "mousedown" && event.which !== 1){ - return - } - } - this.titleScreen.style.cursor = "auto" - this.clean() - assets.sounds["se_don"].play() - this.goNext() } goNext(fromP2){ if(p2.session && !fromP2){ p2.send("songsel") - }else if(fromP2 || this.touched || localStorage.getItem("tutorial") === "true"){ + }else if(fromP2 || localStorage.getItem("tutorial") === "true"){ + if(this.touched){ + localStorage.setItem("tutorial", "true") + } pageEvents.remove(p2, "message") setTimeout(() => { - new SongSelect(false, false, this.touched) + new SongSelect(false, false, this.touched, this.songId) }, 500) }else{ setTimeout(() => { - new Tutorial() + new SettingsView(this.touched, true, this.songId) }, 500) } } - - getLang(){ - if(localStorage.lang && localStorage.lang in allStrings){ - return localStorage.lang + setLang(lang, noEvent){ + settings.setLang(lang, true) + if(this.songId){ + return } - if("languages" in navigator){ - var userLang = navigator.languages.slice() - userLang.unshift(navigator.language) - for(var i in userLang){ - for(var j in allStrings){ - if(allStrings[j].regex.test(userLang[i])){ - return j - } - } - } - } - return "ja" - } - setLang(lang){ - strings = lang this.proceed.innerText = strings.titleProceed this.proceed.setAttribute("alt", strings.titleProceed) - this.langId.innerText = strings.id.toUpperCase() - this.langId.setAttribute("alt", strings.id.toUpperCase()) this.disclaimerText.innerText = strings.titleDisclaimer this.disclaimerText.setAttribute("alt", strings.titleDisclaimer) this.disclaimerCopyright.innerText = strings.titleCopyright this.disclaimerCopyright.setAttribute("alt", strings.titleCopyright) - loader.screen.style.fontFamily = strings.font - loader.screen.style.fontWeight = strings.font === "Microsoft YaHei, sans-serif" ? "bold" : "" - if(failedTests.length !== 0){ - showUnsupported(strings) - } this.logo.updateSubtitle() } - addLangs(){ - for(var i in allStrings){ - var option = document.createElement("option") - option.value = i - if(i === this.lang){ - option.selected = true - } - option.appendChild(document.createTextNode(allStrings[i].name)) - this.langDropdown.appendChild(option) - } - } - langChange(){ - this.lang = this.langDropdown.value - localStorage.lang = this.lang - this.setLang(allStrings[this.lang]) - } - clean(){ + this.keyboard.clean() this.gamepad.clean() this.logo.clean() assets.sounds["v_title"].stop() - pageEvents.keyRemove(this, "all") pageEvents.remove(this.titleScreen, ["mousedown", "touchstart"]) - pageEvents.remove(this.langDropdown, "change") delete this.titleScreen delete this.proceed delete this.titleDisclaimer delete this.titleCopyright - delete this.langDropdown } } diff --git a/public/src/js/tutorial.js b/public/src/js/tutorial.js index 7de7e31..2d1302c 100644 --- a/public/src/js/tutorial.js +++ b/public/src/js/tutorial.js @@ -1,15 +1,72 @@ class Tutorial{ - constructor(fromSongSel){ + constructor(fromSongSel, songId){ this.fromSongSel = fromSongSel + this.songId = songId loader.changePage("tutorial", true) assets.sounds["bgm_setsume"].playLoop(0.1, false, 0, 1.054, 16.054) - this.endButton = document.getElementById("tutorial-end-button") + this.endButton = this.getElement("view-end-button") - var tutorialTitle = document.getElementById("tutorial-title") - tutorialTitle.innerText = strings.howToPlay - tutorialTitle.setAttribute("alt", strings.howToPlay) - var tutorialContent = document.getElementById("tutorial-content") - var keys = ["F", "J", "D", "K", "Q", "SHIFT", "CTRL"] + this.tutorialTitle = this.getElement("view-title") + this.tutorialDiv = document.createElement("div") + this.getElement("view-content").appendChild(this.tutorialDiv) + this.setStrings() + + pageEvents.add(this.endButton, ["mousedown", "touchstart"], event => { + if(event.type === "touchstart"){ + event.preventDefault() + this.touched = true + }else if(event.type === "mousedown" && event.which !== 1){ + return + } + this.onEnd(true) + }) + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "esc", "don_l", "don_r"] + }, this.onEnd.bind(this)) + this.gamepad = new Gamepad({ + confirm: ["start", "b", "ls", "rs"] + }, this.onEnd.bind(this)) + + pageEvents.send("tutorial") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + insertText(text, parent){ + parent.appendChild(document.createTextNode(text)) + } + insertKey(key, parent){ + var kbd = document.createElement("kbd") + kbd.innerText = key + parent.appendChild(kbd) + } + onEnd(pressed, name){ + if(pressed){ + this.clean() + assets.sounds["se_don"].play() + try{ + localStorage.setItem("tutorial", "true") + }catch(e){} + setTimeout(() => { + new SongSelect(this.fromSongSel ? "tutorial" : false, false, this.touched, this.songId) + }, 500) + } + } + setStrings(){ + this.tutorialTitle.innerText = strings.howToPlay + this.tutorialTitle.setAttribute("alt", strings.howToPlay) + this.endButton.innerText = strings.tutorial.ok + this.endButton.setAttribute("alt", strings.tutorial.ok) + this.tutorialDiv.innerHTML = "" + var kbdSettings = settings.getItem("keyboardSettings") + var pauseKey = pageEvents.kbd.indexOf("q") === -1 ? "Q" : "ESC" + var keys = [ + kbdSettings.don_l[0].toUpperCase(), + kbdSettings.don_r[0].toUpperCase(), + kbdSettings.ka_l[0].toUpperCase(), + kbdSettings.ka_r[0].toUpperCase(), + pauseKey, "SHIFT", "CTRL" + ] var keyIndex = 0 strings.tutorial.basics.forEach(string => { var par = document.createElement("p") @@ -20,7 +77,7 @@ class Tutorial{ } this.insertText(stringKey, par) }) - tutorialContent.appendChild(par) + this.tutorialDiv.appendChild(par) }) var par = document.createElement("p") var span = document.createElement("span") @@ -37,43 +94,15 @@ class Tutorial{ this.insertText(stringKey, par) }) }) - tutorialContent.appendChild(par) - this.endButton.innerText = strings.tutorial.ok - this.endButton.setAttribute("alt", strings.tutorial.ok) - - pageEvents.once(this.endButton, ["mousedown", "touchstart"]).then(this.onEnd.bind(this)) - pageEvents.keyOnce(this, 13, "down").then(this.onEnd.bind(this)) - - this.gamepad = new Gamepad({ - "confirm": ["start", "b", "ls", "rs"] - }, this.onEnd.bind(this)) - } - insertText(text, parent){ - parent.appendChild(document.createTextNode(text)) - } - insertKey(key, parent){ - var kbd = document.createElement("kbd") - kbd.innerText = key - parent.appendChild(kbd) - } - onEnd(event){ - var touched = false - if(event && event.type === "touchstart"){ - event.preventDefault() - touched = true - } - this.clean() - assets.sounds["se_don"].play() - localStorage.setItem("tutorial", "true") - setTimeout(() => { - new SongSelect(this.fromSongSel ? "tutorial" : false, false, touched) - }, 500) + this.tutorialDiv.appendChild(par) } clean(){ + this.keyboard.clean() this.gamepad.clean() - assets.sounds["bgm_setsume"].stop() pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) - pageEvents.keyRemove(this, 13) + assets.sounds["bgm_setsume"].stop() + delete this.tutorialTitle delete this.endButton + delete this.tutorialDiv } } diff --git a/public/src/js/view.js b/public/src/js/view.js index 61513d0..06c30c7 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -74,6 +74,34 @@ this.nextBeat = 0 this.gogoTime = 0 this.drumroll = [] + this.touchEvents = 0 + if(this.controller.parsedSongData.branches){ + this.branch = "normal" + this.branchAnimate = { + ms: -Infinity, + fromBranch: "normal" + } + this.branchMap = { + "normal": { + "bg": "rgba(0, 0, 0, 0)", + "text": "#d3d3d3", + "stroke": "#393939", + "shadow": "#000" + }, + "advanced": { + "bg": "rgba(29, 129, 189, 0.4)", + "text": "#94d7e7", + "stroke": "#315973", + "shadow": "#082031" + }, + "master": { + "bg": "rgba(230, 29, 189, 0.4)", + "text": "#f796ef", + "stroke": "#7e2e6e", + "shadow": "#3e0836" + } + } + } this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval this.font = strings.font @@ -84,11 +112,13 @@ this.titleCache = new CanvasCache() this.comboCache = new CanvasCache() this.pauseCache = new CanvasCache() + this.branchCache = new CanvasCache() this.multiplayer = this.controller.multiplayer this.touchEnabled = this.controller.touchEnabled this.touch = -Infinity + this.touchAnimation = settings.getItem("touchAnimation") if(this.multiplayer !== 2){ @@ -96,6 +126,8 @@ this.touchDrumDiv = document.getElementById("touch-drum") this.touchDrumImg = document.getElementById("touch-drum-img") + this.setBgImage(this.touchDrumImg, assets.image["touch_drum"].src) + if(this.controller.autoPlayEnabled){ this.touchDrumDiv.style.display = "none" } @@ -131,7 +163,8 @@ } this.setDonBg() - this.lastMousemove = this.controller.game.getAccurateTime() + this.startTime = this.controller.game.getAccurateTime() + this.lastMousemove = this.startTime pageEvents.mouseAdd(this, this.onmousemove.bind(this)) this.refresh() @@ -150,6 +183,14 @@ var touchMultiplayer = this.touchEnabled && this.multiplayer && !this.portrait this.pixelRatio = window.devicePixelRatio || 1 + var resolution = settings.getItem("resolution") + if(resolution === "medium"){ + this.pixelRatio *= 0.75 + }else if(resolution === "low"){ + this.pixelRatio *= 0.5 + }else if(resolution === "lowest"){ + this.pixelRatio *= 0.25 + } winW *= this.pixelRatio winH *= this.pixelRatio if(this.portrait){ @@ -161,6 +202,7 @@ } var ratio = (ratioX < ratioY ? ratioX : ratioY) + var resized = false if(this.winW !== winW || this.winH !== winH){ this.winW = winW this.winH = winH @@ -180,6 +222,7 @@ } this.fillComboCache() this.setDonBgHeight() + resized = true }else if(this.controller.game.paused && !document.hasFocus()){ return }else if(this.multiplayer !== 2){ @@ -668,7 +711,7 @@ } ctx.restore() - // Bar pressed keys + // Branch background var keyTime = this.controller.getKeyTime() var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka" var padding = this.slotPos.paddingLeft @@ -676,12 +719,78 @@ var barY = this.slotPos.y - 65 * mul var barH = 130 * mul + if(this.branchAnimate && ms <= this.branchAnimate.ms + 300){ + var alpha = Math.max(0, (ms - this.branchAnimate.ms) / 300) + ctx.globalAlpha = 1 - alpha + ctx.fillStyle = this.branchMap[this.branchAnimate.fromBranch].bg + ctx.fillRect(padding, barY, winW - padding, barH) + ctx.globalAlpha = alpha + } + if(this.branch){ + ctx.fillStyle = this.branchMap[this.branch].bg + ctx.fillRect(padding, barY, winW - padding, barH) + ctx.globalAlpha = 1 + } + + // Current branch text + if(this.branch){ + if(resized){ + this.fillBranchCache() + } + var textW = Math.floor(260 * mul) + var textH = Math.floor(barH) + var textX = winW - textW + var oldOffset = 0 + var newOffset = 0 + var elapsed = ms - this.startTime + if(elapsed < 250){ + textX = winW + }else if(elapsed < 500){ + textX += (1 - this.draw.easeOutBack((elapsed - 250) / 250)) * textW + } + if(this.branchAnimate && ms - this.branchAnimate.ms < 310 && ms >= this.branchAnimate.ms){ + var fromBranch = this.branchAnimate.fromBranch + var elapsed = ms - this.branchAnimate.ms + var reverse = fromBranch === "master" || fromBranch === "advanced" && this.branch === "normal" ? -1 : 1 + if(elapsed < 65){ + oldOffset = elapsed / 65 * 12 * mul * reverse + ctx.globalAlpha = 1 + var newAlpha = 0 + }else if(elapsed < 215){ + var animPoint = (elapsed - 65) / 150 + oldOffset = (12 - animPoint * 48) * mul * reverse + newOffset = (36 - animPoint * 48) * mul * reverse + ctx.globalAlpha = this.draw.easeIn(1 - animPoint) + var newAlpha = this.draw.easeIn(animPoint) + }else{ + newOffset = (1 - (elapsed - 215) / 95) * -12 * mul * reverse + ctx.globalAlpha = 0 + var newAlpha = 1 + } + this.branchCache.get({ + ctx: ctx, + x: textX, y: barY + oldOffset, + w: textW, h: textH, + id: fromBranch + }) + ctx.globalAlpha = newAlpha + } + this.branchCache.get({ + ctx: ctx, + x: textX, y: barY + newOffset, + w: textW, h: textH, + id: this.branch + }) + ctx.globalAlpha = 1 + } + + // Go go time background if(this.gogoTime || ms <= this.gogoTimeStarted + 100){ var grd = ctx.createLinearGradient(padding, 0, winW, 0) - grd.addColorStop(0, "#512a2c") - grd.addColorStop(0.46, "#6f2a2d") - grd.addColorStop(0.76, "#8a4763") - grd.addColorStop(1, "#2c2a2c") + grd.addColorStop(0, "rgba(255, 0, 0, 0.16)") + grd.addColorStop(0.45, "rgba(255, 0, 0, 0.28)") + grd.addColorStop(0.77, "rgba(255, 83, 157, 0.4)") + grd.addColorStop(1, "rgba(255, 83, 157, 0)") ctx.fillStyle = grd if(!this.touchEnabled){ var alpha = Math.min(100, ms - this.gogoTimeStarted) / 100 @@ -692,6 +801,8 @@ } ctx.fillRect(padding, barY, winW - padding, barH) } + + // Bar pressed keys if(keyTime[sound] > ms - 130){ var gradients = { "don": "255, 0, 0", @@ -719,11 +830,10 @@ ) // Taiko pressed keys - var kbd = this.controller.getBindings() var keys = ["ka_l", "ka_r", "don_l", "don_r"] for(var i = 0; i < keys.length; i++){ - var keyMS = ms - keyTime[kbd[keys[i]]] + var keyMS = ms - keyTime[keys[i]] if(keyMS < 130){ if(keyMS > 70 && !this.touchEnabled){ ctx.globalAlpha = this.draw.easeOut(1 - (keyMS - 70) / 60) @@ -750,7 +860,7 @@ comboScale = this.draw.fade(scoreMS / 100) } var glyphW = 51 - var glyphH = 64 + var glyphH = 65 var letterSpacing = (comboText.length >= 4 ? 38 : 42) * mul var orange = comboCount >= 100 ? "1" : "0" @@ -789,6 +899,7 @@ ctx.lineWidth = 7 * mul ctx.textAlign = "center" ctx.miterLimit = 1 + ctx.strokeStyle = "#000" ctx.strokeText(strings.combo, comboX, comboTextY) ctx.miterLimit = 10 ctx.fillText(strings.combo, comboX, comboTextY) @@ -999,7 +1110,7 @@ var songSkinName = selectedSong.songSkin.name var supportsBlend = "mixBlendMode" in this.songBg.style var songLayers = [document.getElementById("layer1"), document.getElementById("layer2")] - var prefix = selectedSong.songSkin.prefix || "" + var prefix = "" if(selectedSong.category in this.categories){ var catId = this.categories[selectedSong.category].sort @@ -1010,8 +1121,9 @@ if(!selectedSong.songSkin.song){ var id = selectedSong.songBg this.songBg.classList.add("songbg-" + id) - this.setLayers(songLayers, prefix + "bg_song_" + id + (supportsBlend ? "" : "a"), supportsBlend) + this.setLayers(songLayers, "bg_song_" + id + (supportsBlend ? "" : "a"), supportsBlend) }else if(selectedSong.songSkin.song !== "none"){ + var prefix = selectedSong.songSkin.prefix || "" var notStatic = selectedSong.songSkin.song !== "static" if(notStatic){ this.songBg.classList.add("songbg-" + selectedSong.songSkin.song) @@ -1021,7 +1133,9 @@ if(!selectedSong.songSkin.stage){ this.songStage.classList.add("song-stage-" + selectedSong.songStage) + this.setBgImage(this.songStage, assets.image["bg_stage_" + selectedSong.songStage].src) }else if(selectedSong.songSkin.stage !== "none"){ + var prefix = selectedSong.songSkin.prefix || "" this.setBgImage(this.songStage, assets.image[prefix + "bg_stage_" + songSkinName].src) } } @@ -1030,7 +1144,7 @@ var songSkinName = selectedSong.songSkin.name var donLayers = [] var filename = !selectedSong.songSkin.don && this.multiplayer === 2 ? "bg_don2_" : "bg_don_" - var prefix = selectedSong.songSkin.prefix || "" + var prefix = "" this.donBg = document.createElement("div") this.donBg.classList.add("donbg") @@ -1049,10 +1163,11 @@ var asset1, asset2 if(!selectedSong.songSkin.don){ this.donBg.classList.add("donbg-" + selectedSong.donBg) - this.setLayers(donLayers, prefix + filename + selectedSong.donBg, true) + this.setLayers(donLayers, filename + selectedSong.donBg, true) asset1 = filename + selectedSong.donBg + "a" asset2 = filename + selectedSong.donBg + "b" }else if(selectedSong.songSkin.don !== "none"){ + var prefix = selectedSong.songSkin.prefix || "" var notStatic = selectedSong.songSkin.don !== "static" if(notStatic){ this.donBg.classList.add("donbg-" + selectedSong.songSkin.don) @@ -1076,6 +1191,11 @@ } setDonBgHeight(){ this.donBg.style.setProperty("--h", getComputedStyle(this.donBg).height) + var gameDiv = this.gameDiv + gameDiv.classList.add("fix-animations") + setTimeout(()=>{ + gameDiv.classList.remove("fix-animations") + }, 50) } setLayers(elements, file, ab){ if(ab){ @@ -1101,15 +1221,23 @@ 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) - if(ms >= startingTime && ms <= finishTime){ + if(measure.visible && (!measure.branch || measure.branch.active) && ms >= startingTime && ms <= finishTime){ var measureX = this.slotPos.x + this.msToPos(measure.ms - ms, measure.speed) - this.ctx.strokeStyle = "#bdbdbd" + this.ctx.strokeStyle = measure.branchFirst ? "#ff0" : "#bdbdbd" this.ctx.lineWidth = 3 this.ctx.beginPath() this.ctx.moveTo(measureX, measureY) this.ctx.lineTo(measureX, measureY + measureH) this.ctx.stroke() } + if(this.multiplayer !== 2 && ms >= measure.ms && measure.nextBranch && !measure.viewChecked && measure.gameChecked){ + measure.viewChecked = true + if(measure.nextBranch.active !== this.branch){ + this.branchAnimate.ms = ms + this.branchAnimate.fromBranch = this.branch + } + this.branch = measure.nextBranch.active + } }) } updateNoteFaces(){ @@ -1136,21 +1264,21 @@ for(var i = circles.length; i--;){ var circle = circles[i] - var speed = circle.getSpeed() + var speed = circle.speed var timeForDistance = this.posToMs(distanceForCircle + this.slotPos.size / 2, speed) - var startingTime = circle.getMS() - timeForDistance - var finishTime = circle.getEndTime() + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + 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) - if(circle.getPlayed() <= 0 || circle.getScore() === 0){ - if(ms >= startingTime && ms <= finishTime && circle.getPlayed() !== -1){ + if(circle.isPlayed <= 0 || circle.score === 0){ + if((!circle.branch || circle.branch.active) && ms >= startingTime && ms <= finishTime && circle.isPlayed !== -1){ this.drawCircle(circle) } - }else if(!circle.isAnimated()){ + }else if(!circle.animating){ // Start animation to gauge circle.animate(ms) } - if(ms >= circle.ms && !circle.gogoChecked){ + if(ms >= circle.ms && !circle.gogoChecked && (!circle.branch || circle.branch.active)){ if(this.gogoTime != circle.gogoTime){ this.toggleGogoTime(circle) } @@ -1164,9 +1292,9 @@ for(var i = 0; i < circles.length; i++){ var circle = circles[i] - if(circle.isAnimated()){ + if(circle.animating){ - var animT = circle.getAnimT() + var animT = circle.animT if(ms < animT + 490){ if(circle.fixedPos){ @@ -1182,7 +1310,7 @@ var pos = this.animateBezier[3] this.drawCircle(circle, pos, (ms - animT - 490) / 160) }else{ - circle.endAnimation() + circle.animationEnded = true } } } @@ -1210,13 +1338,13 @@ var lyricsSize = 20 * mul var fill, size, faceID - var type = circle.getType() + var type = circle.type var ms = this.getMS() - var circleMs = circle.getMS() - var endTime = circle.getEndTime() - var animated = circle.isAnimated() - var speed = circle.getSpeed() - var played = circle.getPlayed() + var circleMs = circle.ms + var endTime = circle.endTime + var animated = circle.animating + var speed = circle.speed + var played = circle.isPlayed var drumroll = 0 var endX = 0 @@ -1322,9 +1450,9 @@ ctx.fill() ctx.globalAlpha = 1 } - if(!circle.isAnimated()){ + if(!circle.animating && circle.text){ // Text - var text = circle.getText() + var text = circle.text var textX = circlePos.x var textY = circlePos.y + 83 * mul ctx.font = lyricsSize + "px Kozuka, Microsoft YaHei, sans-serif" @@ -1374,8 +1502,8 @@ fillComboCache(){ var fontSize = 58 var letterSpacing = fontSize * 0.67 - var glyphW = 50 - var glyphH = 64 + var glyphW = 51 + var glyphH = 65 var textX = 5 var textY = 5 var letterBorder = fontSize * 0.15 @@ -1435,6 +1563,37 @@ }) this.globalAlpha = 1 } + fillBranchCache(){ + var mul = this.slotPos.size / 106 + var textW = Math.floor(260 * mul) + var barH = Math.floor(130 * mul) + var branchNames = this.controller.game.branchNames + var textX = textW - 33 * mul + var textY = 63 * mul + var fontSize = (strings.id === "en" ? 33 : (strings.id === "ko" ? 38 : 43)) * mul + this.branchCache.resize((textW + 1), (barH + 1) * 3, this.ratio) + for(var i in branchNames){ + this.branchCache.set({ + w: textW, + h: barH, + id: branchNames[i] + }, ctx => { + var currentMap = this.branchMap[branchNames[i]] + ctx.font = this.draw.bold(this.font) + fontSize + "px " + this.font + ctx.lineJoin = "round" + ctx.miterLimit = 1 + ctx.textAlign = "right" + ctx.textBaseline = "middle" + ctx.lineWidth = 8 * mul + ctx.strokeStyle = currentMap.shadow + ctx.strokeText(strings.branch[branchNames[i]], textX, textY + 4 * mul) + ctx.strokeStyle = currentMap.stroke + ctx.strokeText(strings.branch[branchNames[i]], textX, textY) + ctx.fillStyle = currentMap.text + ctx.fillText(strings.branch[branchNames[i]], textX, textY) + }) + } + } toggleGogoTime(circle){ this.gogoTime = circle.gogoTime this.gogoTimeStarted = circle.ms @@ -1465,7 +1624,7 @@ if(this.gogoTime){ var circles = this.controller.parsedSongData.circles var lastCircle = circles[circles.length - 1] - var endTime = lastCircle.getEndTime() + 3000 + var endTime = lastCircle.endTime + 3000 if(ms >= endTime){ this.toggleGogoTime({ gogoTime: 0, @@ -1553,14 +1712,16 @@ this.touchDrumDiv.style.width = drumWidth + "px" this.touchDrumDiv.style.height = drumHeight + "px" } - if(this.touch > ms - 100){ - if(!this.drumPadding){ - this.drumPadding = true - this.touchDrumImg.style.backgroundPositionY = "7px" + if(this.touchAnimation){ + if(this.touch > ms - 100){ + if(!this.drumPadding){ + this.drumPadding = true + this.touchDrumImg.style.backgroundPositionY = "7px" + } + }else if(this.drumPadding){ + this.drumPadding = false + this.touchDrumImg.style.backgroundPositionY = "" } - }else if(this.drumPadding){ - this.drumPadding = false - this.touchDrumImg.style.backgroundPositionY = "" } } } @@ -1601,16 +1762,16 @@ this.touchNote("ka_r") } } + this.touchEvents++ } } } touchNote(note){ var keyboard = this.controller.keyboard - var kbd = keyboard.getBindings() var ms = this.controller.game.getAccurateTime() this.touch = ms - keyboard.setKey(kbd[note], false) - keyboard.setKey(kbd[note], true, ms) + keyboard.setKey(false, note) + keyboard.setKey(true, note, ms) } mod(length, index){ return ((index % length) + length) % length @@ -1631,12 +1792,17 @@ switch(pos){ case 1: assets.sounds["se_don"].play() - return this.controller.restartSong() + this.controller.restartSong() + pageEvents.send("pause-restart") + break case 2: assets.sounds["se_don"].play() - return this.controller.songSelection() + this.controller.songSelection() + pageEvents.send("pause-song-select") + break default: - return this.controller.togglePause() + this.controller.togglePause() + break } } onmousedown(event){ @@ -1721,6 +1887,8 @@ this.assets.clean() this.titleCache.clean() this.comboCache.clean() + this.pauseCache.clean() + this.branchCache.clean() if(this.multiplayer !== 2){ if(this.touchEnabled){ diff --git a/public/src/views/about.html b/public/src/views/about.html index 7ec8436..3506eb3 100644 --- a/public/src/views/about.html +++ b/public/src/views/about.html @@ -1,9 +1,9 @@ -
-
-
-
+
+
+
+
- -
+
diff --git a/public/src/views/debug.html b/public/src/views/debug.html index eba64ac..e57addc 100644 --- a/public/src/views/debug.html +++ b/public/src/views/debug.html @@ -9,6 +9,21 @@
x-+
+
+
Branch:
+
+ x +
+
+
Music volume:
+
+ x-+ +
diff --git a/public/src/views/session.html b/public/src/views/session.html index 5e57005..f47ce54 100644 --- a/public/src/views/session.html +++ b/public/src/views/session.html @@ -1,9 +1,9 @@ -
-
-
-
+
+
+
+
-
+
diff --git a/public/src/views/settings.html b/public/src/views/settings.html new file mode 100644 index 0000000..fbac462 --- /dev/null +++ b/public/src/views/settings.html @@ -0,0 +1,24 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/src/views/titlescreen.html b/public/src/views/titlescreen.html index e660734..449427f 100644 --- a/public/src/views/titlescreen.html +++ b/public/src/views/titlescreen.html @@ -6,8 +6,3 @@
-
- -
- -
diff --git a/public/src/views/tutorial.html b/public/src/views/tutorial.html index 239daf2..079f48b 100644 --- a/public/src/views/tutorial.html +++ b/public/src/views/tutorial.html @@ -1,7 +1,7 @@ -
-
-
-
-
+
+
+
+
+
diff --git a/server.py b/server.py index 19ddd03..aafcad4 100644 --- a/server.py +++ b/server.py @@ -185,6 +185,7 @@ async def connection(ws, path): if "other_user" in user and "ws" in user["other_user"]: if type == "note"\ or type == "drumroll"\ + or type == "branch"\ or type == "gameresults": await user["other_user"]["ws"].send(msgobj(type, value)) elif type == "songsel" and user["session"]: @@ -210,6 +211,7 @@ async def connection(ws, path): user["other_user"]["ws"].send(sent_msg1), user["other_user"]["ws"].send(sent_msg2) ]) + del user["other_user"]["other_user"] del user["other_user"] else: # Other user disconnected @@ -304,6 +306,7 @@ async def connection(ws, path): user["other_user"]["ws"].send(sent_msg1), user["other_user"]["ws"].send(sent_msg2) ]) + del user["other_user"]["other_user"] del user["other_user"] else: # Other user disconnected @@ -324,6 +327,7 @@ async def connection(ws, path): user["other_user"]["ws"].send(msgobj("gameend")), user["other_user"]["ws"].send(status_event()) ]) + del user["other_user"]["other_user"] if user["action"] == "waiting": del server_status["waiting"][user["gameid"]] await notify_status()