Merge pull request #186 from bui/songselect-add-crowns

SongSelect: Add crowns
This commit is contained in:
Bui 2020-03-07 13:54:14 +00:00 committed by GitHub
commit 5a1be53e21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 865 additions and 180 deletions

42
app.py
View File

@ -25,6 +25,7 @@ def get_db():
db = getattr(g, '_database', None) db = getattr(g, '_database', None)
if db is None: if db is None:
db = g._database = sqlite3.connect(DATABASE) db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db return db
@ -96,8 +97,8 @@ def route_api_preview():
if not song_row: if not song_row:
abort(400) abort(400)
song_type = song_row[0][12] song_type = song_row[0]['type']
prev_path = make_preview(song_id, song_type, song_row[0][15]) prev_path = make_preview(song_id, song_type, song_row[0]['preview'])
if not prev_path: if not prev_path:
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id) return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
@ -112,43 +113,44 @@ def route_api_songs():
raw_categories = query_db('select * from categories') raw_categories = query_db('select * from categories')
categories = {} categories = {}
for cat in raw_categories: for cat in raw_categories:
categories[cat[0]] = cat[1] categories[cat['id']] = cat['title']
raw_song_skins = query_db('select * from song_skins') raw_song_skins = query_db('select * from song_skins')
song_skins = {} song_skins = {}
for skin in raw_song_skins: for skin in raw_song_skins:
song_skins[skin[0]] = {'name': skin[1], 'song': skin[2], 'stage': skin[3], 'don': skin[4]} song_skins[skin[0]] = {'name': skin['name'], 'song': skin['song'], 'stage': skin['stage'], 'don': skin['don']}
songs_out = [] songs_out = []
for song in songs: for song in songs:
song_id = song[0] song_id = song['id']
song_type = song[12] song_type = song['type']
preview = song[15] preview = song['preview']
category_out = categories[song[11]] if song[11] in categories else "" category_out = categories[song['category']] if song['category'] in categories else ''
song_skin_out = song_skins[song[14]] if song[14] in song_skins else None song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None
maker = None maker = None
if song[17] == 0: if song['maker_id'] == 0:
maker = 0 maker = 0
elif song[17] and song[17] > 0: elif song['maker_id'] and song['maker_id'] > 0:
maker = {'name': song[18], 'url': song[19], 'id': song[17]} maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']}
songs_out.append({ songs_out.append({
'id': song_id, 'id': song_id,
'title': song[1], 'title': song['title'],
'title_lang': song[2], 'title_lang': song['title_lang'],
'subtitle': song[3], 'subtitle': song['subtitle'],
'subtitle_lang': song[4], 'subtitle_lang': song['subtitle_lang'],
'stars': [ 'stars': [
song[5], song[6], song[7], song[8], song[9] song['easy'], song['normal'], song['hard'], song['oni'], song['ura']
], ],
'preview': preview, 'preview': preview,
'category': category_out, 'category': category_out,
'type': song_type, 'type': song_type,
'offset': song[13], 'offset': song['offset'],
'song_skin': song_skin_out, 'song_skin': song_skin_out,
'volume': song[16], 'volume': song['volume'],
'maker': maker 'maker': maker,
'hash': song['hash']
}) })
return jsonify(songs_out) return jsonify(songs_out)

View File

