4 Commits
F-B ... Bang2

8 changed files with 1714 additions and 1082 deletions

155
app.py
View File

@@ -105,6 +105,9 @@ db.users.create_index('username', unique=True)
db.songs.create_index('id', unique=True) db.songs.create_index('id', unique=True)
db.songs.create_index('song_type') db.songs.create_index('song_type')
db.scores.create_index('username') db.scores.create_index('username')
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('month', 1), ('score_value', -1)])
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('username', 1), ('month', 1)], unique=True)
db.leaderboards.create_index('month')
class HashException(Exception): class HashException(Exception):
@@ -746,6 +749,158 @@ def route_api_scores_get():
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don}) return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
@app.route(basedir + 'api/leaderboard/submit', methods=['POST'])
@login_required
def route_api_leaderboard_submit():
data = request.get_json()
if not schema.validate(data, schema.leaderboard_submit):
return abort(400)
username = session.get('username')
user = db.users.find_one({'username': username})
if not user:
return api_error('user_not_found')
song_id = data.get('song_id')
difficulty = data.get('difficulty')
score_data = data.get('score')
# Validate difficulty
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
if difficulty not in valid_difficulties:
return api_error('invalid_difficulty')
# Get current month (YYYY-MM format)
current_month = time.strftime('%Y-%m', time.gmtime())
# Check if user already has a record for this song/difficulty/month
existing = db.leaderboards.find_one({
'song_id': song_id,
'difficulty': difficulty,
'username': username,
'month': current_month
})
# Parse score (assuming it's in the same format as the scores collection)
try:
if isinstance(score_data, str):
import json as json_module
score_obj = json_module.loads(score_data)
else:
score_obj = score_data
score_value = int(score_obj.get('score', 0))
except:
return api_error('invalid_score_format')
if existing:
# Update only if new score is higher
existing_score = existing.get('score_value', 0)
if score_value > existing_score:
db.leaderboards.update_one(
{'_id': existing['_id']},
{'$set': {
'score': score_data,
'score_value': score_value,
'display_name': user['display_name'],
'submitted_at': time.time()
}}
)
return jsonify({'status': 'ok', 'message': 'score_updated'})
else:
return jsonify({'status': 'ok', 'message': 'score_not_higher'})
else:
# Check if this score would be in top 50
count = db.leaderboards.count_documents({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
})
if count >= 50:
# Find the 50th score
leaderboard = list(db.leaderboards.find({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
}).sort('score_value', -1).limit(50))
if len(leaderboard) >= 50:
last_score = leaderboard[49].get('score_value', 0)
if score_value <= last_score:
return jsonify({'status': 'ok', 'message': 'score_too_low'})
# Insert new record
db.leaderboards.insert_one({
'song_id': song_id,
'difficulty': difficulty,
'username': username,
'display_name': user['display_name'],
'score': score_data,
'score_value': score_value,
'submitted_at': time.time(),
'month': current_month
})
# Remove entries beyond 50th place
if count >= 50:
# Get all entries sorted by score
all_entries = list(db.leaderboards.find({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
}).sort('score_value', -1))
# Delete entries beyond 50
if len(all_entries) > 50:
for entry in all_entries[50:]:
db.leaderboards.delete_one({'_id': entry['_id']})
return jsonify({'status': 'ok', 'message': 'score_submitted'})
@app.route(basedir + 'api/leaderboard/get')
def route_api_leaderboard_get():
song_id = request.args.get('song_id', None)
difficulty = request.args.get('difficulty', None)
if not song_id or not difficulty:
return abort(400)
try:
song_id = int(song_id)
except:
return abort(400)
# Validate difficulty
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
if difficulty not in valid_difficulties:
return abort(400)
# Get current month
current_month = time.strftime('%Y-%m', time.gmtime())
# Get top 50 scores
leaderboard = list(db.leaderboards.find({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
}, {
'_id': False,
'username': True,
'display_name': True,
'score': True,
'score_value': True,
'submitted_at': True
}).sort('score_value', -1).limit(50))
# Add rank to each entry
for i, entry in enumerate(leaderboard):
entry['rank'] = i + 1
return jsonify({'status': 'ok', 'leaderboard': leaderboard, 'month': current_month})
@app.route(basedir + 'privacy') @app.route(basedir + 'privacy')
def route_api_privacy(): def route_api_privacy():
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt'))) last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))

View File

