diff --git a/.gitignore b/.gitignore index df15598..120c2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ Temporary Items public/songs public/api taiko.db -version.json \ No newline at end of file +version.json +public/index.html diff --git a/README.md b/README.md index a39eda3..01874aa 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,10 @@ Still in developement. Works best with Chrome. Create a SQLite databse named `taiko.db` with the following schema: - CREATE TABLE "songs" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `title_en` TEXT, `easy` INTEGER, `normal` INTEGER, `hard` INTEGER, `oni` INTEGER, `enabled` INTEGER NOT NULL, `category` INTEGER ) + CREATE TABLE "songs" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `title_en` TEXT, `easy` INTEGER, `normal` INTEGER, `hard` INTEGER, `oni` INTEGER, `enabled` INTEGER NOT NULL, `category` INTEGER, `type` TEXT , `offset` REAL NOT NULL ) + CREATE TABLE "categories" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `title_en` TEXT NOT NULL ) -When inserting rows, leave any difficulty columns as NULL if you don't intend to add notecharts for them. +When inserting song rows, leave any difficulty columns as NULL if you don't intend to add notecharts for them. Each song's data is contained within a directory under `public/songs/`. For example: diff --git a/app.py b/app.py index 66a2166..216b357 100644 --- a/app.py +++ b/app.py @@ -60,6 +60,26 @@ def get_osu_key(osu, section, key, default=None): return default +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 + + @app.teardown_appcontext def close_connection(exception): db = getattr(g, '_database', None) @@ -85,12 +105,19 @@ def route_api_songs(): categories[cat[0]] = {'title': cat[1], 'title_en': cat[2]} songs_out = [] for song in songs: - osus = [osu for osu in os.listdir('public/songs/%s' % song[0]) if osu in ['easy.osu', 'normal.osu', 'hard.osu', 'oni.osu']] - if osus: - osud = parse_osu('public/songs/%s/%s' % (song[0], osus[0])) - preview = int(get_osu_key(osud, 'General', 'PreviewTime', 0)) + type = song[9] + if type == "tja": + if os.path.isfile('public/songs/%s/main.tja' % song[0]): + preview = get_tja_preview('public/songs/%s/main.tja' % song[0]) + else: + preview = 0 else: - preview = 0 + osus = [osu for osu in os.listdir('public/songs/%s' % song[0]) if osu in ['easy.osu', 'normal.osu', 'hard.osu', 'oni.osu']] + if osus: + osud = parse_osu('public/songs/%s/%s' % (song[0], osus[0])) + preview = int(get_osu_key(osud, 'General', 'PreviewTime', 0)) + else: + preview = 0 category_out = categories[song[8]] if song[8] in categories else def_category songs_out.append({ @@ -102,7 +129,9 @@ def route_api_songs(): ], 'preview': preview, 'category': category_out['title'], - 'category_en': category_out['title_en'] + 'category_en': category_out['title_en'], + 'type': type, + 'offset': song[10] }) return jsonify(songs_out) diff --git a/public/src/js/controller.js b/public/src/js/controller.js index ae49f8d..a497129 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -8,8 +8,13 @@ class Controller{ this.snd = this.multiplayer ? "_p" + this.multiplayer : "" var backgroundURL = "/songs/" + this.selectedSong.folder + "/bg.png" - var songParser = new ParseSong(songData) - this.parsedSongData = songParser.getData() + + if(selectedSong.type === "tja"){ + this.parsedSongData = new ParseTja(songData, selectedSong.difficulty, selectedSong.offset) + }else{ + this.parsedSongData = new ParseOsu(songData, selectedSong.offset) + } + this.offset = this.parsedSongData.soundOffset assets.songs.forEach(song => { if(song.id == this.selectedSong.folder){ @@ -168,9 +173,6 @@ class Controller{ getBindings(){ return this.keyboard.getBindings() } - getSongData(){ - return this.game.getSongData() - } getElapsedTime(){ return this.game.elapsedTime } diff --git a/public/src/js/game.js b/public/src/js/game.js index 17475d3..0baf900 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -49,7 +49,6 @@ class Game{ update(){ // Main operations this.updateTime() - this.checkTiming() this.updateCirclesStatus() this.checkPlays() // Event operations @@ -277,6 +276,7 @@ class Game{ var started = this.fadeOutStarted if(started){ var ms = this.elapsedTime + var musicDuration = this.controller.mainAsset.duration * 1000 - this.controller.offset if(this.musicFadeOut === 0){ if(this.controller.multiplayer === 1){ p2.send("gameresults", this.getGlobalScore()) @@ -286,10 +286,10 @@ class Game{ this.controller.gameEnded() p2.send("gameend") this.musicFadeOut++ - }else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= this.controller.mainAsset.duration * 1000 + 250)){ + }else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= musicDuration + 250)){ this.controller.displayResults() this.musicFadeOut++ - }else if(this.musicFadeOut === 3 && (ms >= started + 9600 && ms >= this.controller.mainAsset.duration * 1000 + 1250)){ + }else if(this.musicFadeOut === 3 && (ms >= started + 9600 && ms >= musicDuration + 1250)){ this.controller.clean() if(this.controller.scoresheet){ this.controller.scoresheet.startRedraw() @@ -297,16 +297,9 @@ class Game{ } } } - checkTiming(){ - if(this.songData.timingPoints[this.currentTimingPoint + 1]){ - if(this.elapsedTime >= this.songData.timingPoints[this.currentTimingPoint + 1].start){ - this.currentTimingPoint++ - } - } - } playMainMusic(){ - var ms = this.elapsedTime - if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms { + promises.push(loader.ajax(this.getSongPath(this.selectedSong)).then(data => { this.songData = data.replace(/\0/g, "").split("\n") })) Promise.all(promises).then(() => { @@ -50,8 +50,13 @@ class loadSong{ alert("An error occurred, please refresh") }) } - getOsuPath(selectedSong){ - return "/songs/" + selectedSong.folder + "/" + selectedSong.difficulty + ".osu" + getSongPath(selectedSong){ + var directory = "/songs/" + selectedSong.folder + "/" + if(selectedSong.type === "tja"){ + return directory + "main.tja" + }else{ + return directory + selectedSong.difficulty + ".osu" + } } setupMultiplayer(){ if(this.multiplayer){ @@ -70,14 +75,20 @@ class loadSong{ this.selectedSong2 = { title: this.selectedSong.title, folder: this.selectedSong.folder, - difficulty: event.value + difficulty: event.value, + type: this.selectedSong.type, + offset: this.selectedSong.offset } - loader.ajax(this.getOsuPath(this.selectedSong2)).then(data => { - this.song2Data = data.replace(/\0/g, "").split("\n") + if(this.selectedSong.type === "tja"){ p2.send("gamestart") - }, () => { - p2.send("gamestart") - }) + }else{ + loader.ajax(this.getSongPath(this.selectedSong2)).then(data => { + this.song2Data = data.replace(/\0/g, "").split("\n") + p2.send("gamestart") + }, () => { + p2.send("gamestart") + }) + } } }else if(event.type === "gamestart"){ this.clean() diff --git a/public/src/js/parsesong.js b/public/src/js/parseosu.js similarity index 88% rename from public/src/js/parsesong.js rename to public/src/js/parseosu.js index 668d064..68948ab 100644 --- a/public/src/js/parsesong.js +++ b/public/src/js/parseosu.js @@ -1,5 +1,5 @@ -class ParseSong{ - constructor(fileContent){ +class ParseOsu{ + constructor(fileContent, offset){ this.osu = { OFFSET: 0, MSPERBEAT: 1, @@ -36,11 +36,13 @@ class ParseSong{ } this.data = [] for(let line of fileContent){ - line = line.trim().replace(/\/\/.*/, "") + line = line.replace(/\/\/.*/, "").trim() if(line !== ""){ this.data.push(line) } } + this.offset = (offset || 0) * -1000 + this.soundOffset = 0 this.beatInfo = { beatInterval: 0, lastBeatInterval: 0, @@ -126,7 +128,7 @@ class ParseSong{ this.difficulty.lastMultiplier = sliderMultiplier } timingPoints.push({ - start: start, + start: start + this.offset, sliderMultiplier: sliderMultiplier, measure: parseInt(values[this.osu.METER]), gogoTime: parseInt(values[this.osu.KIAIMODE]) @@ -139,20 +141,18 @@ class ParseSong{ var measureNumber = 0 for(var i = 0; i start){ + if(this.timingPoints[j].start - this.offset > start){ break } speed = this.timingPoints[j].sliderMultiplier @@ -258,11 +263,11 @@ class ParseSong{ var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier)) circles.push(new Circle({ id: circleID, - start: start, + start: start + this.offset, type: "balloon", txt: "ふうせん", speed: speed, - endTime: endTime, + endTime: endTime + this.offset, requiredHits: requiredHits, gogoTime: gogoTime })) @@ -284,11 +289,11 @@ class ParseSong{ } circles.push(new Circle({ id: circleID, - start: start, + start: start + this.offset, type: type, txt: txt, speed: speed, - endTime: endTime, + endTime: endTime + this.offset, gogoTime: gogoTime })) @@ -318,7 +323,7 @@ class ParseSong{ if(!emptyValue){ circles.push(new Circle({ id: circleID, - start: start, + start: start + this.offset, type: type, txt: txt, speed: speed, @@ -334,16 +339,4 @@ class ParseSong{ } return circles } - getData(){ - return { - generalInfo: this.generalInfo, - metaData: this.metadata, - editor: this.editor, - beatInfo: this.beatInfo, - difficulty: this.difficulty, - timingPoints: this.timingPoints, - circles: this.circles, - measures: this.measures - } - } } diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js new file mode 100644 index 0000000..ce1fc82 --- /dev/null +++ b/public/src/js/parsetja.js @@ -0,0 +1,338 @@ +class ParseTja{ + constructor(file, difficulty, offset){ + this.data = [] + for(let line of file){ + line = line.replace(/\/\/.*/, "").trim() + if(line !== ""){ + this.data.push(line) + } + } + this.difficulty = difficulty + this.offset = (offset || 0) * -1000 + this.soundOffset = 0 + this.noteTypes = [ + {name: false, txt: false}, + {name: "don", txt: "ドン"}, + {name: "ka", txt: "カッ"}, + {name: "daiDon", txt: "ドン(大)"}, + {name: "daiKa", txt: "カッ(大)"}, + {name: "drumroll", txt: "連打ーっ!!"}, + {name: "daiDrumroll", txt: "連打(大)ーっ!!"}, + {name: "balloon", txt: "ふうせん"}, + {name: false, txt: false}, + {name: "balloon", txt: "ふうせん"} + ] + this.courseTypes = ["easy", "normal", "hard", "oni"] + + this.metadata = this.parseMetadata() + this.measures = [] + this.beatInfo = {} + this.circles = this.parseCircles() + } + parseMetadata(){ + var metaNumbers = ["bpm", "offset"] + var inSong = false + var courses = {} + var currentCourse = {} + var courseName = this.difficulty + for(var lineNum = 0; lineNum < this.data.length; lineNum++){ + var line = this.data[lineNum] + + if(line.slice(0, 1) === "#"){ + + var name = line.slice(1).toLowerCase() + if(name === "start" && !inSong){ + + inSong = true + for(var name in currentCourse){ + if(!(courseName in courses)){ + courses[courseName] = {} + } + courses[courseName][name] = currentCourse[name] + } + courses[courseName].start = lineNum + 1 + courses[courseName].end = this.data.length + + }else if(name === "end" && inSong){ + inSong = false + courses[courseName].end = lineNum + } + + }else if(!inSong){ + + if(line.indexOf(":") > 0){ + + var [name, value] = this.split(line, ":") + name = name.toLowerCase().trim() + value = value.trim() + + if(name === "course"){ + if(value in this.courseTypes){ + courseName = this.courseTypes[value] + }else{ + courseName = value.toLowerCase() + } + }else if(name === "balloon"){ + value = value ? value.split(",").map(digit => parseInt(digit)) : [] + }else if(this.inArray(name, metaNumbers)){ + value = parseFloat(value) + } + + currentCourse[name] = value + } + + } + } + return courses + } + inArray(string, array){ + return array.indexOf(string) >= 0 + } + split(string, delimiter){ + var index = string.indexOf(delimiter) + if(index < 0){ + return [string, ""] + } + return [string.slice(0, index), string.slice(index + delimiter.length)] + } + 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 scroll = 1 + var measure = 4 + this.beatInfo.beatInterval = 60000 / bpm + var gogo = false + var barLine = true + + var balloonID = 0 + var balloons = meta.balloon || [] + + var lastDrumroll = false + var branch = false + var branchType + var branchPreference = "m" + + var currentMeasure = [] + var firstMeasure = true + var firstNote = true + var circles = [] + var circleID = 0 + + 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, + speed: speed + }) + if(firstMeasure){ + firstMeasure = false + var msPerMeasure = 60000 * measure / bpm + for(var measureMs = ms - msPerMeasure; measureMs > 0; measureMs -= msPerMeasure){ + this.measures.push({ + ms: measureMs, + speed: speed + }) + } + } + } + if(currentMeasure.length){ + for(var i = 0; i < currentMeasure.length; i++){ + var note = currentMeasure[i] + if(firstNote && note.type){ + firstNote = false + if(ms < 0){ + this.soundOffset = ms + ms = 0 + } + } + note.start = ms + if(note.endDrumroll){ + note.endDrumroll.endTime = ms + } + var msPerMeasure = 60000 * measure / bpm + ms += msPerMeasure / currentMeasure.length + } + for(var i = 0; i < currentMeasure.length; i++){ + var note = currentMeasure[i] + if(note.type){ + circleID++ + var circleObj = new Circle({ + id: circleID, + start: note.start, + type: note.type, + txt: note.txt, + speed: note.bpm * note.scroll / 60, + gogoTime: note.gogo, + endTime: note.endTime, + requiredHits: note.requiredHits + }) + if(lastDrumroll === note){ + lastDrumroll = circleObj + } + + circles.push(circleObj) + } + } + }else{ + var msPerMeasure = 60000 * measure / bpm + ms += msPerMeasure + } + } + + for(var lineNum = meta.start; lineNum < meta.end; lineNum++){ + var line = this.data[lineNum] + if(line.slice(0, 1) === "#"){ + + var line = line.slice(1).toLowerCase() + var [name, value] = this.split(line, " ") + + 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 "branchstart": + branch = true + branchType = "" + 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" + }else{ + branchPreference = "n" + } + break + case "branchend": + case "section": + branch = false + break + case "n": case "e": case "m": + branchType = name + 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 + } + + }else if(!branch || branch && branchType === branchPreference){ + + var string = line.split("") + + for(let symbol of string){ + + var error = false + switch(symbol){ + + case "0": + currentMeasure.push({ + bpm: bpm, + scroll: scroll + }) + break + case "1": case "2": case "3": case "4": + var type = this.noteTypes[symbol] + var circleObj = { + type: type.name, + txt: type.txt, + gogo: gogo, + bpm: bpm, + scroll: scroll + } + if(lastDrumroll){ + circleObj.endDrumroll = lastDrumroll + lastDrumroll = false + } + currentMeasure.push(circleObj) + break + case "5": case "6": case "7": case "9": + var type = this.noteTypes[symbol] + var circleObj = { + type: type.name, + txt: type.txt, + gogo: gogo, + bpm: bpm, + scroll: scroll + } + if(lastDrumroll){ + circleObj.endDrumroll = lastDrumroll + } + if(symbol === "7" || symbol === "9"){ + var hits = balloons[balloonID] + if(!hits || hits < 1){ + hits = 1 + } + circleObj.requiredHits = hits + balloonID++ + } + lastDrumroll = circleObj + currentMeasure.push(circleObj) + break + case "8": + if(lastDrumroll){ + currentMeasure.push({ + endDrumroll: lastDrumroll, + bpm: bpm, + scroll: scroll + }) + lastDrumroll = false + }else{ + currentMeasure.push({ + bpm: bpm, + scroll: scroll + }) + } + break + case ",": + pushMeasure() + currentMeasure = [] + break + default: + error = true + break + + } + if(error){ + break + } + } + + } + } + pushMeasure() + if(lastDrumroll){ + lastDrumroll.endTime = ms + } + + return circles + } +} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index ddf6a0c..d894a9a 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -88,7 +88,9 @@ class SongSelect{ skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default, stars: song.stars, category: song.category, - preview: song.preview || 0 + preview: song.preview || 0, + type: song.type, + offset: song.offset }) } this.songs.sort((a, b) => { @@ -470,7 +472,9 @@ class SongSelect{ "title": selectedSong.title, "folder": selectedSong.id, "difficulty": this.difficultyId[difficulty], - "category": selectedSong.category + "category": selectedSong.category, + "type": selectedSong.type, + "offset": selectedSong.offset }, shift, ctrl, touch) } toTitleScreen(){ diff --git a/public/src/js/view.js b/public/src/js/view.js index 1377c0e..13cbb94 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -48,7 +48,7 @@ class View{ this.drumroll = [] - this.beatInterval = this.controller.getSongData().beatInfo.beatInterval + this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval this.assets = new ViewAssets(this) this.touch = -Infinity @@ -216,13 +216,14 @@ class View{ //this.drawTime() } updateDonFaces(){ - if(this.controller.getElapsedTime() >= this.nextBeat){ + var ms = this.controller.getElapsedTime() + while(ms >= this.nextBeat){ this.nextBeat += this.beatInterval if(this.controller.getCombo() >= 50){ - this.currentBigDonFace = (this.currentBigDonFace + 1) % 2 - this.currentDonFace = (this.currentDonFace + 1) % 2 - } - else{ + var face = Math.floor(ms / this.beatInterval) % 2 + this.currentBigDonFace = face + this.currentDonFace = face + }else{ this.currentBigDonFace = 1 this.currentDonFace = 0 } @@ -289,16 +290,12 @@ class View{ } } drawMeasures(){ - var measures = this.controller.getSongData().measures + var measures = this.controller.parsedSongData.measures var currentTime = this.controller.getElapsedTime() measures.forEach((measure, index)=>{ var timeForDistance = this.posToMs(this.distanceForCircle, measure.speed) - if( - currentTime >= measure.ms - timeForDistance - && currentTime <= measure.ms + 350 - && measure.nb == 0 - ){ + if(currentTime >= measure.ms - timeForDistance && currentTime <= measure.ms + 350){ this.drawMeasure(measure) } }) diff --git a/templates/index.html b/templates/index.html index 240953d..ddb810d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -27,7 +27,7 @@ - + @@ -51,6 +51,7 @@ +