Add .tja file support

This commit is contained in:
LoveEevee 2018-10-11 01:13:24 +03:00
parent 9b2c740b9d
commit 39655fc534
11 changed files with 395 additions and 70 deletions

3
.gitignore vendored
View File

@ -44,4 +44,5 @@ Temporary Items
public/songs public/songs
public/api public/api
taiko.db taiko.db
version.json version.json
public/index.html

View File

@ -10,9 +10,10 @@ Still in developement. Works best with Chrome.
Create a SQLite databse named `taiko.db` with the following schema: 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: Each song's data is contained within a directory under `public/songs/`. For example:

4
app.py
View File

@ -102,7 +102,9 @@ def route_api_songs():
], ],
'preview': preview, 'preview': preview,
'category': category_out['title'], 'category': category_out['title'],
'category_en': category_out['title_en'] 'category_en': category_out['title_en'],
'type': song[9],
'offset': song[10]
}) })
return jsonify(songs_out) return jsonify(songs_out)

View File

@ -8,8 +8,13 @@ class Controller{
this.snd = this.multiplayer ? "_p" + this.multiplayer : "" this.snd = this.multiplayer ? "_p" + this.multiplayer : ""
var backgroundURL = "/songs/" + this.selectedSong.folder + "/bg.png" 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 => { assets.songs.forEach(song => {
if(song.id == this.selectedSong.folder){ if(song.id == this.selectedSong.folder){
@ -168,9 +173,6 @@ class Controller{
getBindings(){ getBindings(){
return this.keyboard.getBindings() return this.keyboard.getBindings()
} }
getSongData(){
return this.game.getSongData()
}
getElapsedTime(){ getElapsedTime(){
return this.game.elapsedTime return this.game.elapsedTime
} }

View File

@ -49,7 +49,6 @@ class Game{
update(){ update(){
// Main operations // Main operations
this.updateTime() this.updateTime()
this.checkTiming()
this.updateCirclesStatus() this.updateCirclesStatus()
this.checkPlays() this.checkPlays()
// Event operations // Event operations
@ -277,6 +276,7 @@ class Game{
var started = this.fadeOutStarted var started = this.fadeOutStarted
if(started){ if(started){
var ms = this.elapsedTime var ms = this.elapsedTime
var musicDuration = this.controller.mainAsset.duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){ if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){ if(this.controller.multiplayer === 1){
p2.send("gameresults", this.getGlobalScore()) p2.send("gameresults", this.getGlobalScore())
@ -286,10 +286,10 @@ class Game{
this.controller.gameEnded() this.controller.gameEnded()
p2.send("gameend") p2.send("gameend")
this.musicFadeOut++ 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.controller.displayResults()
this.musicFadeOut++ 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() this.controller.clean()
if(this.controller.scoresheet){ if(this.controller.scoresheet){
this.controller.scoresheet.startRedraw() 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(){ playMainMusic(){
var ms = this.elapsedTime var ms = this.elapsedTime + this.controller.offset
if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms<this.fadeOutStarted + 1600)){ if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms < this.fadeOutStarted + 1600)){
if(this.controller.multiplayer !== 2){ if(this.controller.multiplayer !== 2){
this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000)) this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000))
} }
@ -362,9 +355,6 @@ class Game{
getCircles(){ getCircles(){
return this.songData.circles return this.songData.circles
} }
getSongData(){
return this.songData
}
updateCurrentCircle(){ updateCurrentCircle(){
this.currentCircle++ this.currentCircle++
} }

View File

@ -40,7 +40,7 @@ class loadSong{
}, reject) }, reject)
} }
})) }))
promises.push(loader.ajax(this.getOsuPath(this.selectedSong)).then(data => { promises.push(loader.ajax(this.getSongPath(this.selectedSong)).then(data => {
this.songData = data.replace(/\0/g, "").split("\n") this.songData = data.replace(/\0/g, "").split("\n")
})) }))
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
@ -50,8 +50,13 @@ class loadSong{
alert("An error occurred, please refresh") alert("An error occurred, please refresh")
}) })
} }
getOsuPath(selectedSong){ getSongPath(selectedSong){
return "/songs/" + selectedSong.folder + "/" + selectedSong.difficulty + ".osu" var directory = "/songs/" + selectedSong.folder + "/"
if(selectedSong.type === "tja"){
return directory + "main.tja"
}else{
return directory + selectedSong.difficulty + ".osu"
}
} }
setupMultiplayer(){ setupMultiplayer(){
if(this.multiplayer){ if(this.multiplayer){

View File

@ -1,5 +1,5 @@
class ParseSong{ class ParseOsu{
constructor(fileContent){ constructor(fileContent, offset){
this.osu = { this.osu = {
OFFSET: 0, OFFSET: 0,
MSPERBEAT: 1, MSPERBEAT: 1,
@ -36,11 +36,13 @@ class ParseSong{
} }
this.data = [] this.data = []
for(let line of fileContent){ for(let line of fileContent){
line = line.trim().replace(/\/\/.*/, "") line = line.replace(/\/\/.*/, "").trim()
if(line !== ""){ if(line !== ""){
this.data.push(line) this.data.push(line)
} }
} }
this.offset = (offset || 0) * -1000
this.soundOffset = 0
this.beatInfo = { this.beatInfo = {
beatInterval: 0, beatInterval: 0,
lastBeatInterval: 0, lastBeatInterval: 0,
@ -126,7 +128,7 @@ class ParseSong{
this.difficulty.lastMultiplier = sliderMultiplier this.difficulty.lastMultiplier = sliderMultiplier
} }
timingPoints.push({ timingPoints.push({
start: start, start: start + this.offset,
sliderMultiplier: sliderMultiplier, sliderMultiplier: sliderMultiplier,
measure: parseInt(values[this.osu.METER]), measure: parseInt(values[this.osu.METER]),
gogoTime: parseInt(values[this.osu.KIAIMODE]) gogoTime: parseInt(values[this.osu.KIAIMODE])
@ -139,20 +141,18 @@ class ParseSong{
var measureNumber = 0 var measureNumber = 0
for(var i = 0; i<this.timingPoints.length; i++){ for(var i = 0; i<this.timingPoints.length; i++){
if(this.timingPoints[i + 1]){ if(this.timingPoints[i + 1]){
var limit = this.timingPoints[i + 1].start var limit = this.timingPoints[i + 1].start - this.offset
}else{ }else{
var limit = this.circles[this.circles.length - 1].getMS() var limit = this.circles[this.circles.length - 1].getMS() - this.offset
} }
for(var j = this.timingPoints[i].start; j <= limit; j += this.beatInfo.beatInterval){ for(var start = this.timingPoints[i].start; start <= limit; start += this.beatInfo.beatInterval){
measures.push({ if(measureNumber === 0){
ms: j, measures.push({
nb: measureNumber, ms: start + this.offset,
speed: this.timingPoints[i].sliderMultiplier speed: this.timingPoints[i].sliderMultiplier
}) })
measureNumber++
if(measureNumber === this.timingPoints[i].measure + 1){
measureNumber = 0
} }
measureNumber = (measureNumber + 1) % (this.timingPoints[i].measure + 1)
} }
} }
return measures return measures
@ -242,9 +242,14 @@ class ParseSong{
var hitSound = parseInt(values[this.osu.HITSOUND]) var hitSound = parseInt(values[this.osu.HITSOUND])
var beatLength = speed var beatLength = speed
var lastMultiplier = this.difficulty.lastMultiplier var lastMultiplier = this.difficulty.lastMultiplier
if(circleID === 1 && start + this.offset < 0){
var offset = start + this.offset
this.soundOffset = offset
this.offset -= offset
}
for(var j = 0; j < this.timingPoints.length; j++){ for(var j = 0; j < this.timingPoints.length; j++){
if(this.timingPoints[j].start > start){ if(this.timingPoints[j].start - this.offset > start){
break break
} }
speed = this.timingPoints[j].sliderMultiplier speed = this.timingPoints[j].sliderMultiplier
@ -258,11 +263,11 @@ class ParseSong{
var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier)) var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier))
circles.push(new Circle({ circles.push(new Circle({
id: circleID, id: circleID,
start: start, start: start + this.offset,
type: "balloon", type: "balloon",
txt: "ふうせん", txt: "ふうせん",
speed: speed, speed: speed,
endTime: endTime, endTime: endTime + this.offset,
requiredHits: requiredHits, requiredHits: requiredHits,
gogoTime: gogoTime gogoTime: gogoTime
})) }))
@ -284,11 +289,11 @@ class ParseSong{
} }
circles.push(new Circle({ circles.push(new Circle({
id: circleID, id: circleID,
start: start, start: start + this.offset,
type: type, type: type,
txt: txt, txt: txt,
speed: speed, speed: speed,
endTime: endTime, endTime: endTime + this.offset,
gogoTime: gogoTime gogoTime: gogoTime
})) }))
@ -318,7 +323,7 @@ class ParseSong{
if(!emptyValue){ if(!emptyValue){
circles.push(new Circle({ circles.push(new Circle({
id: circleID, id: circleID,
start: start, start: start + this.offset,
type: type, type: type,
txt: txt, txt: txt,
speed: speed, speed: speed,
@ -334,16 +339,4 @@ class ParseSong{
} }
return circles 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
}
}
} }

330
public/src/js/parsetja.js Normal file
View File

@ -0,0 +1,330 @@
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 = () => {
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)
}
}
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
})
}
}
}
}
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 "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
}
}

View File

@ -88,7 +88,9 @@ class SongSelect{
skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default, skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default,
stars: song.stars, stars: song.stars,
category: song.category, category: song.category,
preview: song.preview || 0 preview: song.preview || 0,
type: song.type,
offset: song.offset
}) })
} }
this.songs.sort((a, b) => { this.songs.sort((a, b) => {
@ -470,7 +472,9 @@ class SongSelect{
"title": selectedSong.title, "title": selectedSong.title,
"folder": selectedSong.id, "folder": selectedSong.id,
"difficulty": this.difficultyId[difficulty], "difficulty": this.difficultyId[difficulty],
"category": selectedSong.category "category": selectedSong.category,
"type": selectedSong.type,
"offset": selectedSong.offset
}, shift, ctrl, touch) }, shift, ctrl, touch)
} }
toTitleScreen(){ toTitleScreen(){

View File

@ -48,7 +48,7 @@ class View{
this.drumroll = [] this.drumroll = []
this.beatInterval = this.controller.getSongData().beatInfo.beatInterval this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval
this.assets = new ViewAssets(this) this.assets = new ViewAssets(this)
this.touch = -Infinity this.touch = -Infinity
@ -289,16 +289,12 @@ class View{
} }
} }
drawMeasures(){ drawMeasures(){
var measures = this.controller.getSongData().measures var measures = this.controller.parsedSongData.measures
var currentTime = this.controller.getElapsedTime() var currentTime = this.controller.getElapsedTime()
measures.forEach((measure, index)=>{ measures.forEach((measure, index)=>{
var timeForDistance = this.posToMs(this.distanceForCircle, measure.speed) var timeForDistance = this.posToMs(this.distanceForCircle, measure.speed)
if( if(currentTime >= measure.ms - timeForDistance && currentTime <= measure.ms + 350){
currentTime >= measure.ms - timeForDistance
&& currentTime <= measure.ms + 350
&& measure.nb == 0
){
this.drawMeasure(measure) this.drawMeasure(measure)
} }
}) })

View File

@ -27,7 +27,7 @@
<script src="/src/js/assets.js?{{version.commit_short}}"></script> <script src="/src/js/assets.js?{{version.commit_short}}"></script>
<script src="/src/js/loadsong.js?{{version.commit_short}}"></script> <script src="/src/js/loadsong.js?{{version.commit_short}}"></script>
<script src="/src/js/parsesong.js?{{version.commit_short}}"></script> <script src="/src/js/parseosu.js?{{version.commit_short}}"></script>
<script src="/src/js/titlescreen.js?{{version.commit_short}}"></script> <script src="/src/js/titlescreen.js?{{version.commit_short}}"></script>
<script src="/src/js/scoresheet.js?{{version.commit_short}}"></script> <script src="/src/js/scoresheet.js?{{version.commit_short}}"></script>
<script src="/src/js/songselect.js?{{version.commit_short}}"></script> <script src="/src/js/songselect.js?{{version.commit_short}}"></script>
@ -51,6 +51,7 @@
<script src="/src/js/loader.js?{{version.commit_short}}"></script> <script src="/src/js/loader.js?{{version.commit_short}}"></script>
<script src="/src/js/canvastest.js?{{version.commit_short}}"></script> <script src="/src/js/canvastest.js?{{version.commit_short}}"></script>
<script src="/src/js/canvascache.js?{{version.commit_short}}"></script> <script src="/src/js/canvascache.js?{{version.commit_short}}"></script>
<script src="/src/js/parsetja.js?{{version.commit_short}}"></script>
</head> </head>
<body> <body>