@ -1,6 +1,7 @@
var assets = { var assets = {
"js": [ "js": [
"lib/fontdetect.min.js", "lib/fontdetect.min.js",
"lib/md5.min.js",
"loadsong.js", "loadsong.js",
"parseosu.js", "parseosu.js",
"titlescreen.js", "titlescreen.js",
@ -30,7 +31,8 @@ var assets = {
"session.js", "session.js",
"importsongs.js", "importsongs.js",
"logo.js", "logo.js",
"settings.js" "settings.js",
"scorestorage.js"
], ],
"css": [ "css": [
"main.css", "main.css",

View File

@ -1273,27 +1273,29 @@
ctx.translate(-47, -39) ctx.translate(-47, -39)
ctx.miterLimit = 1.7 ctx.miterLimit = 1.7
if(!this.crownCache.w){ if(config.whiteOutline){
this.crownCache.resize(140, 140, config.ratio) if(!this.crownCache.w){
this.crownCache.resize(140, 140, config.ratio)
}
var offset = 140 / 2 - 94 / 2
this.crownCache.get({
ctx: ctx,
x: -offset,
y: -offset,
w: 140,
h: 140,
id: "crown"
}, ctx => {
ctx.save()
ctx.translate(offset, offset)
ctx.strokeStyle = "#fff"
ctx.lineWidth = 35
ctx.miterLimit = 1.7
ctx.filter = "blur(1.5px)"
ctx.stroke(this.crownPath)
ctx.restore()
})
} }
var offset = 140 / 2 - 94 / 2
this.crownCache.get({
ctx: ctx,
x: -offset,
y: -offset,
w: 140,
h: 140,
id: "crown"
}, ctx => {
ctx.save()
ctx.translate(offset, offset)
ctx.strokeStyle = "#fff"
ctx.lineWidth = 35
ctx.miterLimit = 1.7
ctx.filter = "blur(1.5px)"
ctx.stroke(this.crownPath)
ctx.restore()
})
if(config.shine){ if(config.shine){
ctx.strokeStyle = "#fff" ctx.strokeStyle = "#fff"
@ -1302,7 +1304,7 @@
ctx.globalAlpha = 1 - config.shine ctx.globalAlpha = 1 - config.shine
} }
ctx.strokeStyle = "#000" ctx.strokeStyle = config.type ? "#000" : "rgba(255, 193, 0, 0.5)"
ctx.lineWidth = 18 ctx.lineWidth = 18
ctx.stroke(this.crownPath) ctx.stroke(this.crownPath)
@ -1313,21 +1315,25 @@
ctx.globalAlpha = 1 - config.shine ctx.globalAlpha = 1 - config.shine
} }
var grd = ctx.createLinearGradient(0, 0, 94, 0) if(config.type){
if(config.type === "gold"){ var grd = ctx.createLinearGradient(0, 0, 94, 0)
grd.addColorStop(0, "#ffffc5") if(config.type === "gold"){
grd.addColorStop(0.23, "#ffff44") grd.addColorStop(0, "#ffffc5")
grd.addColorStop(0.53, "#efbd12") grd.addColorStop(0.23, "#ffff44")
grd.addColorStop(0.83, "#ffff44") grd.addColorStop(0.53, "#efbd12")
grd.addColorStop(1, "#efbd12") grd.addColorStop(0.83, "#ffff44")
}else if(config.type === "silver"){ grd.addColorStop(1, "#efbd12")
grd.addColorStop(0, "#d6efef") }else if(config.type === "silver"){
grd.addColorStop(0.23, "#bddfde") grd.addColorStop(0, "#d6efef")
grd.addColorStop(0.53, "#97c1c0") grd.addColorStop(0.23, "#bddfde")
grd.addColorStop(0.83, "#bddfde") grd.addColorStop(0.53, "#97c1c0")
grd.addColorStop(1, "#97c1c0") grd.addColorStop(0.83, "#bddfde")
grd.addColorStop(1, "#97c1c0")
}
ctx.fillStyle = grd
}else{
ctx.fillStyle = "#ffdb2c"
} }
ctx.fillStyle = grd
ctx.fill(this.crownPath) ctx.fill(this.crownPath)
ctx.restore() ctx.restore()
@ -1347,10 +1353,10 @@
var secondTop = config.multiplayer ? 0 : 8 var secondTop = config.multiplayer ? 0 : 8
config.percentage = Math.max(0, Math.min(1, config.percentage)) config.percentage = Math.max(0, Math.min(1, config.percentage))
var cleared = config.percentage - 1 / 50 >= config.clear var cleared = config.percentage >= config.clear
var gaugeW = 14 * 50 var gaugeW = 14 * 50
var gaugeClear = gaugeW * config.clear var gaugeClear = gaugeW * (config.clear - 1 / 50)
var gaugeFilled = gaugeW * config.percentage var gaugeFilled = gaugeW * config.percentage
ctx.fillStyle = "#000" ctx.fillStyle = "#000"

View File

@ -176,7 +176,7 @@ class Controller{
gameEnded(){ gameEnded(){
var score = this.getGlobalScore() var score = this.getGlobalScore()
var vp var vp
if(Math.round(score.gauge / 2) - 1 >= 25){ if(this.game.rules.clearReached(score.gauge)){
if(score.bad === 0){ if(score.bad === 0){
vp = "fullcombo" vp = "fullcombo"
this.playSound("v_fullcombo", 1.350) this.playSound("v_fullcombo", 1.350)

View File

@ -18,10 +18,11 @@ class Game{
title: selectedSong.title, title: selectedSong.title,
difficulty: this.rules.difficulty difficulty: this.rules.difficulty
} }
this.HPGain = 100 / this.songData.circles.filter(circle => { var combo = this.songData.circles.filter(circle => {
var type = circle.type var type = circle.type
return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active) return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active)
}).length }).length
this.soulPoints = this.rules.soulPoints(combo)
this.paused = false this.paused = false
this.started = false this.started = false
this.mainMusicPlaying = false this.mainMusicPlaying = false
@ -639,12 +640,15 @@ class Game{
switch(score){ switch(score){
case 450: case 450:
this.globalScore.good++ this.globalScore.good++
this.globalScore.gauge += this.soulPoints.good
break break
case 230: case 230:
this.globalScore.ok++ this.globalScore.ok++
this.globalScore.gauge += this.soulPoints.ok
break break
case 0: case 0:
this.globalScore.bad++ this.globalScore.bad++
this.globalScore.gauge += this.soulPoints.bad
break break
} }
if (this.songData.scoremode) { if (this.songData.scoremode) {
@ -658,12 +662,10 @@ class Game{
} }
} }
// Gauge update // Gauge update
if(score !== 0){ if(this.globalScore.gauge < 0){
this.globalScore.gauge += this.HPGain
}else if(this.globalScore.gauge - this.HPGain > 0){
this.globalScore.gauge -= this.HPGain
}else{
this.globalScore.gauge = 0 this.globalScore.gauge = 0
}else if(this.globalScore.gauge > 10000){
this.globalScore.gauge = 10000
} }
// Points update // Points update
if (this.songData.scoremode == 2) { if (this.songData.scoremode == 2) {
@ -730,10 +732,6 @@ class Game{
this.currentCircle = closestCircle 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){ if(this.controller.multiplayer === 1){
p2.send("branch", activeName) p2.send("branch", activeName)
} }

View File

@ -18,7 +18,51 @@ class GameRules{
this.bad = 13 / 2 * frame this.bad = 13 / 2 * frame
break break
} }
switch(this.difficulty){
case "easy":
this.gaugeClear = 30 / 50
break
case "normal":
case "hard":
this.gaugeClear = 35 / 50
break
case "oni":
case "ura":
this.gaugeClear = 40 / 50
break
}
this.daiLeniency = 2 * frame this.daiLeniency = 2 * frame
} }
soulPoints(combo){
var good, ok, bad
switch(this.difficulty){
case "easy":
good = Math.floor(10000 / combo * 1.575)
ok = Math.floor(good * 0.75)
bad = Math.ceil(good * -2)
break
case "normal":
good = Math.floor(10000 / combo / 0.7)
ok = Math.floor(good * 0.75)
bad = Math.ceil(good / -0.75)
break
case "hard":
good = Math.floor(10000 / combo * 1.5)
ok = Math.floor(good * 0.75)
bad = Math.ceil(good / -0.8)
break
case "oni":
case "ura":
good = Math.floor(10000 / combo / 0.7)
ok = Math.floor(good * 0.5)
bad = Math.ceil(good * -1.6)
break
}
return {good: good, ok: ok, bad: bad}
}
clearReached(gauge){
var gaugePercent = Math.round(gauge / 200) / 50
return gaugePercent >= this.gaugeClear
}
} }

View File

@ -274,6 +274,13 @@
if(songObj.stars.length !== 0){ if(songObj.stars.length !== 0){
this.songs[index] = songObj this.songs[index] = songObj
} }
var hash = md5.base64(event.target.result).slice(0, -2)
songObj.hash = hash
scoreStorage.songTitles[songObj.title] = hash
var score = scoreStorage.get(hash)
if(score){
score.title = songObj.title
}
}).catch(() => {}) }).catch(() => {})
reader.readAsText(file, "sjis") reader.readAsText(file, "sjis")
return promise return promise
@ -314,6 +321,13 @@
} }
this.songs[index] = songObj this.songs[index] = songObj
songObj.category = category || this.getCategory(file) songObj.category = category || this.getCategory(file)
var hash = md5.base64(event.target.result).slice(0, -2)
songObj.hash = hash
scoreStorage.songTitles[songObj.title] = hash
var score = scoreStorage.get(hash)
if(score){
score.title = songObj.title
}
}).catch(() => {}) }).catch(() => {})
reader.readAsText(file) reader.readAsText(file)
return promise return promise