@@ -0,0 +1,13 @@
#leaderboard {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1000;
}
#leaderboard-canvas {
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,390 @@
class Leaderboard {
constructor() {
this.init()
}
init() {
this.canvas = document.getElementById("leaderboard-canvas")
if (!this.canvas) {
return
}
this.ctx = this.canvas.getContext("2d")
var resolution = settings.getItem("resolution")
var noSmoothing = resolution === "low" || resolution === "lowest"
if (noSmoothing) {
this.ctx.imageSmoothingEnabled = false
}
this.songId = null
this.difficulty = null
this.leaderboardData = []
this.currentMonth = ""
this.visible = false
this.draw = new CanvasDraw(noSmoothing)
// Keyboard controls
this.keyboard = new Keyboard({
confirm: ["enter", "escape", "don_l", "don_r"],
back: ["escape"],
left: ["left", "ka_l"],
right: ["right", "ka_r"]
}, this.keyPress.bind(this))
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.onClose.bind(this))
}
keyPress(pressed, name) {
if (!pressed || !this.visible) {
return
}
if (name === "confirm" || name === "back") {
this.close()
} else if (name === "left") {
this.changeDifficulty(-1)
} else if (name === "right") {
this.changeDifficulty(1)
}
}
async display(songId, difficulty) {
this.songId = songId
this.difficulty = difficulty
this.visible = true
loader.changePage("leaderboard", false)
// Fetch leaderboard data
await this.fetchLeaderboard()
// Start rendering
this.redrawRunning = true
this.redrawBind = this.redraw.bind(this)
this.redraw()
assets.sounds["se_don"].play()
}
async fetchLeaderboard() {
try {
var response = await loader.ajax(
`${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${this.songId}&difficulty=${this.difficulty}`
)
var data = JSON.parse(response)
if (data.status === "ok") {
this.leaderboardData = data.leaderboard || []
this.currentMonth = data.month || ""
} else {
this.leaderboardData = []
}
} catch (e) {
console.error("Failed to fetch leaderboard:", e)
this.leaderboardData = []
}
}
changeDifficulty(direction) {
var difficulties = ["easy", "normal", "hard", "oni", "ura"]
var currentIndex = difficulties.indexOf(this.difficulty)
if (currentIndex === -1) {
return
}
var newIndex = (currentIndex + direction + difficulties.length) % difficulties.length
this.difficulty = difficulties[newIndex]
this.fetchLeaderboard().then(() => {
assets.sounds["se_ka"].play()
})
}
onClose(event) {
if (!this.visible) {
return
}
var rect = this.canvas.getBoundingClientRect()
var x = (event.offsetX || event.touches[0].pageX - rect.left)
var y = (event.offsetY || event.touches[0].pageY - rect.top)
// Check if clicked outside modal - updated dimensions
var centerX = this.canvas.width / 2
var centerY = this.canvas.height / 2
var modalWidth = 880 // Updated from 800
var modalHeight = 640 // Updated from 600
if (x < centerX - modalWidth / 2 || x > centerX + modalWidth / 2 ||
y < centerY - modalHeight / 2 || y > centerY + modalHeight / 2) {
this.close()
}
}
close() {
this.visible = false
this.redrawRunning = false
if (this.keyboard) {
this.keyboard.clean()
}
assets.sounds["se_cancel"].play()
// Return to song select - get touchEnabled from global or default to false
setTimeout(() => {
var touchEnabled = typeof window.touchEnabled !== 'undefined' ? window.touchEnabled : false
new SongSelect(false, false, touchEnabled)
}, 100)
}
redraw() {
if (!this.redrawRunning) {
return
}
requestAnimationFrame(this.redrawBind)
var winW = innerWidth
var winH = innerHeight
var ratio = winH / 720
this.canvas.width = winW
this.canvas.height = winH
this.ctx.save()
// Draw semi-transparent background
this.ctx.fillStyle = "rgba(0, 0, 0, 0.8)"
this.ctx.fillRect(0, 0, winW, winH)
this.ctx.scale(ratio, ratio)
// Draw modal background with gradient
var modalX = 200
var modalY = 40
var modalW = 880
var modalH = 640
// Gradient background
var bgGradient = this.ctx.createLinearGradient(modalX, modalY, modalX, modalY + modalH)
bgGradient.addColorStop(0, "#ffffff")
bgGradient.addColorStop(1, "#f0f4f8")
this.ctx.fillStyle = bgGradient
this.ctx.shadowColor = "rgba(0, 0, 0, 0.3)"
this.ctx.shadowBlur = 30
this.ctx.shadowOffsetX = 0
this.ctx.shadowOffsetY = 10
this.ctx.fillRect(modalX, modalY, modalW, modalH)
this.ctx.shadowBlur = 0
// Modal border with gradient
var borderGradient = this.ctx.createLinearGradient(modalX, modalY, modalX + modalW, modalY + modalH)
borderGradient.addColorStop(0, "#4a90e2")
borderGradient.addColorStop(0.5, "#7b68ee")
borderGradient.addColorStop(1, "#ff6b9d")
this.ctx.strokeStyle = borderGradient
this.ctx.lineWidth = 6
this.ctx.strokeRect(modalX, modalY, modalW, modalH)
// Draw title with gradient
var titleGradient = this.ctx.createLinearGradient(0, 90, 0, 130)
titleGradient.addColorStop(0, "#4a90e2")
titleGradient.addColorStop(1, "#7b68ee")
this.ctx.fillStyle = titleGradient
this.ctx.font = "bold 48px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.shadowColor = "rgba(0, 0, 0, 0.2)"
this.ctx.shadowBlur = 5
this.ctx.shadowOffsetX = 2
this.ctx.shadowOffsetY = 2
this.ctx.fillText("🏆 排行榜 Leaderboard", 640, 100)
this.ctx.shadowBlur = 0
// Draw difficulty selector with improved styling
var diffX = 640
var diffY = 155
var difficulties = [
{ id: "easy", name: "簡単", color: "#00a0e9", glow: "#00d4ff" },
{ id: "normal", name: "普通", color: "#00a040", glow: "#00ff66" },
{ id: "hard", name: "難しい", color: "#ff8c00", glow: "#ffb347" },
{ id: "oni", name: "鬼", color: "#dc143c", glow: "#ff6b9d" },
{ id: "ura", name: "裏", color: "#9400d3", glow: "#da70d6" }
]
this.ctx.font = "bold 22px " + (strings.font || "sans-serif")
for (var i = 0; i < difficulties.length; i++) {
var diff = difficulties[i]
var x = diffX - 220 + i * 110
if (diff.id === this.difficulty) {
// Selected difficulty with glow effect
this.ctx.shadowColor = diff.glow
this.ctx.shadowBlur = 15
var gradient = this.ctx.createLinearGradient(x - 50, diffY - 30, x + 50, diffY + 20)
gradient.addColorStop(0, diff.color)
gradient.addColorStop(1, diff.glow)
this.ctx.fillStyle = gradient
this.ctx.fillRect(x - 50, diffY - 30, 100, 50)
this.ctx.shadowBlur = 0
this.ctx.fillStyle = "#ffffff"
this.ctx.shadowColor = "rgba(0, 0, 0, 0.5)"
this.ctx.shadowBlur = 3
} else {
// Unselected difficulty
this.ctx.strokeStyle = diff.color
this.ctx.lineWidth = 3
this.ctx.strokeRect(x - 50, diffY - 30, 100, 50)
this.ctx.fillStyle = diff.color
}
this.ctx.textAlign = "center"
this.ctx.fillText(diff.name, x, diffY)
this.ctx.shadowBlur = 0
}
// Draw month info with style
this.ctx.fillStyle = "#666666"
this.ctx.font = "20px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("📅 当月排行 " + this.currentMonth, 640, 215)
// Header bar
var headerY = 250
var headerGradient = this.ctx.createLinearGradient(modalX, headerY, modalX, headerY + 35)
headerGradient.addColorStop(0, "#e8eef5")
headerGradient.addColorStop(1, "#d0dae8")
this.ctx.fillStyle = headerGradient
this.ctx.fillRect(modalX + 20, headerY, modalW - 40, 35)
this.ctx.fillStyle = "#333333"
this.ctx.font = "bold 18px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("排名", modalX + 80, headerY + 23)
this.ctx.textAlign = "left"
this.ctx.fillText("玩家", modalX + 140, headerY + 23)
this.ctx.textAlign = "right"
this.ctx.fillText("分数", modalX + modalW - 60, headerY + 23)
// Draw leaderboard entries
var startY = 295
var rowHeight = 38
this.ctx.font = "22px " + (strings.font || "sans-serif")
this.ctx.textAlign = "left"
if (this.leaderboardData.length === 0) {
this.ctx.fillStyle = "#999999"
this.ctx.font = "24px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("暂无排行数据", 640, startY + 120)
} else {
for (var i = 0; i < Math.min(this.leaderboardData.length, 15); i++) {
var entry = this.leaderboardData[i]
var y = startY + i * rowHeight
var rank = entry.rank
// Rank background color with gradient
var gradient
if (rank === 1) {
// Gold gradient for 1st place
gradient = this.ctx.createLinearGradient(modalX + 20, y - 25, modalX + 20, y + 10)
gradient.addColorStop(0, "#ffd700")
gradient.addColorStop(1, "#ffed4e")
this.ctx.fillStyle = gradient
this.ctx.shadowColor = "#ffd700"
this.ctx.shadowBlur = 20
} else if (rank === 2) {
// Silver gradient for 2nd place
gradient = this.ctx.createLinearGradient(modalX + 20, y - 25, modalX + 20, y + 10)
gradient.addColorStop(0, "#c0c0c0")
gradient.addColorStop(1, "#e8e8e8")
this.ctx.fillStyle = gradient
this.ctx.shadowColor = "#c0c0c0"
this.ctx.shadowBlur = 15
} else if (rank === 3) {
// Bronze gradient for 3rd place
gradient = this.ctx.createLinearGradient(modalX + 20, y - 25, modalX + 20, y + 10)
gradient.addColorStop(0, "#cd7f32")
gradient.addColorStop(1, "#e9a86a")
this.ctx.fillStyle = gradient
this.ctx.shadowColor = "#cd7f32"
this.ctx.shadowBlur = 12
} else {
// Regular entries with subtle gradient
gradient = this.ctx.createLinearGradient(modalX + 20, y - 25, modalX + 20, y + 10)
gradient.addColorStop(0, "#ffffff")
gradient.addColorStop(1, "#f8f9fa")
this.ctx.fillStyle = gradient
}
// Rounded rectangle effect
this.ctx.fillRect(modalX + 20, y - 25, modalW - 40, 35)
this.ctx.shadowBlur = 0
// Border for entries
if (rank <= 3) {
this.ctx.strokeStyle = rank === 1 ? "#ffd700" : rank === 2 ? "#c0c0c0" : "#cd7f32"
this.ctx.lineWidth = 2
this.ctx.strokeRect(modalX + 20, y - 25, modalW - 40, 35)
}
// Rank with medal emoji for top 3
if (rank === 1) {
this.ctx.fillStyle = "#8b6914"
this.ctx.font = "bold 24px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("🥇", modalX + 80, y + 2)
} else if (rank === 2) {
this.ctx.fillStyle = "#5c5c5c"
this.ctx.font = "bold 24px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("🥈", modalX + 80, y + 2)
} else if (rank === 3) {
this.ctx.fillStyle = "#6d4423"
this.ctx.font = "bold 24px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("🥉", modalX + 80, y + 2)
} else {
this.ctx.fillStyle = rank <= 3 ? "#ffffff" : "#555555"
this.ctx.font = "bold 22px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("#" + rank, modalX + 80, y + 2)
}
// Display name
this.ctx.fillStyle = rank <= 3 ? "#1a1a1a" : "#333333"
this.ctx.font = (rank <= 3 ? "bold " : "") + "20px " + (strings.font || "sans-serif")
this.ctx.textAlign = "left"
var displayName = entry.display_name || entry.username
if (displayName.length > 18) {
displayName = displayName.substring(0, 18) + "..."
}
this.ctx.fillText(displayName, modalX + 140, y + 2)
// Score with formatting
var score = entry.score && entry.score.score ? entry.score.score : 0
this.ctx.fillStyle = rank <= 3 ? "#1a1a1a" : "#555555"
this.ctx.font = (rank <= 3 ? "bold " : "") + "20px " + (strings.font || "sans-serif")
this.ctx.textAlign = "right"
this.ctx.fillText(score.toLocaleString(), modalX + modalW - 60, y + 2)
}
}
// Draw close hint with icon
this.ctx.fillStyle = "#888888"
this.ctx.font = "18px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center"
this.ctx.fillText("⌨️ 按ESC或点击外部关闭 Press ESC or click outside to close", 640, 665)
this.ctx.restore()
}
clean() {
if (this.keyboard) {
this.keyboard.clean()
}
if (this.redrawRunning) {
this.redrawRunning = false
}
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
}
}

