diff --git a/.gitignore b/.gitignore index 994f1e6..6d44399 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ $RECYCLE.BIN/ .Spotlight-V100 .Trashes +.vscode + # Directories potentially created on remote AFP share .AppleDB .AppleDesktop diff --git a/app.py b/app.py index 11c4b44..d288d98 100644 --- a/app.py +++ b/app.py @@ -86,7 +86,7 @@ def route_index(): @app.route('/api/preview') -@app.cache.cached(timeout=15) +@app.cache.cached(timeout=15, query_string=True) def route_api_preview(): song_id = request.args.get('id', None) if not song_id or not re.match('^[0-9]+$', song_id): diff --git a/public/assets/audio/se_jump.wav b/public/assets/audio/se_jump.wav new file mode 100644 index 0000000..69208ef Binary files /dev/null and b/public/assets/audio/se_jump.wav differ diff --git a/public/assets/img/vectors.json b/public/assets/img/vectors.json index 536fb30..eb64b07 100644 --- a/public/assets/img/vectors.json +++ b/public/assets/img/vectors.json @@ -66,5 +66,14 @@ "", "logo5": +"", + +"category": +"", + +"categoryShadow": +"", + +"categoryHighlight": "" } diff --git a/public/src/js/assets.js b/public/src/js/assets.js index dd94554..39a32b8 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -90,6 +90,7 @@ var assets = { "se_don.wav", "se_ka.wav", "se_pause.wav", + "se_jump.wav", "se_calibration.wav", "v_results.wav", diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index baea01c..df8a25c 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -45,6 +45,12 @@ shadow: new Path2D(vectors.optionsShadow) } + this.categoryPath = { + main: new Path2D(vectors.category), + shadow: new Path2D(vectors.categoryShadow), + highlight: new Path2D(vectors.categoryHighlight) + } + this.regex = { comma: /[,.]/, ideographicComma: /[、。]/, @@ -225,10 +231,14 @@ highlight(config){ var ctx = config.ctx ctx.save() - var _x = config.x + 3.5 - var _y = config.y + 3.5 - var _w = config.w - 7 - var _h = config.h - 7 + if(config.shape){ + ctx.translate(config.x, config.y) + }else{ + var _x = config.x + 3.5 + var _y = config.y + 3.5 + var _w = config.w - 7 + var _h = config.h - 7 + } if(config.animate){ ctx.globalAlpha = this.fade((this.getMS() - config.animateMS) % 2000 / 2000) }else if(config.opacity){ @@ -243,19 +253,25 @@ h: _h, radius: config.radius }) - }else{ + }else if(!config.shape){ ctx.beginPath() ctx.rect(_x, _y, _w, _h) } + if(config.shape){ + var stroke = () => ctx.stroke(config.shape) + }else{ + var stroke = () => ctx.stroke() + } + var size = config.size || 14 ctx.strokeStyle = "rgba(255, 249, 1, 0.45)" - ctx.lineWidth = 14 - ctx.stroke() + ctx.lineWidth = size + stroke() ctx.strokeStyle = "rgba(255, 249, 1, .8)" - ctx.lineWidth = 8 - ctx.stroke() + ctx.lineWidth = 8 / 14 * size + stroke() ctx.strokeStyle = "#fff" - ctx.lineWidth = 6 - ctx.stroke() + ctx.lineWidth = 6 / 14 * size + stroke() ctx.restore() } @@ -1472,6 +1488,48 @@ ctx.restore() } + category(config){ + var ctx = config.ctx + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale || config.right){ + ctx.scale((config.right ? -1 : 1) * (config.scale || 1), config.scale || 1) + } + ctx.translate(-15.5 + 14, -11) + for(var i = 0; i < 4; i++){ + if(i < 2){ + ctx.lineWidth = 6 + ctx.strokeStyle = "#000" + ctx.stroke(this.categoryPath.main) + }else{ + ctx.fillStyle = config.fill + ctx.fill(this.categoryPath.main) + ctx.fillStyle = "rgba(255, 255, 255, 0.25)" + ctx.fill(this.categoryPath.main) + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.fill(this.categoryPath.shadow) + } + if(i % 2 === 0){ + ctx.translate(-14, 0) + }else if(i === 1){ + ctx.translate(14, 0) + } + } + if(config.highlight){ + this.highlight({ + ctx: ctx, + x: 0, + y: 0, + opacity: 0.8, + shape: this.categoryPath.highlight, + size: 8 + }) + } + + ctx.restore() + } + alpha(amount, ctx, callback, winW, winH){ if(amount >= 1){ return callback(ctx) diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 80c7671..926d1e1 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -123,7 +123,8 @@ class SongSelect{ songSkin: song.song_skin || {}, music: song.music, volume: song.volume, - maker: song.maker + maker: song.maker, + canJump: true }) } this.songs.sort((a, b) => { @@ -144,7 +145,8 @@ class SongSelect{ title: strings.randomSong, skin: this.songSkin.random, action: "random", - category: strings.random + category: strings.random, + canJump: true }) if(touchEnabled){ if(fromTutorial === "tutorial"){ @@ -290,7 +292,8 @@ class SongSelect{ locked: true, hasPointer: false, options: 0, - selLock: false + selLock: false, + catJump: false } this.songSelecting = { speed: 800, @@ -315,13 +318,15 @@ class SongSelect{ this.gamepad = new Gamepad({ confirm: ["b", "start", "ls", "rs"], back: ["a"], - left: ["l", "lb", "lt", "lsl"], - right: ["r", "rb", "rt", "lsr"], + left: ["l", "lsl", "lt"], + right: ["r", "lsr", "rt"], up: ["u", "lsu"], down: ["d", "lsd"], session: ["back"], ctrl: ["y"], - shift: ["x"] + shift: ["x"], + jump_left: ["lb"], + jump_right: ["rb"] }, this.keyPress.bind(this)) if(!assets.customSongs){ @@ -353,7 +358,7 @@ class SongSelect{ } } - keyPress(pressed, name, event){ + keyPress(pressed, name, event, repeat){ if(pressed){ if(!this.pressedKeys[name]){ this.pressedKeys[name] = this.getMS() + 300 @@ -365,6 +370,7 @@ class SongSelect{ if(name === "ctrl" || name === "shift" || !this.redrawRunning){ return } + var shift = event ? event.shiftKey : this.pressedKeys["shift"] if(this.state.screen === "song"){ if(name === "confirm"){ this.toSelectDifficulty() @@ -373,9 +379,25 @@ class SongSelect{ }else if(name === "session"){ this.toSession() }else if(name === "left"){ - this.moveToSong(-1) + if(shift){ + if(!repeat){ + this.categoryJump(-1) + } + }else{ + this.moveToSong(-1) + } }else if(name === "right"){ - this.moveToSong(1) + if(shift){ + if(!repeat){ + this.categoryJump(1) + } + }else{ + this.moveToSong(1) + } + }else if(name === "jump_left" && !repeat){ + this.categoryJump(-1) + }else if(name === "jump_right" && !repeat){ + this.categoryJump(1) } }else if(this.state.screen === "difficulty"){ if(name === "confirm"){ @@ -384,7 +406,7 @@ class SongSelect{ }else if(this.selectedDiff === 1){ this.toOptions(1) }else{ - this.toLoadSong(this.selectedDiff - this.diffOptions.length, this.pressedKeys["shift"], this.pressedKeys["ctrl"]) + this.toLoadSong(this.selectedDiff - this.diffOptions.length, shift, this.pressedKeys["ctrl"]) } }else if(name === "back" || name === "session"){ this.toSongSelect() @@ -424,7 +446,9 @@ class SongSelect{ var touch = true } if(this.state.screen === "song"){ - if(mouse.x > 641 && mouse.y > 603){ + if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ + this.categoryJump(mouse.x < 640 ? -1 : 1) + }else if(mouse.x > 641 && mouse.y > 603){ this.toSession() }else{ var moveBy = this.songSelMouse(mouse.x, mouse.y) @@ -473,7 +497,9 @@ class SongSelect{ var mouse = this.mouseOffset(event.offsetX, event.offsetY) var moveTo = null if(this.state.screen === "song"){ - if(mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ + if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ + moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext" + }else if(mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ moveTo = "session" }else{ var moveTo = this.songSelMouse(mouse.x, mouse.y) @@ -580,6 +606,20 @@ class SongSelect{ this.pointer(false) } } + + categoryJump(moveBy){ + if(this.state.locked === 1){ + return + } + + this.state.catJump = true + this.state.move = moveBy; + this.state.locked = 1 + + this.endPreview() + this.playSound("se_jump") + } + moveToDiff(moveBy){ if(this.state.locked !== 1){ this.state.move = moveBy @@ -787,7 +827,7 @@ class SongSelect{ for(var key in this.pressedKeys){ if(this.pressedKeys[key]){ if(ms >= this.pressedKeys[key] + 50){ - this.keyPress(true, key) + this.keyPress(true, key, null, true) this.pressedKeys[key] = ms } } @@ -949,16 +989,31 @@ class SongSelect{ }) var category = this.songs[this.selectedSong].category - if(category){ - var selectedSong = this.songs[this.selectedSong] - this.categoryCache.get({ - ctx: ctx, - x: winW / 2 - 280 / 2, - y: frameTop, - w: 280, - h: this.songAsset.marginTop, - id: category + selectedSong.skin.outline - }, ctx => { + var selectedSong = this.songs[this.selectedSong] + this.draw.category({ + ctx: ctx, + x: winW / 2 - 280 / 2 - 30, + y: frameTop + 60, + fill: selectedSong.skin.background, + highlight: this.state.moveHover === "categoryPrev" + }) + this.draw.category({ + ctx: ctx, + x: winW / 2 + 280 / 2 + 30, + y: frameTop + 60, + right: true, + fill: selectedSong.skin.background, + highlight: this.state.moveHover === "categoryNext" + }) + this.categoryCache.get({ + ctx: ctx, + x: winW / 2 - 280 / 2, + y: frameTop, + w: 280, + h: this.songAsset.marginTop, + id: category + selectedSong.skin.outline + }, ctx => { + if(category){ if(category in strings.categories){ var categoryName = strings.categories[category] }else{ @@ -978,8 +1033,8 @@ class SongSelect{ {outline: selectedSong.skin.outline, letterBorder: 12, shadow: [3, 3, 3]}, {fill: "#fff"} ]) - }) - } + } + }) } if(screen === "song"){ @@ -994,16 +1049,67 @@ class SongSelect{ var resize2 = changeSpeed - resize var scroll = resize2 - resize - scrollDelay * 2 var elapsed = ms - this.state.moveMS - if(this.state.move && ms > this.state.moveMS + resize2 - scrollDelay){ - this.playSound("se_ka") + + if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){ + var isJump = this.state.catJump var previousSelectedSong = this.selectedSong - this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move) + + if(!isJump){ + this.playSound("se_ka") + this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move) + }else{ + var currentCat = this.songs[this.selectedSong].category + var currentIdx = this.mod(this.songs.length, this.selectedSong) + + if(this.state.move > 0){ + var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) > currentIdx && song.category !== currentCat && song.canJump) + if(!nextSong){ + nextSong = this.songs[0] + } + }else{ + var isFirstInCat = this.songs.findIndex(song => song.category === currentCat) == this.selectedSong + if(!isFirstInCat){ + var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) < currentIdx && song.category === currentCat && song.canJump) + }else{ + var idx = this.songs.length - 1 + var nextSong + var lastCat + for(;idx>=0;idx--){ + if(this.songs[idx].category !== lastCat && this.songs[idx].action !== "back"){ + lastCat = this.songs[idx].category + if(nextSong){ + break + } + } + if(lastCat !== currentCat && idx < currentIdx){ + nextSong = idx + } + } + nextSong = this.songs[nextSong] + } + + if(!nextSong){ + var rev = [...this.songs].reverse() + nextSong = rev.find(song => song.canJump) + } + } + + this.selectedSong = this.songs.indexOf(nextSong) + this.state.catJump = false + } + if(previousSelectedSong !== this.selectedSong){ pageEvents.send("song-select-move", this.songs[this.selectedSong]) } this.state.move = 0 this.state.locked = 2 - localStorage["selectedSong"] = this.selectedSong + if(assets.customSongs){ + assets.customSelected = this.selectedSong + }else if(!p2.session){ + try{ + localStorage["selectedSong"] = this.selectedSong + }catch(e){} + } if(this.songs[this.selectedSong].action !== "back"){ var cat = this.songs[this.selectedSong].category diff --git a/public/src/js/strings.js b/public/src/js/strings.js index c2553d8..4ff9367 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -96,6 +96,7 @@ otherControls: "他のコントロール", otherTutorial: [ "%sはゲームを一時停止します", + "曲をえらぶしながら%sか%sキーを押してジャンルをスキップします", "むずかしさをえらぶしながら%sキーを押しながらオートモードを有効", "むずかしさをえらぶしながら%sキーを押しながらネットプレイモードを有効" ], @@ -287,6 +288,7 @@ function StringsEn(){ otherControls: "Other controls", otherTutorial: [ "%s \u2014 pause game", + '%s and %s while selecting song \u2014 navigate categories', "%s while selecting difficulty \u2014 enable autoplay mode", "%s while selecting difficulty \u2014 enable 2P mode" ], @@ -478,6 +480,7 @@ function StringsCn(){ otherControls: "其他控制", otherTutorial: [ "%s暂停游戏", + '%s and %s while selecting song \u2014 navigate categories', "选择难度时按住%s以启用自动模式", "选择难度时按住%s以启用网络对战模式" ], @@ -669,6 +672,7 @@ function StringsTw(){ otherControls: "其他控制", otherTutorial: [ "%s暫停遊戲", + '%s and %s while selecting song \u2014 navigate categories', "選擇難度時按住%s以啟用自動模式", "選擇難度時按住%s以啟用網上對打模式" ], @@ -860,6 +864,7 @@ function StringsKo(){ otherControls: "기타 컨트롤", otherTutorial: [ "%s \u2014 게임을 일시 중지합니다", + '%s and %s while selecting song \u2014 navigate categories', "난이도 선택 동안 %s 홀드 \u2014 오토 모드 활성화", "난이도 선택 동안 %s 홀드 \u2014 넷 플레이 모드 활성화" ], diff --git a/public/src/js/tutorial.js b/public/src/js/tutorial.js index 2d1302c..b753ede 100644 --- a/public/src/js/tutorial.js +++ b/public/src/js/tutorial.js @@ -59,13 +59,13 @@ class Tutorial{ 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 pauseKey = "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" + pauseKey, "SHIFT+LEFT", "SHIFT+RIGHT", "SHIFT", "CTRL" ] var keyIndex = 0 strings.tutorial.basics.forEach(string => {