10
public/src/js/lib/md5.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -206,6 +206,19 @@ class Loader{
settings = new Settings() settings = new Settings()
pageEvents.setKbd() pageEvents.setKbd()
scoreStorage = new ScoreStorage()
for(var i in assets.songsDefault){
var song = assets.songsDefault[i]
if(!song.hash){
song.hash = song.title
}
scoreStorage.songTitles[song.title] = song.hash
var score = scoreStorage.get(song.hash)
if(score){
score.title = song.title
}
}
Promise.all(this.promises).then(() => { Promise.all(this.promises).then(() => {
this.canvasTest.drawAllImages().then(result => { this.canvasTest.drawAllImages().then(result => {
perf.allImg = result perf.allImg = result

View File

@ -83,6 +83,7 @@ var perf = {
var strings var strings
var vectors var vectors
var settings var settings
var scoreStorage
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){

View File

@ -1,6 +1,7 @@
class Scoresheet{ class Scoresheet{
constructor(controller, results, multiplayer, touchEnabled){ constructor(controller, results, multiplayer, touchEnabled){
this.controller = controller this.controller = controller
this.resultsObj = results
this.results = {} this.results = {}
for(var i in results){ for(var i in results){
this.results[i] = results[i].toString() this.results[i] = results[i].toString()
@ -54,6 +55,7 @@ class Scoresheet{
"ura": 4 "ura": 4
} }
this.scoreSaved = false
this.redrawRunning = true this.redrawRunning = true
this.redrawBind = this.redraw.bind(this) this.redrawBind = this.redraw.bind(this)
this.redraw() this.redraw()
@ -248,6 +250,9 @@ class Scoresheet{
if(this.state.screen === "fadeIn" && elapsed < 1000){ if(this.state.screen === "fadeIn" && elapsed < 1000){
bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2) bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2)
} }
if((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved){
this.saveScore()
}
if(bgOffset){ if(bgOffset){
ctx.save() ctx.save()
@ -319,15 +324,18 @@ class Scoresheet{
var elapsed = 0 var elapsed = 0
} }
var gaugePercent = Math.round(this.results.gauge / 2) / 50 var gaugePercent = Math.round(this.results.gauge / 200) / 50
var gaugeClear = [this.controller.game.rules.gaugeClear]
if(players === 2){ if(players === 2){
var gauge2 = Math.round(p2.results.gauge / 2) / 50 gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear)
if(gauge2 > gaugePercent){ }
gaugePercent = gauge2 var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000
if(players === 2){
var gauge2 = Math.round(p2.results.gauge / 200) / 50
if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){
failedOffset = 0
} }
} }
var gaugeClear = 25 / 50
var failedOffset = gaugePercent >= gaugeClear ? 0 : -2000
if(elapsed >= 3100 + failedOffset){ if(elapsed >= 3100 + failedOffset){
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
ctx.save() ctx.save()
@ -335,8 +343,8 @@ class Scoresheet{
if(p === 1){ if(p === 1){
results = p2.results results = p2.results
} }
var resultGauge = Math.round(results.gauge / 2) / 50 var resultGauge = Math.round(results.gauge / 200) / 50
var clear = resultGauge >= gaugeClear var clear = resultGauge >= gaugeClear[p]
if(p === 1 || !this.multiplayer && clear){ if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290) ctx.translate(0, 290)
} }
@ -570,7 +578,7 @@ class Scoresheet{
if(this.tetsuoHanaClass){ if(this.tetsuoHanaClass){
this.tetsuoHana.classList.remove(this.tetsuoHanaClass) this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
} }
this.tetsuoHanaClass = gaugePercent >= gaugeClear ? "dance" : "failed" this.tetsuoHanaClass = this.controller.game.rules.clearReached(this.results.gauge) ? "dance" : "failed"
this.tetsuoHana.classList.add(this.tetsuoHanaClass) this.tetsuoHana.classList.add(this.tetsuoHanaClass)
} }
} }
@ -589,25 +597,26 @@ class Scoresheet{
results = p2.results results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
var gaugePercent = Math.round(results.gauge / 2) / 50 var gaugePercent = Math.round(results.gauge / 200) / 50
var w = 712 var w = 712
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: 558 + w, x: 558 + w,
y: 116, y: 116,
clear: 25 / 50, clear: gaugeClear[p],
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
scale: w / 788, scale: w / 788,
scoresheet: true, scoresheet: true,
blue: p === 1 blue: p === 1
}) })
var rules = p === 0 ? this.controller.game.rules : this.controller.syncWith.game.rules
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: 1215, x: 1215,
y: 144, y: 144,
scale: 36 / 42, scale: 36 / 42,
cleared: gaugePercent - 1 / 50 >= 25 / 50 cleared: rules.clearReached(results.gauge)
}) })
} }
}) })
@ -625,7 +634,8 @@ class Scoresheet{
results = p2.results results = p2.results
} }
var crownType = null var crownType = null
if(Math.round(results.gauge / 2) - 1 >= 25){ var rules = p === 0 ? this.controller.game.rules : this.controller.syncWith.game.rules
if(rules.clearReached(results.gauge)){
crownType = results.bad === "0" ? "gold" : "silver" crownType = results.bad === "0" ? "gold" : "silver"
} }
if(crownType !== null){ if(crownType !== null){
@ -668,6 +678,7 @@ class Scoresheet{
y: 218, y: 218,
scale: crownScale, scale: crownScale,
shine: shine, shine: shine,
whiteOutline: true,
ratio: ratio ratio: ratio
}) })
@ -849,6 +860,37 @@ class Scoresheet{
return Date.now() return Date.now()
} }
saveScore(){
if(!this.controller.autoPlayEnabled){
if(this.resultsObj.points < 0){
this.resultsObj.points = 0
}
var title = this.controller.selectedSong.originalTitle
var hash = this.controller.selectedSong.hash
var difficulty = this.resultsObj.difficulty
var oldScore = scoreStorage.get(hash, difficulty, true)
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
var crown = ""
if(clearReached){
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
}
if(!oldScore || oldScore.points <= this.resultsObj.points){
if(oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)){
crown = oldScore.crown
}
this.resultsObj.crown = crown
delete this.resultsObj.title
delete this.resultsObj.difficulty
delete this.resultsObj.gauge
scoreStorage.add(hash, difficulty, this.resultsObj, true, title)
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
oldScore.crown = crown
scoreStorage.add(hash, difficulty, oldScore, true, title)
}
}
this.scoreSaved = true
}
clean(){ clean(){
this.keyboard.clean() this.keyboard.clean()
this.gamepad.clean() this.gamepad.clean()

View File

@ -0,0 +1,151 @@
class ScoreStorage{
constructor(){
this.scores = {}
this.songTitles = {}
this.difficulty = ["oni", "ura", "hard", "normal", "easy"]
this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"]
this.crownValue = ["", "silver", "gold"]
this.load()
}
load(){
this.scores = {}
this.scoreStrings = {}
try{
var localScores = localStorage.getItem("scoreStorage")
if(localScores){
this.scoreStrings = JSON.parse(localScores)
}
}catch(e){}
for(var hash in this.scoreStrings){
var scoreString = this.scoreStrings[hash]
var songAdded = false
if(typeof scoreString === "string" && scoreString){
var diffArray = scoreString.split(";")
for(var i in this.difficulty){
if(diffArray[i]){
var crown = parseInt(diffArray[i].slice(0, 1)) || 0
var score = {
crown: this.crownValue[crown] || ""
}
var scoreArray = diffArray[i].slice(1).split(",")
for(var j in this.scoreKeys){
var name = this.scoreKeys[j]
var value = parseInt(scoreArray[j], 36) || 0
if(value < 0){
value = 0
}
score[name] = value
}
if(!songAdded){
this.scores[hash] = {title: null}
songAdded = true
}
this.scores[hash][this.difficulty[i]] = score
}
}
}
}
}
save(){
for(var hash in this.scores){
this.writeString(hash)
}
this.write()
}
write(){
try{
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
}catch(e){}
}
writeString(hash){
var score = this.scores[hash]
var diffArray = []
var notEmpty = false
for(var i = this.difficulty.length; i--;){
var diff = this.difficulty[i]
if(score[diff]){
var scoreArray = []
var crown = this.crownValue.indexOf(score[diff].crown).toString()
for(var j in this.scoreKeys){
var name = this.scoreKeys[j]
var value = score[diff][name]
value = Math.floor(value).toString(36)
scoreArray.push(value)
}
diffArray.unshift(crown + scoreArray.join(","))
notEmpty = true
}else if(notEmpty){
diffArray.unshift("")
}
}
this.scoreStrings[hash] = diffArray.join(";")
}
titleHash(song){
if(song in this.songTitles){
return this.songTitles[song]
}else{
return song
}
}
get(song, difficulty, isHash){
if(!song){
return this.scores
}else{
var hash = isHash ? song : this.titleHash(song)
if(difficulty){
if(hash in this.scores){
return this.scores[hash][difficulty]
}
}else{
return this.scores[hash]
}
}
}
add(song, difficulty, scoreObject, isHash, setTitle){
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){
this.scores[hash] = {}
}
if(setTitle){
this.scores[hash].title = setTitle
}
this.scores[hash][difficulty] = scoreObject
this.writeString(hash)
this.write()
}
template(){
var template = {crown: ""}
for(var i in this.scoreKeys){
var name = this.scoreKeys[i]
template[name] = 0
}
return template
}
remove(song, difficulty, isHash){
var hash = isHash ? song : this.titleHash(song)
if(hash in this.scores){
if(difficulty){
if(difficulty in this.scores[hash]){
delete this.scores[hash][difficulty]
var noDiff = true
for(var i in this.difficulty){
if(this.scores[hash][this.difficulty[i]]){
noDiff = false
break
}
}
if(noDiff){
delete this.scores[hash]
delete this.scoreStrings[hash]
}else{
this.writeString(hash)
}
}
}else{
delete this.scores[hash]
delete this.scoreStrings[hash]
}
this.write()
}
}
}

View File

@ -113,6 +113,7 @@ class SongSelect{
this.songs.push({ this.songs.push({
id: song.id, id: song.id,
title: title, title: title,
originalTitle: song.title,
subtitle: subtitle, subtitle: subtitle,
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,
@ -124,7 +125,8 @@ class SongSelect{
music: song.music, music: song.music,
volume: song.volume, volume: song.volume,
maker: song.maker, maker: song.maker,
canJump: true canJump: true,
hash: song.hash || song.title
}) })
} }
this.songs.sort((a, b) => { this.songs.sort((a, b) => {
@ -738,13 +740,15 @@ class SongSelect{
new LoadSong({ new LoadSong({
"title": selectedSong.title, "title": selectedSong.title,
"originalTitle": selectedSong.originalTitle,
"folder": selectedSong.id, "folder": selectedSong.id,
"difficulty": this.difficultyId[difficulty], "difficulty": this.difficultyId[difficulty],
"category": selectedSong.category, "category": selectedSong.category,
"type": selectedSong.type, "type": selectedSong.type,
"offset": selectedSong.offset, "offset": selectedSong.offset,
"songSkin": selectedSong.songSkin, "songSkin": selectedSong.songSkin,
"stars": selectedSong.stars[difficulty] "stars": selectedSong.stars[difficulty],
"hash": selectedSong.hash
}, autoplay, multiplayer, touch) }, autoplay, multiplayer, touch)
} }
toOptions(moveBy){ toOptions(moveBy){
@ -957,86 +961,6 @@ class SongSelect{
} }
} }
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({
ctx: ctx,
x: frameLeft,
y: frameTop,
w: textW + 53 + 60,
h: this.songAsset.marginTop + 15,
id: "song"
}, ctx => {
this.draw.layeredText({
ctx: ctx,
text: strings.selectSong,
fontSize: 48,
fontFamily: this.font,
x: 53,
y: 30,
width: textW,
letterSpacing: strings.id === "en" ? 0 : 2,
forceShadow: true
}, [
{x: -2, y: -2, outline: "#000", letterBorder: 22},
{},
{x: 2, y: 2, shadow: [3, 3, 3]},
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
{x: -2, y: -2, outline: "#ff797b"},
{outline: "#f70808"},
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
])
})
var category = this.songs[this.selectedSong].category
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{
var categoryName = category
}
this.draw.layeredText({
ctx: ctx,
text: categoryName,
fontSize: 40,
fontFamily: this.font,
x: 280 / 2,
y: 38,
width: 255,
align: "center",
forceShadow: true
}, [
{outline: selectedSong.skin.outline, letterBorder: 12, shadow: [3, 3, 3]},
{fill: "#fff"}
])
}
})
}
if(screen === "song"){ if(screen === "song"){
if(this.songs[this.selectedSong].stars){ if(this.songs[this.selectedSong].stars){
selectedWidth = this.songAsset.selectedWidth selectedWidth = this.songAsset.selectedWidth
@ -1226,6 +1150,86 @@ class SongSelect{
} }
} }
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({
ctx: ctx,
x: frameLeft,
y: frameTop,
w: textW + 53 + 60,
h: this.songAsset.marginTop + 15,
id: "song"
}, ctx => {
this.draw.layeredText({
ctx: ctx,
text: strings.selectSong,
fontSize: 48,
fontFamily: this.font,
x: 53,
y: 30,
width: textW,
letterSpacing: strings.id === "en" ? 0 : 2,
forceShadow: true
}, [
{x: -2, y: -2, outline: "#000", letterBorder: 22},
{},
{x: 2, y: 2, shadow: [3, 3, 3]},
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
{x: -2, y: -2, outline: "#ff797b"},
{outline: "#f70808"},
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
])
})
var category = this.songs[this.selectedSong].category
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{
var categoryName = category
}
this.draw.layeredText({
ctx: ctx,
text: categoryName,
fontSize: 40,
fontFamily: this.font,
x: 280 / 2,
y: 38,
width: 255,
align: "center",
forceShadow: true
}, [
{outline: selectedSong.skin.outline, letterBorder: 12, shadow: [3, 3, 3]},
{fill: "#fff"}
])
}
})
}
var currentSong = this.songs[this.selectedSong] var currentSong = this.songs[this.selectedSong]
var highlight = 0 var highlight = 0
if(!currentSong.stars){ if(!currentSong.stars){
@ -1254,6 +1258,15 @@ class SongSelect{
this.currentSongCache.clear() this.currentSongCache.clear()
} }
if(selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
x: winW / 2 - selectedWidth / 2 + xOffset,
y: songTop + this.songAsset.height - selectedHeight
})
}
this.draw.songFrame({ this.draw.songFrame({
ctx: ctx, ctx: ctx,
x: winW / 2 - selectedWidth / 2 + xOffset, x: winW / 2 - selectedWidth / 2 + xOffset,
@ -1385,6 +1398,20 @@ class SongSelect{
} }
var drawDifficulty = (ctx, i, currentUra) => { var drawDifficulty = (ctx, i, currentUra) => {
if(currentSong.stars[i] || currentUra){ if(currentSong.stars[i] || currentUra){
var score = scoreStorage.get(currentSong.hash)
var crownDiff = currentUra ? "ura" : this.difficultyId[i]
var crownType = ""
if(score && score[crownDiff]){
crownType = score[crownDiff].crown
}
this.draw.crown({
ctx: ctx,
type: crownType,
x: songSel ? x + 33 + i * 60 : x + 402 + i * 100,
y: songSel ? y + 75 : y + 30,
scale: 0.25,
ratio: this.ratio / this.pixelRatio
})
if(songSel){ if(songSel){
var _x = x + 33 + i * 60 var _x = x + 33 + i * 60
var _y = y + 120 var _y = y + 120
@ -1910,6 +1937,7 @@ class SongSelect{
drawClosedSong(config){ drawClosedSong(config){
var ctx = config.ctx var ctx = config.ctx
this.drawSongCrown(config)
config.width = this.songAsset.width config.width = this.songAsset.width
config.height = this.songAsset.height config.height = this.songAsset.height
config.border = this.songAsset.border config.border = this.songAsset.border
@ -1959,6 +1987,39 @@ class SongSelect{
} }
} }
drawSongCrown(config){
if(!config.song.action && config.song.hash){
var ctx = config.ctx
var score = scoreStorage.get(config.song.hash)
for(var i = this.difficultyId.length; i--;){
var diff = this.difficultyId[i]
if(!score){
break
}
if(config.song.stars[i] && score[diff] && score[diff].crown){
this.draw.crown({
ctx: ctx,
type: score[diff].crown,
x: config.x + this.songAsset.width / 2,
y: config.y - 13,
scale: 0.3,
ratio: this.ratio / this.pixelRatio
})
this.draw.diffIcon({
ctx: ctx,
diff: i,
x: config.x + this.songAsset.width / 2 + 8,
y: config.y - 8,
scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5,
border: 6.5,
small: true
})
break
}
}
}
}
startPreview(loadOnly){ startPreview(loadOnly){
var currentSong = this.songs[this.selectedSong] var currentSong = this.songs[this.selectedSong]
var id = currentSong.id var id = currentSong.id

View File

@ -17,6 +17,7 @@
this.songBg = document.getElementById("songbg") this.songBg = document.getElementById("songbg")
this.songStage = document.getElementById("song-stage") this.songStage = document.getElementById("song-stage")
this.rules = this.controller.game.rules
this.portraitClass = false this.portraitClass = false
this.touchp2Class = false this.touchp2Class = false
this.darkDonBg = false this.darkDonBg = false
@ -347,7 +348,7 @@
} }
var score = this.controller.getGlobalScore() var score = this.controller.getGlobalScore()
var gaugePercent = Math.round(score.gauge / 2) / 50 var gaugePercent = Math.round(score.gauge / 200) / 50
if(this.multiplayer === 2){ if(this.multiplayer === 2){
var scoreImg = "bg_score_p2" var scoreImg = "bg_score_p2"
@ -497,7 +498,7 @@
ctx: ctx, ctx: ctx,
x: winW, x: winW,
y: this.multiplayer === 2 ? 468 : 273, y: this.multiplayer === 2 ? 468 : 273,
clear: 25 / 50, clear: this.rules.gaugeClear,
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
scale: 0.7, scale: 0.7,
@ -509,7 +510,7 @@
x: winW - 40, x: winW - 40,
y: this.multiplayer === 2 ? 484 : 293, y: this.multiplayer === 2 ? 484 : 293,
scale: 0.75, scale: 0.75,
cleared: gaugePercent - 1 / 50 >= 25 / 50 cleared: this.rules.clearReached(score.gauge)
}) })
// Note bar // Note bar
@ -572,7 +573,7 @@
ctx: ctx, ctx: ctx,
x: winW, x: winW,
y: this.multiplayer === 2 ? 357 : 135, y: this.multiplayer === 2 ? 357 : 135,
clear: 25 / 50, clear: this.rules.gaugeClear,
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
multiplayer: this.multiplayer === 2, multiplayer: this.multiplayer === 2,
@ -582,7 +583,7 @@
ctx: ctx, ctx: ctx,
x: winW - 57, x: winW - 57,
y: this.multiplayer === 2 ? 378 : 165, y: this.multiplayer === 2 ? 378 : 165,
cleared: gaugePercent - 1 / 50 >= 25 / 50 cleared: this.rules.clearReached(score.gauge)
}) })
// Note bar // Note bar
@ -1881,8 +1882,8 @@
} }
}else{ }else{
var animation = this.assets.don.getAnimation() var animation = this.assets.don.getAnimation()
var gauge = this.controller.getGlobalScore().gauge var score = this.controller.getGlobalScore()
var cleared = Math.round(gauge / 2) - 1 >= 25 var cleared = this.rules.clearReached(score.gauge)
if(animation === "gogo" || cleared && animation === "normal" || !cleared && animation === "clear"){ if(animation === "gogo" || cleared && animation === "normal" || !cleared && animation === "clear"){
this.assets.don.normalAnimation() this.assets.don.normalAnimation()
} }

View File

@ -42,16 +42,20 @@ class ViewAssets{
var length = this.don.getAnimationLength("gogo") var length = this.don.getAnimationLength("gogo")
this.don.setUpdateSpeed(4 / length) this.don.setUpdateSpeed(4 / length)
this.don.setAnimation("gogo") this.don.setAnimation("gogo")
}else if(Math.round(this.controller.getGlobalScore().gauge / 2) - 1 >= 25){
this.don.setAnimationStart(0)
var length = this.don.getAnimationLength("clear")
this.don.setUpdateSpeed(2 / length)
this.don.setAnimation("clear")
}else{ }else{
this.don.setAnimationStart(0) var score = this.controller.getGlobalScore()
var length = this.don.getAnimationLength("normal") var cleared = this.controller.game.rules.clearReached(score.gauge)
this.don.setUpdateSpeed(4 / length) if(cleared){
this.don.setAnimation("normal") this.don.setAnimationStart(0)
var length = this.don.getAnimationLength("clear")
this.don.setUpdateSpeed(2 / length)
this.don.setAnimation("clear")
}else{
this.don.setAnimationStart(0)
var length = this.don.getAnimationLength("normal")
this.don.setUpdateSpeed(4 / length)
this.don.setAnimation("normal")
}
} }
} }
this.don.addFrames("clear", 30, "don_anim_clear") this.don.addFrames("clear", 30, "don_anim_clear")