View File

@@ -1,8 +1,8 @@
class Scoresheet{ class Scoresheet {
constructor(...args){ constructor(...args) {
this.init(...args) this.init(...args)
} }
init(controller, results, multiplayer, touchEnabled){ init(controller, results, multiplayer, touchEnabled) {
this.controller = controller this.controller = controller
this.resultsObj = results this.resultsObj = results
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0] this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
@@ -11,12 +11,12 @@ class Scoresheet{
this.results[player0] = {} this.results[player0] = {}
this.rules = [] this.rules = []
this.rules[player0] = this.controller.game.rules this.rules[player0] = this.controller.game.rules
if(multiplayer){ if (multiplayer) {
this.player.push(p2.player === 2 ? 0 : 1) this.player.push(p2.player === 2 ? 0 : 1)
this.results[this.player[1]] = p2.results this.results[this.player[1]] = p2.results
this.rules[this.player[1]] = this.controller.syncWith.game.rules this.rules[this.player[1]] = this.controller.syncWith.game.rules
} }
for(var i in results){ for (var i in results) {
this.results[player0][i] = results[i] === null ? null : results[i].toString() this.results[player0][i] = results[i] === null ? null : results[i].toString()
} }
this.multiplayer = multiplayer this.multiplayer = multiplayer
@@ -26,10 +26,10 @@ class Scoresheet{
this.ctx = this.canvas.getContext("2d") this.ctx = this.canvas.getContext("2d")
var resolution = settings.getItem("resolution") var resolution = settings.getItem("resolution")
var noSmoothing = resolution === "low" || resolution === "lowest" var noSmoothing = resolution === "low" || resolution === "lowest"
if(noSmoothing){ if (noSmoothing) {
this.ctx.imageSmoothingEnabled = false this.ctx.imageSmoothingEnabled = false
} }
if(resolution === "lowest"){ if (resolution === "lowest") {
this.canvas.style.imageRendering = "pixelated" this.canvas.style.imageRendering = "pixelated"
} }
this.game = document.getElementById("game") this.game = document.getElementById("game")
@@ -78,12 +78,12 @@ class Scoresheet{
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689) assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
this.session = p2.session this.session = p2.session
if(this.session){ if (this.session) {
if(p2.getMessage("songsel")){ if (p2.getMessage("songsel")) {
this.toSongsel(true) this.toSongsel(true)
} }
pageEvents.add(p2, "message", response => { pageEvents.add(p2, "message", response => {
if(response.type === "songsel"){ if (response.type === "songsel") {
this.toSongsel(true) this.toSongsel(true)
} }
}) })
@@ -100,51 +100,51 @@ class Scoresheet{
touchEvents: controller.view.touchEvents touchEvents: controller.view.touchEvents
}) })
} }
keyDown(pressed){ keyDown(pressed) {
if(pressed && this.redrawing){ if (pressed && this.redrawing) {
this.toNext() this.toNext()
} }
} }
mouseDown(event){ mouseDown(event) {
if(event.type === "touchstart"){ if (event.type === "touchstart") {
event.preventDefault() event.preventDefault()
this.canvas.style.cursor = "" this.canvas.style.cursor = ""
this.state.pointerLocked = true this.state.pointerLocked = true
}else{ } else {
this.state.pointerLocked = false this.state.pointerLocked = false
if(event.which !== 1){ if (event.which !== 1) {
return return
} }
} }
this.toNext() this.toNext()
} }
toNext(){ toNext() {
var elapsed = this.getMS() - this.state.screenMS var elapsed = this.getMS() - this.state.screenMS
if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){ if (this.state.screen === "fadeIn" && elapsed >= this.state.startDelay) {
this.toScoresShown() this.toScoresShown()
}else if(this.state.screen === "scoresShown" && elapsed >= 1000){ } else if (this.state.screen === "scoresShown" && elapsed >= 1000) {
this.toSongsel() this.toSongsel()
} }
} }
toScoresShown(){ toScoresShown() {
if(!p2.session){ if (!p2.session) {
this.state.screen = "scoresShown" this.state.screen = "scoresShown"
this.state.screenMS = this.getMS() this.state.screenMS = this.getMS()
this.controller.playSound("neiro_1_don", 0, true) this.controller.playSound("neiro_1_don", 0, true)
} }
} }
toSongsel(fromP2){ toSongsel(fromP2) {
if(!p2.session || fromP2){ if (!p2.session || fromP2) {
snd.musicGain.fadeOut(0.5) snd.musicGain.fadeOut(0.5)
this.state.screen = "fadeOut" this.state.screen = "fadeOut"
this.state.screenMS = this.getMS() this.state.screenMS = this.getMS()
if(!fromP2){ if (!fromP2) {
this.controller.playSound("neiro_1_don", 0, true) this.controller.playSound("neiro_1_don", 0, true)
} }
} }
} }
startRedraw(){ startRedraw() {
this.redrawing = true this.redrawing = true
requestAnimationFrame(this.redrawBind) requestAnimationFrame(this.redrawBind)
this.winW = null this.winW = null
@@ -152,7 +152,7 @@ class Scoresheet{
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this)) pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this))
if(!this.multiplayer){ if (!this.multiplayer) {
this.tetsuoHana = document.createElement("div") this.tetsuoHana = document.createElement("div")
this.tetsuoHana.id = "tetsuohana" this.tetsuoHana.id = "tetsuohana"
var flowersBg = "url('" + assets.image["results_flowers"].src + "')" var flowersBg = "url('" + assets.image["results_flowers"].src + "')"
@@ -160,12 +160,12 @@ class Scoresheet{
var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')" var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')"
var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"] var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"]
var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg] var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg]
for(var i = 0; i < id.length; i++){ for (var i = 0; i < id.length; i++) {
if(id[i] === "mikoshi"){ if (id[i] === "mikoshi") {
var divOut = document.createElement("div") var divOut = document.createElement("div")
divOut.id = id[i] + "-out" divOut.id = id[i] + "-out"
this.tetsuoHana.appendChild(divOut) this.tetsuoHana.appendChild(divOut)
}else{ } else {
var divOut = this.tetsuoHana var divOut = this.tetsuoHana
} }
var div = document.createElement("div") var div = document.createElement("div")
@@ -180,16 +180,16 @@ class Scoresheet{
} }
} }
redraw(){ redraw() {
if(!this.redrawRunning){ if (!this.redrawRunning) {
return return
} }
if(this.redrawing){ if (this.redrawing) {
requestAnimationFrame(this.redrawBind) requestAnimationFrame(this.redrawBind)
} }
var ms = this.getMS() var ms = this.getMS()
if(!this.redrawRunning){ if (!this.redrawRunning) {
return return
} }
@@ -200,11 +200,11 @@ class Scoresheet{
var winH = lastHeight var winH = lastHeight
this.pixelRatio = window.devicePixelRatio || 1 this.pixelRatio = window.devicePixelRatio || 1
var resolution = settings.getItem("resolution") var resolution = settings.getItem("resolution")
if(resolution === "medium"){ if (resolution === "medium") {
this.pixelRatio *= 0.75 this.pixelRatio *= 0.75
}else if(resolution === "low"){ } else if (resolution === "low") {
this.pixelRatio *= 0.5 this.pixelRatio *= 0.5
}else if(resolution === "lowest"){ } else if (resolution === "lowest") {
this.pixelRatio *= 0.25 this.pixelRatio *= 0.25
} }
winW *= this.pixelRatio winW *= this.pixelRatio
@@ -213,8 +213,8 @@ class Scoresheet{
var ratioY = winH / 720 var ratioY = winH / 720
var ratio = (ratioX < ratioY ? ratioX : ratioY) var ratio = (ratioX < ratioY ? ratioX : ratioY)
if(this.redrawing){ if (this.redrawing) {
if(this.winW !== winW || this.winH !== winH){ if (this.winW !== winW || this.winH !== winH) {
this.canvas.width = Math.max(1, winW) this.canvas.width = Math.max(1, winW)
this.canvas.height = Math.max(1, winH) this.canvas.height = Math.max(1, winH)
ctx.scale(ratio, ratio) ctx.scale(ratio, ratio)
@@ -224,37 +224,37 @@ class Scoresheet{
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
this.nameplateCache.resize(274, 134, ratio + 0.2) this.nameplateCache.resize(274, 134, ratio + 0.2)
if(!this.multiplayer){ if (!this.multiplayer) {
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio) this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
if(this.tetsuoHanaClass === "dance"){ if (this.tetsuoHanaClass === "dance") {
this.tetsuoHana.classList.remove("dance", "dance2") this.tetsuoHana.classList.remove("dance", "dance2")
setTimeout(()=>{ setTimeout(() => {
this.tetsuoHana.classList.add("dance2") this.tetsuoHana.classList.add("dance2")
},50) }, 50)
}else if(this.tetsuoHanaClass === "failed"){ } else if (this.tetsuoHanaClass === "failed") {
this.tetsuoHana.classList.remove("failed") this.tetsuoHana.classList.remove("failed")
setTimeout(()=>{ setTimeout(() => {
this.tetsuoHana.classList.add("failed") this.tetsuoHana.classList.add("failed")
},50) }, 50)
} }
} }
}else if(!document.hasFocus() && this.state.screen === "scoresShown"){ } else if (!document.hasFocus() && this.state.screen === "scoresShown") {
if(this.state["countup0"]){ if (this.state["countup0"]) {
this.stopSound("se_results_countup", 0) this.stopSound("se_results_countup", 0)
} }
if(this.state["countup1"]){ if (this.state["countup1"]) {
this.stopSound("se_results_countup", 1) this.stopSound("se_results_countup", 1)
} }
return return
}else{ } else {
ctx.clearRect(0, 0, winW / ratio, winH / ratio) ctx.clearRect(0, 0, winW / ratio, winH / ratio)
} }
}else{ } else {
ctx.scale(ratio, ratio) ctx.scale(ratio, ratio)
if(!this.canvasCache.canvas){ if (!this.canvasCache.canvas) {
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
} }
if(!this.nameplateCache.canvas){ if (!this.nameplateCache.canvas) {
this.nameplateCache.resize(274, 67, ratio + 0.2) this.nameplateCache.resize(274, 67, ratio + 0.2)
} }
} }
@@ -272,14 +272,14 @@ class Scoresheet{
var bgOffset = 0 var bgOffset = 0
var elapsed = ms - this.state.screenMS var elapsed = ms - this.state.screenMS
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){ if ((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved) {
this.saveScore() this.saveScore()
} }
if(bgOffset){ if (bgOffset) {
ctx.save() ctx.save()
ctx.translate(0, -bgOffset) ctx.translate(0, -bgOffset)
} }
@@ -297,7 +297,7 @@ class Scoresheet{
ctx.fillRect(0, winH / 2 - 12, winW, 12) ctx.fillRect(0, winH / 2 - 12, winW, 12)
ctx.fillStyle = "rgba(0, 0, 0, 0.25)" ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
ctx.fillRect(0, winH / 2, winW, 20) ctx.fillRect(0, winH / 2, winW, 20)
if(bgOffset !== 0){ if (bgOffset !== 0) {
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.fillRect(0, winH / 2 - 2, winW, 2) ctx.fillRect(0, winH / 2 - 2, winW, 2)
} }
@@ -306,7 +306,7 @@ class Scoresheet{
ctx.fillStyle = "#bf2900" ctx.fillStyle = "#bf2900"
ctx.fillRect(0, frameTop + 64, winW, 8) ctx.fillRect(0, frameTop + 64, winW, 8)
if(bgOffset){ if (bgOffset) {
ctx.restore() ctx.restore()
ctx.save() ctx.save()
ctx.translate(0, bgOffset) ctx.translate(0, bgOffset)
@@ -325,9 +325,9 @@ class Scoresheet{
ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)" ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)"
ctx.fillRect(0, winH / 2, winW, 12) ctx.fillRect(0, winH / 2, winW, 12)
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
if(bgOffset === 0){ if (bgOffset === 0) {
ctx.fillRect(0, winH / 2 - 2, winW, 4) ctx.fillRect(0, winH / 2 - 2, winW, 4)
}else{ } else {
ctx.fillRect(0, winH / 2, winW, 2) ctx.fillRect(0, winH / 2, winW, 2)
} }
ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529" ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529"
@@ -337,46 +337,46 @@ class Scoresheet{
ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a" ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a"
ctx.fillRect(0, winH - frameTop - 66, winW, 2) ctx.fillRect(0, winH - frameTop - 66, winW, 2)
if(bgOffset){ if (bgOffset) {
ctx.restore() ctx.restore()
} }
if(this.state.screen === "scoresShown" || this.state.screen === "fadeOut"){ if (this.state.screen === "scoresShown" || this.state.screen === "fadeOut") {
var elapsed = Infinity var elapsed = Infinity
}else if(this.redrawing){ } else if (this.redrawing) {
var elapsed = ms - this.state.screenMS - this.state.startDelay var elapsed = ms - this.state.screenMS - this.state.startDelay
}else{ } else {
var elapsed = 0 var elapsed = 0
} }
var rules = this.controller.game.rules var rules = this.controller.game.rules
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000 var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
if(players === 2 && failedOffset !== 0){ if (players === 2 && failedOffset !== 0) {
var p2results = this.results[this.player[1]] var p2results = this.results[this.player[1]]
if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){ if (p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)) {
failedOffset = 0 failedOffset = 0
} }
} }
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()
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
var clear = this.rules[p].clearReached(results.gauge) var clear = this.rules[p].clearReached(results.gauge)
if(p === 1 || !this.multiplayer && clear){ if (p === 1 || !this.multiplayer && clear) {
ctx.translate(0, 290) ctx.translate(0, 290)
} }
if(clear){ if (clear) {
ctx.globalCompositeOperation = "lighter" ctx.globalCompositeOperation = "lighter"
} }
ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5 ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5
var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368) var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368)
grd.addColorStop(0, "#000") grd.addColorStop(0, "#000")
if(clear){ if (clear) {
grd.addColorStop(1, "#ffffba") grd.addColorStop(1, "#ffffba")
}else{ } else {
grd.addColorStop(1, "transparent") grd.addColorStop(1, "transparent")
} }
ctx.fillStyle = grd ctx.fillStyle = grd
@@ -385,10 +385,10 @@ class Scoresheet{
} }
} }
if(elapsed >= 0){ if (elapsed >= 0) {
if(this.state.hasPointer === 0){ if (this.state.hasPointer === 0) {
this.state.hasPointer = 1 this.state.hasPointer = 1
if(!this.state.pointerLocked){ if (!this.state.pointerLocked) {
this.canvas.style.cursor = this.session ? "" : "pointer" this.canvas.style.cursor = this.session ? "" : "pointer"
} }
} }
@@ -416,13 +416,13 @@ class Scoresheet{
letterSpacing: strings.id === "en" ? 0 : 3, letterSpacing: strings.id === "en" ? 0 : 3,
forceShadow: true forceShadow: true
}, [ }, [
{x: -2, y: -2, outline: "#000", letterBorder: 22}, { x: -2, y: -2, outline: "#000", letterBorder: 22 },
{}, {},
{x: 2, y: 2, shadow: [2, 2, 7]}, { x: 2, y: 2, shadow: [2, 2, 7] },
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10}, { x: 2, y: 2, outline: "#ad1516", letterBorder: 10 },
{x: -2, y: -2, outline: "#ff797b"}, { x: -2, y: -2, outline: "#ff797b" },
{outline: "#f70808"}, { outline: "#f70808" },
{fill: "#fff", shadow: [-1, 1, 3, 1.5]} { fill: "#fff", shadow: [-1, 1, 3, 1.5] }
]) ])
this.draw.layeredText({ this.draw.layeredText({
@@ -436,18 +436,18 @@ class Scoresheet{
align: "right", align: "right",
forceShadow: true forceShadow: true
}, [ }, [
{outline: "#000", letterBorder: 10, shadow: [1, 1, 3]}, { outline: "#000", letterBorder: 10, shadow: [1, 1, 3] },
{fill: "#fff"} { fill: "#fff" }
]) ])
}) })
ctx.save() ctx.save()
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
if(p === 1){ if (p === 1) {
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
@@ -470,9 +470,9 @@ class Scoresheet{
ctx.miterLimit = 10 ctx.miterLimit = 10
var defaultName = p === 0 ? strings.defaultName : strings.default2PName var defaultName = p === 0 ? strings.defaultName : strings.default2PName
if(p === this.player[0]){ if (p === this.player[0]) {
var name = account.loggedIn ? account.displayName : defaultName var name = account.loggedIn ? account.displayName : defaultName
}else{ } else {
var name = results.name || defaultName var name = results.name || defaultName
} }
this.nameplateCache.get({ this.nameplateCache.get({
@@ -493,7 +493,7 @@ class Scoresheet{
}) })
}) })
if(this.controller.autoPlayEnabled){ if (this.controller.autoPlayEnabled) {
ctx.drawImage(assets.image["badge_auto"], ctx.drawImage(assets.image["badge_auto"],
431, 311, 34, 34 431, 311, 34, 34
) )
@@ -549,8 +549,8 @@ class Scoresheet{
align: "right", align: "right",
width: 36 width: 36
}, [ }, [
{fill: "#fff"}, { fill: "#fff" },
{outline: "#000", letterBorder: 0.5} { outline: "#000", letterBorder: 0.5 }
]) ])
this.draw.score({ this.draw.score({
@@ -590,8 +590,8 @@ class Scoresheet{
width: 154, width: 154,
letterSpacing: strings.id === "ja" ? 1 : 0 letterSpacing: strings.id === "ja" ? 1 : 0
}, [ }, [
{outline: "#000", letterBorder: 8}, { outline: "#000", letterBorder: 8 },
{fill: grd} { fill: grd }
]) ])
this.draw.layeredText({ this.draw.layeredText({
ctx: ctx, ctx: ctx,
@@ -604,8 +604,8 @@ class Scoresheet{
width: 154, width: 154,
letterSpacing: strings.id === "ja" ? 4 : 0 letterSpacing: strings.id === "ja" ? 4 : 0
}, [ }, [
{outline: "#000", letterBorder: 8}, { outline: "#000", letterBorder: 8 },
{fill: "#ffc700"} { fill: "#ffc700" }
]) ])
} }
ctx.restore() ctx.restore()
@@ -613,15 +613,15 @@ class Scoresheet{
ctx.restore() ctx.restore()
} }
if(!this.multiplayer){ if (!this.multiplayer) {
if(elapsed >= 400 && elapsed < 3100 + failedOffset){ if (elapsed >= 400 && elapsed < 3100 + failedOffset) {
if(this.tetsuoHanaClass !== "fadein"){ if (this.tetsuoHanaClass !== "fadein") {
this.tetsuoHana.classList.add("fadein") this.tetsuoHana.classList.add("fadein")
this.tetsuoHanaClass = "fadein" this.tetsuoHanaClass = "fadein"
} }
}else if(elapsed >= 3100 + failedOffset){ } else if (elapsed >= 3100 + failedOffset) {
if(this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed"){ if (this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed") {
if(this.tetsuoHanaClass){ if (this.tetsuoHanaClass) {
this.tetsuoHana.classList.remove(this.tetsuoHanaClass) this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
} }
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed" this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
@@ -630,19 +630,19 @@ class Scoresheet{
} }
} }
if(elapsed >= 800){ if (elapsed >= 800) {
ctx.save() ctx.save()
ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.setTransform(1, 0, 0, 1, 0, 0)
this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => { this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => {
ctx.scale(ratio, ratio) ctx.scale(ratio, ratio)
ctx.translate(frameLeft, frameTop) ctx.translate(frameLeft, frameTop)
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
if(p === 1){ if (p === 1) {
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
var w = 712 var w = 712
@@ -670,50 +670,50 @@ class Scoresheet{
ctx.restore() ctx.restore()
} }
if(elapsed >= 1200){ if (elapsed >= 1200) {
ctx.save() ctx.save()
ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.setTransform(1, 0, 0, 1, 0, 0)
var noCrownResultWait = -2000; var noCrownResultWait = -2000;
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
var crownType = null var crownType = null
if(this.rules[p].clearReached(results.gauge)){ if (this.rules[p].clearReached(results.gauge)) {
crownType = results.bad === "0" ? "gold" : "silver" crownType = results.bad === "0" ? "gold" : "silver"
} }
if(crownType !== null){ if (crownType !== null) {
noCrownResultWait = 0; noCrownResultWait = 0;
var amount = Math.min(1, (elapsed - 1200) / 450) var amount = Math.min(1, (elapsed - 1200) / 450)
this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => { this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => {
ctx.save() ctx.save()
ctx.scale(ratio, ratio) ctx.scale(ratio, ratio)
ctx.translate(frameLeft, frameTop) ctx.translate(frameLeft, frameTop)
if(p === 1){ if (p === 1) {
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
var crownScale = 1 var crownScale = 1
var shine = 0 var shine = 0
if(amount < 1){ if (amount < 1) {
crownScale = 2.8 * (1 - amount) + 0.9 crownScale = 2.8 * (1 - amount) + 0.9
}else if(elapsed < 1850){ } else if (elapsed < 1850) {
crownScale = 0.9 + (elapsed - 1650) / 2000 crownScale = 0.9 + (elapsed - 1650) / 2000
}else if(elapsed < 2200){ } else if (elapsed < 2200) {
shine = (elapsed - 1850) / 175 shine = (elapsed - 1850) / 175
if(shine > 1){ if (shine > 1) {
shine = 2 - shine shine = 2 - shine
} }
} }
if(this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]){ if (this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]) {
this.state["fullcomboPlayed" + p] = true this.state["fullcomboPlayed" + p] = true
if(crownType === "gold"){ if (crownType === "gold") {
this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p) this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p)
} }
} }
if(this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]){ if (this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]) {
this.state["crownPlayed" + p] = true this.state["crownPlayed" + p] = true
this.playSound("se_results_crown", p) this.playSound("se_results_crown", p)
} }
@@ -735,51 +735,51 @@ class Scoresheet{
ctx.restore() ctx.restore()
} }
if(elapsed >= 2400 + noCrownResultWait){ if (elapsed >= 2400 + noCrownResultWait) {
ctx.save() ctx.save()
ctx.translate(frameLeft, frameTop) ctx.translate(frameLeft, frameTop)
var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"] var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"]
if(!this.state["countupTime0"]){ if (!this.state["countupTime0"]) {
var times = {} var times = {}
var lastTime = 0 var lastTime = 0
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
if(currentTime > lastTime){ if (currentTime > lastTime) {
lastTime = currentTime lastTime = currentTime
} }
} }
for(var i in printNumbers){ for (var i in printNumbers) {
var largestTime = 0 var largestTime = 0
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
times[printNumbers[i]] = lastTime + 500 times[printNumbers[i]] = lastTime + 500
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
if(currentTime > largestTime){ if (currentTime > largestTime) {
largestTime = currentTime largestTime = currentTime
} }
} }
lastTime = largestTime lastTime = largestTime
} }
this.state.fadeInEnd = lastTime this.state.fadeInEnd = lastTime
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
this.state["countupTime" + p] = times this.state["countupTime" + p] = times
} }
} }
for(var p = 0; p < players; p++){ for (var p = 0; p < players; p++) {
var results = this.results[p] var results = this.results[p]
if(!results){ if (!results) {
continue continue
} }
if(p === 1){ if (p === 1) {
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
ctx.save() ctx.save()
@@ -795,24 +795,24 @@ class Scoresheet{
ctx.fillStyle = "#fff" ctx.fillStyle = "#fff"
ctx.strokeStyle = "#fff" ctx.strokeStyle = "#fff"
ctx.lineWidth = 0.5 ctx.lineWidth = 0.5
for(var i = 0; i < points.length; i++){ for (var i = 0; i < points.length; i++) {
ctx.translate(-23.3 * scale, 0) ctx.translate(-23.3 * scale, 0)
ctx.fillText(points[points.length - i - 1], 0, 0) ctx.fillText(points[points.length - i - 1], 0, 0)
ctx.strokeText(points[points.length - i - 1], 0, 0) ctx.strokeText(points[points.length - i - 1], 0, 0)
} }
ctx.restore() ctx.restore()
if(!this.state["countupTime" + p]){ if (!this.state["countupTime" + p]) {
var times = {} var times = {}
var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000 var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000
for(var i in printNumbers){ for (var i in printNumbers) {
times[printNumbers[i]] = lastTime + 500 times[printNumbers[i]] = lastTime + 500
lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
} }
this.state["countupTime" + p] = times this.state["countupTime" + p] = times
} }
for(var i in printNumbers){ for (var i in printNumbers) {
var start = this.state["countupTime" + p][printNumbers[i]] var start = this.state["countupTime" + p][printNumbers[i]]
this.draw.layeredText({ this.draw.layeredText({
ctx: ctx, ctx: ctx,
@@ -824,25 +824,25 @@ class Scoresheet{
letterSpacing: 1, letterSpacing: 1,
align: "right" align: "right"
}, [ }, [
{outline: "#000", letterBorder: 9}, { outline: "#000", letterBorder: 9 },
{fill: "#fff"} { fill: "#fff" }
]) ])
} }
if(this.state.countupShown){ if (this.state.countupShown) {
if(!this.state["countup" + p]){ if (!this.state["countup" + p]) {
this.state["countup" + p] = true this.state["countup" + p] = true
this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07]) this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07])
} }
}else if(this.state["countup" + p]){ } else if (this.state["countup" + p]) {
this.state["countup" + p] = false this.state["countup" + p] = false
this.stopSound("se_results_countup", p) this.stopSound("se_results_countup", p)
if(this.state.screen === "fadeIn"){ if (this.state.screen === "fadeIn") {
this.playSound("neiro_1_don", p) this.playSound("neiro_1_don", p)
} }
} }
if(this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd){ if (this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd) {
this.state.screen = "scoresShown" this.state.screen = "scoresShown"
this.state.screenMS = this.getMS() this.state.screenMS = this.getMS()
} }
@@ -850,28 +850,28 @@ class Scoresheet{
ctx.restore() ctx.restore()
} }
if(this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){ if (this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000) {
this.state.scoreNext = true this.state.scoreNext = true
if(p2.session){ if (p2.session) {
p2.send("songsel") p2.send("songsel")
}else{ } else {
this.toSongsel(true) this.toSongsel(true)
} }
} }
if(this.state.screen === "fadeOut"){ if (this.state.screen === "fadeOut") {
if(this.state.hasPointer === 1){ if (this.state.hasPointer === 1) {
this.state.hasPointer = 2 this.state.hasPointer = 2
this.canvas.style.cursor = "" this.canvas.style.cursor = ""
} }
if(!this.fadeScreenBlack){ if (!this.fadeScreenBlack) {
this.fadeScreenBlack = true this.fadeScreenBlack = true
this.fadeScreen.style.backgroundColor = "#000" this.fadeScreen.style.backgroundColor = "#000"
} }
var elapsed = ms - this.state.screenMS var elapsed = ms - this.state.screenMS
if(elapsed >= 1000){ if (elapsed >= 1000) {
this.clean() this.clean()
this.controller.songSelection(true, this.showWarning) this.controller.songSelection(true, this.showWarning)
} }
@@ -880,80 +880,106 @@ class Scoresheet{
ctx.restore() ctx.restore()
} }
getNumber(score, start, elapsed){ getNumber(score, start, elapsed) {
var numberPos = Math.floor((elapsed - start) / this.frame) var numberPos = Math.floor((elapsed - start) / this.frame)
if(numberPos < 0){ if (numberPos < 0) {
return "" return ""
} }
var output = "" var output = ""
for(var i = 0; i < score.length; i++){ for (var i = 0; i < score.length; i++) {
if(numberPos < 30 * (i + 1)){ if (numberPos < 30 * (i + 1)) {
this.state.countupShown = true this.state.countupShown = true
return this.numbers[numberPos % 30] + output return this.numbers[numberPos % 30] + output
}else{ } else {
output = score[score.length - i - 1] + output output = score[score.length - i - 1] + output
} }
} }
return output return output
} }
getSound(id, p){ getSound(id, p) {
return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")] return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")]
} }
playSound(id, p){ playSound(id, p) {
this.getSound(id, p).play() this.getSound(id, p).play()
} }
loopSound(id, p, args){ loopSound(id, p, args) {
this.getSound(id, p).playLoop(...args) this.getSound(id, p).playLoop(...args)
} }
stopSound(id, p){ stopSound(id, p) {
this.getSound(id, p).stop() this.getSound(id, p).stop()
} }
mod(length, index){ mod(length, index) {
return ((index % length) + length) % length return ((index % length) + length) % length
} }
getMS(){ getMS() {
return Date.now() return Date.now()
} }
saveScore(){ saveScore() {
if(this.controller.saveScore){ if (this.controller.saveScore) {
if(this.resultsObj.points < 0){ if (this.resultsObj.points < 0) {
this.resultsObj.points = 0 this.resultsObj.points = 0
} }
var title = this.controller.selectedSong.originalTitle var title = this.controller.selectedSong.originalTitle
var hash = this.controller.selectedSong.hash var hash = this.controller.selectedSong.hash
var difficulty = this.resultsObj.difficulty var difficulty = this.resultsObj.difficulty
var songId = this.controller.selectedSong.id
var oldScore = scoreStorage.get(hash, difficulty, true) var oldScore = scoreStorage.get(hash, difficulty, true)
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge) var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
var crown = "" var crown = ""
if(clearReached){ if (clearReached) {
crown = this.resultsObj.bad === 0 ? "gold" : "silver" crown = this.resultsObj.bad === 0 ? "gold" : "silver"
} }
if(!oldScore || oldScore.points <= this.resultsObj.points){ if (!oldScore || oldScore.points <= this.resultsObj.points) {
if(oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)){ if (oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)) {
crown = oldScore.crown crown = oldScore.crown
} }
this.resultsObj.crown = crown this.resultsObj.crown = crown
delete this.resultsObj.title delete this.resultsObj.title
delete this.resultsObj.difficulty delete this.resultsObj.difficulty
delete this.resultsObj.gauge delete this.resultsObj.gauge
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => { scoreStorage.add(hash, difficulty, this.resultsObj, true, title).then(() => {
this.showWarning = {name: "scoreSaveFailed"} // Auto-submit to leaderboard if logged in and has song ID
this.submitToLeaderboard(songId, difficulty, this.resultsObj)
}).catch(() => {
this.showWarning = { name: "scoreSaveFailed" }
}) })
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){ } else if (oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)) {
oldScore.crown = crown oldScore.crown = crown
scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => { scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
this.showWarning = {name: "scoreSaveFailed"} this.showWarning = { name: "scoreSaveFailed" }
}) })
} }
} }
this.scoreSaved = true this.scoreSaved = true
} }
clean(){ submitToLeaderboard(songId, difficulty, scoreObj) {
// Only submit if user is logged in and song has valid ID
if (!account.loggedIn || !songId) {
return
}
loader.getCsrfToken().then(token => {
var request = new XMLHttpRequest()
request.open("POST", "api/leaderboard/submit")
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.setRequestHeader("X-CSRFToken", token)
request.send(JSON.stringify({
song_id: songId,
difficulty: difficulty,
score: scoreObj
}))
}).catch(() => {
// Silently fail - leaderboard submission is optional
console.log("Leaderboard submission failed")
})
}
clean() {
this.keyboard.clean() this.keyboard.clean()
this.gamepad.clean() this.gamepad.clean()
this.draw.clean() this.draw.clean()
@@ -962,13 +988,13 @@ class Scoresheet{
snd.buffer.loadSettings() snd.buffer.loadSettings()
this.redrawRunning = false this.redrawRunning = false
pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
if(this.touchEnabled){ if (this.touchEnabled) {
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
} }
if(this.session){ if (this.session) {
pageEvents.remove(p2, "message") pageEvents.remove(p2, "message")
} }
if(!this.multiplayer){ if (!this.multiplayer) {
delete this.tetsuoHana delete this.tetsuoHana
} }
delete this.ctx delete this.ctx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
<div id="leaderboard">
<canvas id="leaderboard-canvas"></canvas>
</div>

View File

@@ -80,3 +80,14 @@ scores_save = {
} }
} }
} }
leaderboard_submit = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'song_id': {'type': 'number'},
'difficulty': {'type': 'string'},
'score': {'type': 'object'}
},
'required': ['song_id', 'difficulty', 'score']
}