4
tools/get_version.bat Normal file
View File

@ -0,0 +1,4 @@
@echo off
(
git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%%y.%%m.%%d"
) > ../version.json

1
tools/get_version.sh Executable file
View File

@ -0,0 +1 @@
git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%y.%m.%d" > ../version.json

196
tools/merge_image.htm Normal file
View File

@ -0,0 +1,196 @@
<html>
<head>
<title>Merge Image</title>
<style>
body{
transition: background-color 0.5s;
background-color: #fff;
font-family: sans-serif;
font-size: 20px;
}
input[type=number]{
font-family: monospace;
font-size: 18px;
padding: 5px;
}
#settings{
display: flex;
margin-bottom: 18px;
height: 40px;
}
label{
display: flex;
align-items: center;
height: 100%;
}
label:not(:first-child){
margin-left: 10px;
border-left: 2px solid #ccc;
padding-left: 8px;
}
input{
margin: 5px;
}
</style>
</head>
<body>
<div id="settings">
<label>
Max height
<input id="max-height" type="number" min="1" max="5000" step="1" value="2000">
px
</label>
<label>
Spacing
<input id="spacing" type="number" min="0" max="100" step="1" value="0">
px
</label>
<label>
<input id="vertical" type="checkbox" checked>
Vertical
</label>
</div>
<div id="hint">Drag and drop your images here...</div>
<canvas id="canvas"></canvas>
<script>
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
var allFiles
var maxHeightElement = document.getElementById("max-height")
var spacingElement = document.getElementById("spacing")
var maxHeight = parseInt(maxHeightElement.value)
var spacing = parseInt(spacingElement.value)
var vectical = true
document.addEventListener("dragover", event => {
event.preventDefault()
event.dataTransfer.dropEffect = "copy"
document.body.style.backgroundColor = "#ccc"
})
document.addEventListener("dragleave", () => {
document.body.style.backgroundColor = "#fff"
})
document.addEventListener("drop", event => {
document.getElementById("hint").style.display = "none"
document.body.style.backgroundColor = "#fff"
event.preventDefault()
allFiles = []
var promises = []
for(let file of event.dataTransfer.files){
promises.push(readFile(file))
}
Promise.all(promises).then(drawCanvas)
})
maxHeightElement.addEventListener("change", event => {
var value = parseInt(event.currentTarget.value)
if(value >= 1 && value <= 5000){
maxHeight = value
if(allFiles && allFiles.length){
drawCanvas()
}
}
})
spacingElement.addEventListener("change", event => {
var value = parseInt(event.currentTarget.value)
if(value >= 0 && value <= 100){
spacing = value
if(allFiles && allFiles.length){
drawCanvas()
}
}
})
document.getElementById("vertical").addEventListener("change", event => {
vertical = event.currentTarget.checked
if(allFiles && allFiles.length){
drawCanvas()
}
})
function readFile(file){
return new Promise((resolve, reject) => {
var reader = new FileReader()
reader.addEventListener("load", () => {
if(reader.result){
var img = document.createElement("img")
img.addEventListener("load", () => {
var noExt = file.name.slice(0, file.name.lastIndexOf("."))
if(parseInt(noExt) == noExt){
var name = parseInt(noExt)
}else{
var name = noExt
}
allFiles.push({
name: name,
img: img
})
resolve()
})
img.addEventListener("error", resolve)
img.addEventListener("abort", resolve)
img.src = reader.result
}else{
resolve()
}
})
reader.addEventListener("error", resolve)
reader.addEventListener("abort", resolve)
reader.readAsDataURL(file)
})
}
function drawCanvas(){
var x = 0
var y = 0
var biggestWidth = 0
var canvasWidth = 0
var canvasHeight = 0
allFiles.sort((a, b) => a.name > b.name ? 1 : -1)
for(var i in allFiles){
var file = allFiles[i]
if(vertical){
if(y + file.img.height > maxHeight + spacing){
y = 0
x += biggestWidth
biggestWidth = 0
}
file.x = x + (x === 0 ? 0 : spacing)
file.y = y + (y === 0 ? 0 : spacing)
y += file.img.height + (y === 0 ? 0 : spacing)
if(file.img.width > biggestWidth){
biggestWidth = file.img.width
}
if(y > canvasHeight){
canvasHeight = y
}
}else{
if(x + file.img.width > maxHeight + spacing){
x = 0
y += biggestWidth
biggestWidth = 0
}
file.x = x + (x === 0 ? 0 : spacing)
file.y = y + (y === 0 ? 0 : spacing)
x += file.img.width + (x === 0 ? 0 : spacing)
if(file.img.height > biggestWidth){
biggestWidth = file.img.height
}
if(x > canvasWidth){
canvasWidth = x
}
}
}
if(vertical){
canvasWidth = x + biggestWidth
}else{
canvasHeight = y + biggestWidth
}
canvas.width = canvasWidth
canvas.height = canvasHeight
for(var i in allFiles){
var file = allFiles[i]
ctx.drawImage(file.img, file.x, file.y, file.img.width, file.img.height)
}
}
</script>
</body>
</html>

86
tools/set_previews.py Normal file
View File

@ -0,0 +1,86 @@
from __future__ import division
import os
import sqlite3
import re
DATABASE = 'taiko.db'
conn = sqlite3.connect(DATABASE)
curs = conn.cursor()
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
if __name__ == '__main__':
songs = curs.execute('select id, type from songs').fetchall()
for song in songs:
preview = get_preview(song[0], song[1]) / 1000
curs.execute('update songs set preview = ? where id = ?', (preview, song[0]))
conn.commit()

49
tools/taikodb_hash.py Normal file
View File

@ -0,0 +1,49 @@
import os
import sys
import hashlib
import base64
import sqlite3
def md5(md5hash, filename):
with open(filename, "rb") as file:
for chunk in iter(lambda: file.read(64 * 1024), b""):
md5hash.update(chunk)
def get_hashes(root):
hashes = {}
diffs = ["easy", "normal", "hard", "oni", "ura"]
dirs = os.listdir(root)
for dir in dirs:
dir_path = os.path.join(root, dir)
if dir.isdigit() and os.path.isdir(dir_path):
files = os.listdir(dir_path)
md5hash = hashlib.md5()
if "main.tja" in files:
md5(md5hash, os.path.join(dir_path, "main.tja"))
else:
for diff in diffs:
if diff + ".osu" in files:
md5(md5hash, os.path.join(dir_path, diff + ".osu"))
hashes[dir] = base64.b64encode(md5hash.digest())[:-2]
return hashes
def write_db(database, songs):
db = sqlite3.connect(database)
hashes = get_hashes(songs)
added = 0
for id in hashes:
added += 1
cur = db.cursor()
cur.execute("update songs set hash = ? where id = ?", (hashes[id].decode(), int(id)))
cur.close()
db.commit()
db.close()
if added:
print("{0} hashes have been added to the database.".format(added))
else:
print("Error: No songs were found in the given directory.")
if len(sys.argv) >= 3:
write_db(sys.argv[1], sys.argv[2])
else:
print("Usage: taikodb_hash.py ../taiko.db ../public/songs")