View File

@@ -1,17 +1,20 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>太鼓ウェブ - Taiko Web | (゚∀゚)</title> <title>太鼓ウェブ - Taiko Web | (゚∀゚)</title>
<link rel="icon" href="{{config.assets_baseurl}}img/favicon.png" type="image/png"> <link rel="icon" href="{{config.assets_baseurl}}img/favicon.png" type="image/png">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="description" content="2025年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 Taiko no Tatsujin rhythm game simulator for desktop and mobile browsers"> <meta name="description"
<meta name="keywords" content="taiko no tatsujin, taiko, don chan, online, rhythm, browser, html5, game, for browsers, pc, arcade, emulator, free, download, website, 太鼓の達人, 太鼓ウェブ, 太鼓之達人, 太鼓達人, 太鼓网页, 网页版, 太鼓網頁, 網頁版, 태고의 달인, 태고 웹"> content="2026年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 Taiko no Tatsujin rhythm game simulator for desktop and mobile browsers">
<meta name="keywords"
content="taiko no tatsujin, taiko, don chan, online, rhythm, browser, html5, game, for browsers, pc, arcade, emulator, free, download, website, 太鼓の達人, 太鼓ウェブ, 太鼓之達人, 太鼓達人, 太鼓网页, 网页版, 太鼓網頁, 網頁版, 태고의 달인, 태고 웹">
<meta name="robots" content="notranslate"> <meta name="robots" content="notranslate">
<meta name="robots" content="noimageindex"> <meta name="robots" content="noimageindex">
<meta name="color-scheme" content="only light"> <meta name="color-scheme" content="only light">
<link rel="canonical" href="https://taikoapp.uk/" /> <link rel="canonical" href="https://taikoapp.uk/" />
<link rel="stylesheet" href="src/css/loader.css?{{version.commit_short}}"> <link rel="stylesheet" href="src/css/loader.css?{{version.commit_short}}">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
@@ -24,14 +27,17 @@
<script src="src/js/lib/jszip.js"></script> <script src="src/js/lib/jszip.js"></script>
</head> </head>
<body> <body>
<div id="assets"></div> <div id="assets"></div>
<div id="screen" class="pattern-bg"></div> <div id="screen" class="pattern-bg"></div>
<div data-nosnippet id="version"> <div data-nosnippet id="version">
{% if version.version and version.commit_short and version.commit %} {% if version.version and version.commit_short and version.commit %}
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a> <a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link"
class="stroke-sub" alt="vLightNova 1.1.0">vLightNova 1.1.0</a>
{% else %} {% else %}
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a> <a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.1.0">vLightNova
1.1.0</a>
{% endif %} {% endif %}
</div> </div>
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script> <script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
@@ -43,4 +49,5 @@
</div> </div>
</noscript> </noscript>
</body> </body>
</html> </html>