12 Commits
Bang ... Bang3

11 changed files with 988 additions and 277 deletions

17
app.py
View File

@@ -789,7 +789,13 @@ def route_api_leaderboard_submit():
else: else:
score_obj = score_data score_obj = score_data
score_value = int(score_obj.get('score', 0)) # Check for 'score' first, then 'points'
# Frontend usually sends 'points'
score_val = score_obj.get('score')
if score_val is None:
score_val = score_obj.get('points')
score_value = int(score_val or 0)
except: except:
return api_error('invalid_score_format') return api_error('invalid_score_format')
@@ -858,7 +864,6 @@ def route_api_leaderboard_submit():
return jsonify({'status': 'ok', 'message': 'score_submitted'}) return jsonify({'status': 'ok', 'message': 'score_submitted'})
@app.route(basedir + 'api/leaderboard/get') @app.route(basedir + 'api/leaderboard/get')
def route_api_leaderboard_get(): def route_api_leaderboard_get():
song_id = request.args.get('song_id', None) song_id = request.args.get('song_id', None)
@@ -867,10 +872,13 @@ def route_api_leaderboard_get():
if not song_id or not difficulty: if not song_id or not difficulty:
return abort(400) return abort(400)
# Accept both numeric IDs and hash strings
# Try to convert to int if possible, otherwise use as string
try: try:
song_id = int(song_id) song_id = int(song_id)
except: except (ValueError, TypeError):
return abort(400) # Keep as string (hash ID)
pass
# Validate difficulty # Validate difficulty
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura'] valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
@@ -901,6 +909,7 @@ def route_api_leaderboard_get():
return jsonify({'status': 'ok', 'leaderboard': leaderboard, 'month': current_month}) 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')))

201
local_debug.py Normal file
View File

@@ -0,0 +1,201 @@
import time
import json
import base64
# Mock DB and objects
class MockCollection:
def __init__(self, name):
self.name = name
self.data = []
def find_one(self, query):
for item in self.data:
match = True
for k, v in query.items():
if item.get(k) != v:
match = False
break
if match:
return item
return None
def find(self, query, projection=None):
results = []
for item in self.data:
match = True
for k, v in query.items():
if item.get(k) != v:
match = False
break
if match:
if projection:
filtered = {k: v for k, v in item.items() if projection.get(k, False)}
if projection.get('_id') is None or projection.get('_id') is True:
filtered['_id'] = item.get('_id')
results.append(filtered)
else:
results.append(item)
return MockCursor(results)
def insert_one(self, doc):
doc['_id'] = len(self.data) + 1
print(f"[{self.name}] Inserted: {doc}")
self.data.append(doc)
def count_documents(self, query):
return len(self.data)
class MockCursor:
def __init__(self, data):
self.data = data
def sort(self, key, direction):
self.data.sort(key=lambda x: x.get(key, 0), reverse=(direction < 0))
return self
def limit(self, n):
self.data = self.data[:n]
return self.data
class MockDB:
def __init__(self):
self.users = MockCollection('users')
self.leaderboards = MockCollection('leaderboards')
db = MockDB()
db.users.insert_one({'username': 'testuser', 'display_name': 'Test User'})
# --- Logic from app.py rewritten for standalone test ---
def logic_submit(data, username):
print(f"\n--- Submitting {data.get('song_id')} ---")
user = db.users.find_one({'username': username})
if not user:
return {'error': 'user_not_found'}
song_id = data.get('song_id')
difficulty = data.get('difficulty')
score_data = data.get('score')
current_month = time.strftime('%Y-%m', time.gmtime())
existing = db.leaderboards.find_one({
'song_id': song_id,
'difficulty': difficulty,
'username': username,
'month': current_month
})
# Patched Logic from app.py
try:
if isinstance(score_data, str):
score_obj = json.loads(score_data)
else:
score_obj = score_data
# Check for 'score' first, then 'points'
score_val = score_obj.get('score')
if score_val is None:
score_val = score_obj.get('points')
score_value = int(score_val or 0)
print(f" Parsed Score Value: {score_value}")
except Exception as e:
print(f"Error parsing score: {e}")
return {'error': 'invalid_score_format'}
if existing:
print(" Existing record found (simulated update).")
else:
print(" Inserting 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
})
return {'status': 'ok'}
def logic_get(args):
print(f"\n--- Getting {args.get('song_id')} ---")
song_id = args.get('song_id', None)
difficulty = args.get('difficulty', None)
if not song_id or not difficulty:
return {'error': 'missing args'}
try:
song_id = int(song_id)
except (ValueError, TypeError):
pass
current_month = time.strftime('%Y-%m', time.gmtime())
leaderboard = list(db.leaderboards.find({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
}, {
'_id': False,
'username': True,
'score_value': True
}).sort('score_value', -1).limit(50))
return {'status': 'ok', 'leaderboard': leaderboard}
# --- Test Cases ---
# Case 1: Hash ID
print("=== TEST CASE 1: Hash ID ===")
logic_submit({
'song_id': "hash123",
'difficulty': 'oni',
'score': {'score': 1000000}
}, 'testuser')
res1 = logic_get({'song_id': "hash123", 'difficulty': 'oni'})
if len(res1['leaderboard']) > 0: print("✅ Success")
else: print("❌ Failed")
# Case 2: Numeric ID
print("\n=== TEST CASE 2: Numeric ID ===")
logic_submit({
'song_id': 123,
'difficulty': 'oni',
'score': {'score': 2000000}
}, 'testuser')
res2 = logic_get({'song_id': "123", 'difficulty': 'oni'})
if len(res2['leaderboard']) > 0: print("✅ Success")
else: print("❌ Failed")
# Case 3: Points Only (Frontend Payload Simulation)
print("\n=== TEST CASE 3: Points Only (Frontend Payload) ===")
# This simulates what the JS 'this.resultsObj' looks like (it has 'points', not 'score')
logic_submit({
'song_id': "points_song",
'difficulty': 'hard',
'score': {'points': 345678, 'bad': 0, 'good': 100} # No 'score' key
}, 'testuser')
res3 = logic_get({'song_id': "points_song", 'difficulty': 'hard'})
passed = False
if len(res3.get('leaderboard', [])) > 0:
val = res3['leaderboard'][0].get('score_value')
if val == 345678:
print(f"✅ SUCCESS: Correctly parsed 'points' -> {val}")
passed = True
else:
print(f"❌ FAILED: Score value mismatch. Expected 345678, got {val}")
else:
print("❌ FAILED: No record found")
if passed:
print("\n---------------------------------------------------")
print("🎉 ALL LOCAL TESTS PASSED. BACKEND LOGIC VERIFIED.")
print("---------------------------------------------------")
else:
print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
print("🛑 TESTS FAILED. CHECK LOGS.")
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")

View File

@@ -7,6 +7,7 @@ var assets = {
"titlescreen.js", "titlescreen.js",
"scoresheet.js", "scoresheet.js",
"songselect.js", "songselect.js",
"leaderboard.js",
"keyboard.js", "keyboard.js",
"gameinput.js", "gameinput.js",
"game.js", "game.js",
@@ -98,7 +99,7 @@ var assets = {
"audioSfx": [ "audioSfx": [
"se_pause.ogg", "se_pause.ogg",
"se_calibration.ogg", "se_calibration.ogg",
"v_results.ogg", "v_results.ogg",
"v_sanka.ogg", "v_sanka.ogg",
"v_songsel.ogg", "v_songsel.ogg",
@@ -112,14 +113,14 @@ var assets = {
"se_don.ogg", "se_don.ogg",
"se_ka.ogg", "se_ka.ogg",
"se_jump.ogg", "se_jump.ogg",
"se_balloon.ogg", "se_balloon.ogg",
"se_gameclear.ogg", "se_gameclear.ogg",
"se_gamefail.ogg", "se_gamefail.ogg",
"se_gamefullcombo.ogg", "se_gamefullcombo.ogg",
"se_results_countup.ogg", "se_results_countup.ogg",
"se_results_crown.ogg", "se_results_crown.ogg",
"v_fullcombo.ogg", "v_fullcombo.ogg",
"v_renda.ogg", "v_renda.ogg",
"v_results_fullcombo.ogg", "v_results_fullcombo.ogg",
@@ -140,6 +141,7 @@ var assets = {
}, },
"views": [ "views": [
"game.html", "game.html",
"leaderboard.html",
"loadsong.html", "loadsong.html",
"songselect.html", "songselect.html",
"titlescreen.html", "titlescreen.html",
@@ -153,7 +155,7 @@ var assets = {
"customsongs.html", "customsongs.html",
"search.html" "search.html"
], ],
"songs": [], "songs": [],
"sounds": {}, "sounds": {},
"image": {}, "image": {},

View File

@@ -1,12 +1,21 @@
class Leaderboard { class Leaderboard {
constructor() { constructor() {
this.init() this.canvas = null
this.ctx = null
this.songId = null
this.difficulty = null
this.leaderboardData = []
this.currentMonth = ""
this.visible = false
this.draw = null
this.keyboard = null
} }
init() { init() {
this.canvas = document.getElementById("leaderboard-canvas") this.canvas = document.getElementById("leaderboard-canvas")
if (!this.canvas) { if (!this.canvas) {
return console.error("Leaderboard canvas not found!")
return false
} }
this.ctx = this.canvas.getContext("2d") this.ctx = this.canvas.getContext("2d")
@@ -16,12 +25,6 @@ class Leaderboard {
this.ctx.imageSmoothingEnabled = false this.ctx.imageSmoothingEnabled = false
} }
this.songId = null
this.difficulty = null
this.leaderboardData = []
this.currentMonth = ""
this.visible = false
this.draw = new CanvasDraw(noSmoothing) this.draw = new CanvasDraw(noSmoothing)
// Keyboard controls // Keyboard controls
@@ -33,6 +36,7 @@ class Leaderboard {
}, this.keyPress.bind(this)) }, this.keyPress.bind(this))
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.onClose.bind(this)) pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.onClose.bind(this))
return true
} }
keyPress(pressed, name) { keyPress(pressed, name) {
@@ -55,6 +59,12 @@ class Leaderboard {
loader.changePage("leaderboard", false) loader.changePage("leaderboard", false)
// Initialize after page is loaded
if (!this.init()) {
console.error("Failed to initialize leaderboard")
return
}
// Fetch leaderboard data // Fetch leaderboard data
await this.fetchLeaderboard() await this.fetchLeaderboard()
@@ -67,10 +77,18 @@ class Leaderboard {
} }
async fetchLeaderboard() { async fetchLeaderboard() {
// Validate songId exists
if (!this.songId) {
console.error("Missing song ID for leaderboard")
this.leaderboardData = []
return
}
try { try {
var response = await loader.ajax( var response = await loader.ajax(
`${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${this.songId}&difficulty=${this.difficulty}` `${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${encodeURIComponent(this.songId)}&difficulty=${this.difficulty}`
) )
var data = JSON.parse(response) var data = JSON.parse(response)
if (data.status === "ok") { if (data.status === "ok") {
this.leaderboardData = data.leaderboard || [] this.leaderboardData = data.leaderboard || []
@@ -84,6 +102,7 @@ class Leaderboard {
} }
} }
changeDifficulty(direction) { changeDifficulty(direction) {
var difficulties = ["easy", "normal", "hard", "oni", "ura"] var difficulties = ["easy", "normal", "hard", "oni", "ura"]
var currentIndex = difficulties.indexOf(this.difficulty) var currentIndex = difficulties.indexOf(this.difficulty)
@@ -108,11 +127,11 @@ class Leaderboard {
var x = (event.offsetX || event.touches[0].pageX - rect.left) var x = (event.offsetX || event.touches[0].pageX - rect.left)
var y = (event.offsetY || event.touches[0].pageY - rect.top) var y = (event.offsetY || event.touches[0].pageY - rect.top)
// Check if clicked outside modal (approximately) // Check if clicked outside modal - updated dimensions
var centerX = this.canvas.width / 2 var centerX = this.canvas.width / 2
var centerY = this.canvas.height / 2 var centerY = this.canvas.height / 2
var modalWidth = 800 var modalWidth = 880 // Updated from 800
var modalHeight = 600 var modalHeight = 640 // Updated from 600
if (x < centerX - modalWidth / 2 || x > centerX + modalWidth / 2 || if (x < centerX - modalWidth / 2 || x > centerX + modalWidth / 2 ||
y < centerY - modalHeight / 2 || y > centerY + modalHeight / 2) { y < centerY - modalHeight / 2 || y > centerY + modalHeight / 2) {
@@ -158,119 +177,223 @@ class Leaderboard {
this.ctx.scale(ratio, ratio) this.ctx.scale(ratio, ratio)
// Draw modal background // Draw modal background with gradient
var modalX = 240 var modalX = 200
var modalY = 60 var modalY = 40
var modalW = 800 var modalW = 880
var modalH = 600 var modalH = 640
this.ctx.fillStyle = "#ffffff" // 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.fillRect(modalX, modalY, modalW, modalH)
this.ctx.shadowBlur = 0
this.ctx.strokeStyle = "#333333" // Modal border with gradient
this.ctx.lineWidth = 4 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) this.ctx.strokeRect(modalX, modalY, modalW, modalH)
// Draw title // Draw title with gradient
this.ctx.fillStyle = "#000000" var titleGradient = this.ctx.createLinearGradient(0, 90, 0, 130)
this.ctx.font = "bold 40px " + (strings.font || "sans-serif") 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.textAlign = "center"
this.ctx.fillText("🏆 排行榜 Leaderboard", 640, 110) 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 // Draw difficulty selector with improved styling
var diffX = 640 var diffX = 640
var diffY = 160 var diffY = 155
var difficulties = [ var difficulties = [
{ id: "easy", name: "簡単", color: "#00a0e9" }, { id: "easy", name: "簡単", color: "#00a0e9", glow: "#00d4ff" },
{ id: "normal", name: "普通", color: "#00a040" }, { id: "normal", name: "普通", color: "#00a040", glow: "#00ff66" },
{ id: "hard", name: "難しい", color: "#ff8c00" }, { id: "hard", name: "難しい", color: "#ff8c00", glow: "#ffb347" },
{ id: "oni", name: "鬼", color: "#dc143c" }, { id: "oni", name: "鬼", color: "#dc143c", glow: "#ff6b9d" },
{ id: "ura", name: "裏", color: "#9400d3" } { id: "ura", name: "裏", color: "#9400d3", glow: "#da70d6" }
] ]
this.ctx.font = "24px " + (strings.font || "sans-serif") this.ctx.font = "bold 22px " + (strings.font || "sans-serif")
for (var i = 0; i < difficulties.length; i++) { for (var i = 0; i < difficulties.length; i++) {
var diff = difficulties[i] var diff = difficulties[i]
var x = diffX - 200 + i * 100 var x = diffX - 220 + i * 110
if (diff.id === this.difficulty) { if (diff.id === this.difficulty) {
this.ctx.fillStyle = diff.color // Selected difficulty with glow effect
this.ctx.fillRect(x - 45, diffY - 25, 90, 40) 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.fillStyle = "#ffffff"
this.ctx.shadowColor = "rgba(0, 0, 0, 0.5)"
this.ctx.shadowBlur = 3
} else { } else {
// Unselected difficulty
this.ctx.strokeStyle = diff.color this.ctx.strokeStyle = diff.color
this.ctx.lineWidth = 2 this.ctx.lineWidth = 3
this.ctx.strokeRect(x - 45, diffY - 25, 90, 40) this.ctx.strokeRect(x - 50, diffY - 30, 100, 50)
this.ctx.fillStyle = diff.color this.ctx.fillStyle = diff.color
} }
this.ctx.textAlign = "center" this.ctx.textAlign = "center"
this.ctx.fillText(diff.name, x, diffY + 5) this.ctx.fillText(diff.name, x, diffY)
this.ctx.shadowBlur = 0
} }
// Draw month info // Draw month info with style
this.ctx.fillStyle = "#666666" this.ctx.fillStyle = "#666666"
this.ctx.font = "18px " + (strings.font || "sans-serif") this.ctx.font = "20px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center" this.ctx.textAlign = "center"
this.ctx.fillText("当月排行 " + this.currentMonth, 640, 210) 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 // Draw leaderboard entries
var startY = 240 var startY = 295
var rowHeight = 35 var rowHeight = 38
this.ctx.font = "20px " + (strings.font || "sans-serif") this.ctx.font = "22px " + (strings.font || "sans-serif")
this.ctx.textAlign = "left" this.ctx.textAlign = "left"
if (this.leaderboardData.length === 0) { if (this.leaderboardData.length === 0) {
this.ctx.fillStyle = "#999999" this.ctx.fillStyle = "#999999"
this.ctx.font = "24px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center" this.ctx.textAlign = "center"
this.ctx.fillText("暂无排行数据", 640, startY + 100) this.ctx.fillText("暂无排行数据", 640, startY + 120)
} else { } else {
for (var i = 0; i < Math.min(this.leaderboardData.length, 15); i++) { for (var i = 0; i < Math.min(this.leaderboardData.length, 15); i++) {
var entry = this.leaderboardData[i] var entry = this.leaderboardData[i]
var y = startY + i * rowHeight var y = startY + i * rowHeight
var rank = entry.rank var rank = entry.rank
// Rank background color // Rank background color with gradient
var gradient
if (rank === 1) { if (rank === 1) {
this.ctx.fillStyle = "#ffd700" // Gold // 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) { } else if (rank === 2) {
this.ctx.fillStyle = "#c0c0c0" // Silver // 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) { } else if (rank === 3) {
this.ctx.fillStyle = "#cd7f32" // Bronze // 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 { } else {
this.ctx.fillStyle = "#f5f5f5" // 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
} }
this.ctx.fillRect(modalX + 20, y - 20, modalW - 40, 30)
// Rank // Rounded rectangle effect
this.ctx.fillStyle = rank <= 3 ? "#ffffff" : "#333333" this.ctx.fillRect(modalX + 20, y - 25, modalW - 40, 35)
this.ctx.font = "bold 20px " + (strings.font || "sans-serif") this.ctx.shadowBlur = 0
this.ctx.textAlign = "center"
this.ctx.fillText(rank, modalX + 60, y) // 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 // Display name
this.ctx.fillStyle = "#000000" this.ctx.fillStyle = rank <= 3 ? "#1a1a1a" : "#333333"
this.ctx.font = "20px " + (strings.font || "sans-serif") this.ctx.font = (rank <= 3 ? "bold " : "") + "20px " + (strings.font || "sans-serif")
this.ctx.textAlign = "left" this.ctx.textAlign = "left"
var displayName = entry.display_name || entry.username var displayName = entry.display_name || entry.username
if (displayName.length > 15) { if (displayName.length > 18) {
displayName = displayName.substring(0, 15) + "..." displayName = displayName.substring(0, 18) + "..."
} }
this.ctx.fillText(displayName, modalX + 100, y) this.ctx.fillText(displayName, modalX + 140, y + 2)
// Score // Score with formatting
var score = entry.score && entry.score.score ? entry.score.score : 0 // Score with formatting
var score = entry.score_value !== undefined ? entry.score_value : (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.textAlign = "right"
this.ctx.fillText(score.toLocaleString(), modalX + modalW - 40, y) this.ctx.fillText(score.toLocaleString(), modalX + modalW - 60, y + 2)
} }
} }
// Draw close hint // Draw close hint with icon
this.ctx.fillStyle = "#666666" this.ctx.fillStyle = "#888888"
this.ctx.font = "18px " + (strings.font || "sans-serif") this.ctx.font = "18px " + (strings.font || "sans-serif")
this.ctx.textAlign = "center" this.ctx.textAlign = "center"
this.ctx.fillText("按ESC或点击外部关闭 Press ESC or click outside to close", 640, 680) this.ctx.fillText("⌨️ 按ESC或点击外部关闭 Press ESC or click outside to close", 640, 665)
this.ctx.restore() this.ctx.restore()
} }

View File

@@ -178,8 +178,94 @@ class Scoresheet {
} }
this.game.appendChild(this.tetsuoHana) this.game.appendChild(this.tetsuoHana)
} }
// Add leaderboard submit button if user is logged in
if (account.loggedIn && !this.multiplayer) {
this.leaderboardBtn = document.createElement("div")
this.leaderboardBtn.id = "leaderboard-submit-btn"
this.leaderboardBtn.innerHTML = "🏆 提交排行榜<br><small>Submit to Leaderboard</small>"
this.leaderboardBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 15px 25px;
background: linear-gradient(135deg, #ff6b9d, #c44db3);
color: white;
border-radius: 15px;
font-size: 18px;
font-family: ${strings.font || "sans-serif"};
text-align: center;
cursor: pointer;
z-index: 1000;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
border: 3px solid #fff;
transition: all 0.2s ease;
user-select: none;
`
this.leaderboardBtn.onmouseover = () => {
if (!this.leaderboardSubmitted) {
this.leaderboardBtn.style.transform = "scale(1.05)"
this.leaderboardBtn.style.boxShadow = "0 6px 20px rgba(0,0,0,0.4)"
}
}
this.leaderboardBtn.onmouseout = () => {
this.leaderboardBtn.style.transform = "scale(1)"
this.leaderboardBtn.style.boxShadow = "0 4px 15px rgba(0,0,0,0.3)"
}
this.leaderboardBtn.onclick = () => this.onLeaderboardBtnClick()
this.game.appendChild(this.leaderboardBtn)
}
} }
onLeaderboardBtnClick() {
if (this.leaderboardSubmitted) {
return
}
// Fallback: This is CRITICAL for cases where saveScore() failed or was skipped
if (!this.leaderboardData) {
console.warn("Leaderboard data missing, attempting to reconstruct...")
var song = this.controller.selectedSong
var results = this.resultsObj
if (song && results) {
var songId = song.id || song.hash
if (songId) {
this.leaderboardData = {
songId: songId,
difficulty: results.difficulty,
scoreObj: Object.assign({ score: results.points }, results)
}
// Clean up scoreObj
if (this.leaderboardData.scoreObj) {
delete this.leaderboardData.scoreObj.title
delete this.leaderboardData.scoreObj.difficulty
delete this.leaderboardData.scoreObj.gauge
}
console.log("Leaderboard data reconstructed:", this.leaderboardData)
}
}
}
if (!this.leaderboardData || !this.leaderboardData.songId) {
this.showLeaderboardNotification("no_song_id")
return
}
// Disable button and show submitting state
this.leaderboardBtn.innerHTML = "⏳ 提交中..."
this.leaderboardBtn.style.background = "linear-gradient(135deg, #888, #666)"
this.leaderboardBtn.style.cursor = "default"
this.submitToLeaderboard(
this.leaderboardData.songId,
this.leaderboardData.difficulty,
this.leaderboardData.scoreObj
)
}
redraw() { redraw() {
if (!this.redrawRunning) { if (!this.redrawRunning) {
return return
@@ -926,13 +1012,24 @@ class Scoresheet {
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 // Use id if available, otherwise use hash
var songId = this.controller.selectedSong.id || hash
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"
} }
// Store data for manual leaderboard submission
this.leaderboardData = {
songId: songId,
difficulty: difficulty,
scoreObj: Object.assign({ score: this.resultsObj.points }, this.resultsObj)
}
this.leaderboardSubmitted = false
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
@@ -941,10 +1038,7 @@ class Scoresheet {
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).then(() => { scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
// Auto-submit to leaderboard if logged in and has song ID
this.submitToLeaderboard(songId, difficulty, this.resultsObj)
}).catch(() => {
this.showWarning = { name: "scoreSaveFailed" } 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)) {
@@ -957,28 +1051,130 @@ class Scoresheet {
this.scoreSaved = true this.scoreSaved = true
} }
submitToLeaderboard(songId, difficulty, scoreObj) { submitToLeaderboard(songId, difficulty, scoreObj) {
// Only submit if user is logged in and song has valid ID // Only submit if user is logged in and song has an ID
if (!account.loggedIn || !songId) { if (!account.loggedIn || !songId) {
return return
} }
var self = this
loader.getCsrfToken().then(token => { loader.getCsrfToken().then(token => {
var request = new XMLHttpRequest() var request = new XMLHttpRequest()
request.open("POST", "api/leaderboard/submit") request.open("POST", "api/leaderboard/submit")
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.setRequestHeader("X-CSRFToken", token) request.setRequestHeader("X-CSRFToken", token)
request.onload = function () {
if (request.status === 200) {
try {
var response = JSON.parse(request.responseText)
if (response.status === "ok") {
self.leaderboardSubmitted = true
self.showLeaderboardNotification(response.message)
// Update button to show success
if (self.leaderboardBtn) {
self.leaderboardBtn.innerHTML = "✅ 已提交<br><small>Submitted!</small>"
self.leaderboardBtn.style.background = "linear-gradient(135deg, #4CAF50, #45a049)"
}
} else {
// Show error
self.showLeaderboardNotification("error")
if (self.leaderboardBtn) {
self.leaderboardBtn.innerHTML = "❌ 失败<br><small>Failed</small>"
self.leaderboardBtn.style.background = "linear-gradient(135deg, #f44336, #d32f2f)"
}
}
} catch (e) {
console.error("Failed to parse leaderboard response:", e)
self.showLeaderboardNotification("error")
}
} else {
self.showLeaderboardNotification("error")
if (self.leaderboardBtn) {
self.leaderboardBtn.innerHTML = "❌ 失败<br><small>Failed</small>"
self.leaderboardBtn.style.background = "linear-gradient(135deg, #f44336, #d32f2f)"
}
}
}
request.onerror = function () {
self.showLeaderboardNotification("error")
if (self.leaderboardBtn) {
self.leaderboardBtn.innerHTML = "❌ 失败<br><small>Failed</small>"
self.leaderboardBtn.style.background = "linear-gradient(135deg, #f44336, #d32f2f)"
}
}
request.send(JSON.stringify({ request.send(JSON.stringify({
song_id: songId, song_id: songId,
difficulty: difficulty, difficulty: difficulty,
score: scoreObj score: scoreObj
})) }))
}).catch(() => { }).catch(() => {
// Silently fail - leaderboard submission is optional
console.log("Leaderboard submission failed") console.log("Leaderboard submission failed")
this.showLeaderboardNotification("error")
if (this.leaderboardBtn) {
this.leaderboardBtn.innerHTML = "❌ 失败<br><small>Failed</small>"
this.leaderboardBtn.style.background = "linear-gradient(135deg, #f44336, #d32f2f)"
}
}) })
} }
showLeaderboardNotification(message) {
var notification = document.createElement("div")
notification.className = "leaderboard-notification"
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
background: linear-gradient(135deg, #4a90e2, #7b68ee);
color: white;
border-radius: 10px;
font-size: 16px;
font-family: ${strings.font || "sans-serif"};
z-index: 10000;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
opacity: 0;
transform: translateX(100px);
transition: all 0.3s ease-out;
`
var text = ""
switch (message) {
case "score_submitted": text = "🏆 成绩已提交到排行榜!"; break
case "score_updated": text = "🎉 排行榜成绩已更新!"; break
case "score_not_higher": text = "📊 已有更高成绩"; break
case "score_too_low": text = "未进入排行榜前50"; break
case "error": text = "❌ 提交失败,请重试"; break
case "no_song_id": text = "❌ 无法提交歌曲ID缺失"; break
default: text = "排行榜已更新"
}
notification.innerText = text
document.body.appendChild(notification)
// Trigger animation
setTimeout(() => {
notification.style.opacity = "1"
notification.style.transform = "translateX(0)"
}, 10)
// Remove after 3 seconds
setTimeout(() => {
notification.style.opacity = "0"
notification.style.transform = "translateX(100px)"
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification)
}
}, 300)
}, 3000)
}
clean() { clean() {
this.keyboard.clean() this.keyboard.clean()
this.gamepad.clean() this.gamepad.clean()
@@ -997,10 +1193,17 @@ class Scoresheet {
if (!this.multiplayer) { if (!this.multiplayer) {
delete this.tetsuoHana delete this.tetsuoHana
} }
// Clean up leaderboard button
if (this.leaderboardBtn && this.leaderboardBtn.parentNode) {
this.leaderboardBtn.parentNode.removeChild(this.leaderboardBtn)
}
delete this.leaderboardBtn
delete this.leaderboardData
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas
delete this.fadeScreen delete this.fadeScreen
delete this.results delete this.results
delete this.rules delete this.rules
} }
} }

View File

@@ -1,19 +1,19 @@
class Search{ class Search {
constructor(...args){ constructor(...args) {
this.init(...args) this.init(...args)
} }
init(songSelect){ init(songSelect) {
this.songSelect = songSelect this.songSelect = songSelect
this.opened = false this.opened = false
this.enabled = true this.enabled = true
this.style = document.createElement("style") this.style = document.createElement("style")
var css = [] var css = []
for(var i in this.songSelect.songSkin){ for (var i in this.songSelect.songSkin) {
var skin = this.songSelect.songSkin[i] var skin = this.songSelect.songSkin[i]
if("id" in skin || i === "default"){ if ("id" in skin || i === "default") {
var id = "id" in skin ? ("cat" + skin.id) : i var id = "id" in skin ? ("cat" + skin.id) : i
css.push(loader.cssRuleset({ css.push(loader.cssRuleset({
[".song-search-" + id]: { [".song-search-" + id]: {
"background-color": skin.background "background-color": skin.background
@@ -33,7 +33,7 @@ class Search{
loader.screen.appendChild(this.style) loader.screen.appendChild(this.style)
} }
normalizeString(string){ normalizeString(string) {
string = string string = string
.replace('', '\'').replace('“', '"').replace('”', '"') .replace('', '\'').replace('“', '"').replace('”', '"')
.replace('。', '.').replace('', ',').replace('、', ',') .replace('。', '.').replace('', ',').replace('、', ',')
@@ -44,29 +44,29 @@ class Search{
return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "") return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
} }
perform(query){ perform(query) {
var results = [] var results = []
var filters = {} var filters = {}
var querySplit = query.split(" ").filter(word => { var querySplit = query.split(" ").filter(word => {
if(word.length > 0){ if (word.length > 0) {
var parts = word.toLowerCase().split(":") var parts = word.toLowerCase().split(":")
if(parts.length > 1){ if (parts.length > 1) {
switch(parts[0]){ switch (parts[0]) {
case "easy": case "easy":
case "normal": case "normal":
case "hard": case "hard":
case "oni": case "oni":
case "ura": case "ura":
var range = this.parseRange(parts[1]) var range = this.parseRange(parts[1])
if(range){ if (range) {
filters[parts[0]] = range filters[parts[0]] = range
} }
break break
case "extreme": case "extreme":
var range = this.parseRange(parts[1]) var range = this.parseRange(parts[1])
if(range){ if (range) {
filters.oni = this.parseRange(parts[1]) filters.oni = this.parseRange(parts[1])
} }
break break
@@ -91,175 +91,175 @@ class Search{
} }
return true return true
}) })
query = this.normalizeString(querySplit.join(" ").trim()) query = this.normalizeString(querySplit.join(" ").trim())
var totalFilters = Object.keys(filters).length var totalFilters = Object.keys(filters).length
var random = false var random = false
var allResults = false var allResults = false
for(var i = 0; i < assets.songs.length; i++){ for (var i = 0; i < assets.songs.length; i++) {
var song = assets.songs[i] var song = assets.songs[i]
var passedFilters = 0 var passedFilters = 0
Object.keys(filters).forEach(filter => { Object.keys(filters).forEach(filter => {
var value = filters[filter] var value = filters[filter]
switch(filter){ switch (filter) {
case "easy": case "easy":
case "normal": case "normal":
case "hard": case "hard":
case "oni": case "oni":
case "ura": case "ura":
if(song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max){ if (song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max) {
passedFilters++ passedFilters++
} }
break break
case "clear": case "clear":
case "silver": case "silver":
case "gold": case "gold":
if(value === "any"){ if (value === "any") {
var score = scoreStorage.scores[song.hash] var score = scoreStorage.scores[song.hash]
scoreStorage.difficulty.forEach(difficulty => { scoreStorage.difficulty.forEach(difficulty => {
if(score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)){ if (score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)) {
passedFilters++ passedFilters++
} }
}) })
} else { } else {
var score = scoreStorage.scores[song.hash] var score = scoreStorage.scores[song.hash]
if(score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)){ if (score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)) {
passedFilters++ passedFilters++
} }
} }
break break
case "played": case "played":
var score = scoreStorage.scores[song.hash] var score = scoreStorage.scores[song.hash]
if((value === "yes" && score) || (value === "no" && !score)){ if ((value === "yes" && score) || (value === "no" && !score)) {
passedFilters++ passedFilters++
} }
break break
case "lyrics": case "lyrics":
if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){ if ((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)) {
passedFilters++ passedFilters++
} }
break break
case "creative": case "creative":
if((value === "yes" && song.maker) || (value === "no" && !song.maker)){ if ((value === "yes" && song.maker) || (value === "no" && !song.maker)) {
passedFilters++ passedFilters++
} }
break break
case "maker": case "maker":
if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){ if (song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())) {
passedFilters++ passedFilters++
} }
break break
case "genre": case "genre":
var cat = assets.categories.find(cat => cat.id === song.category_id) var cat = assets.categories.find(cat => cat.id === song.category_id)
var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title] var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title]
if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){ if (aliases.find(alias => alias.toLowerCase() === value.toLowerCase())) {
passedFilters++ passedFilters++
} }
break break
case "diverge": case "diverge":
var branch = Object.values(song.courses).find(course => course && course.branch) var branch = Object.values(song.courses).find(course => course && course.branch)
if((value === "yes" && branch) || (value === "no" && !branch)){ if ((value === "yes" && branch) || (value === "no" && !branch)) {
passedFilters++ passedFilters++
} }
break break
case "random": case "random":
if(value === "yes" || value === "no"){ if (value === "yes" || value === "no") {
random = value === "yes" random = value === "yes"
passedFilters++ passedFilters++
} }
break break
case "all": case "all":
if(value === "yes" || value === "no"){ if (value === "yes" || value === "no") {
allResults = value === "yes" allResults = value === "yes"
passedFilters++ passedFilters++
} }
break break
} }
}) })
if(passedFilters === totalFilters){ if (passedFilters === totalFilters) {
results.push(song) results.push(song)
} }
} }
var maxResults = allResults ? Infinity : (totalFilters > 0 && !query ? 100 : 50) var maxResults = allResults ? Infinity : (totalFilters > 0 && !query ? 100 : 50)
if(query){ if (query) {
results = fuzzysort.go(query, results, { results = fuzzysort.go(query, results, {
keys: ["titlePrepared", "subtitlePrepared"], keys: ["titlePrepared", "subtitlePrepared"],
allowTypo: true, allowTypo: true,
limit: maxResults, limit: maxResults,
scoreFn: a => { scoreFn: a => {
if(a[0]){ if (a[0]) {
var score0 = a[0].score var score0 = a[0].score
a[0].ranges = this.indexesToRanges(a[0].indexes) a[0].ranges = this.indexesToRanges(a[0].indexes)
if(a[0].indexes.length > 1){ if (a[0].indexes.length > 1) {
var rangeAmount = a[0].ranges.length var rangeAmount = a[0].ranges.length
var lastIdx = -3 var lastIdx = -3
a[0].ranges.forEach(range => { a[0].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){ if (range[0] - lastIdx <= 2) {
rangeAmount-- rangeAmount--
score0 -= 1000 score0 -= 1000
} }
lastIdx = range[1] lastIdx = range[1]
}) })
var index = a[0].target.toLowerCase().indexOf(query) var index = a[0].target.toLowerCase().indexOf(query)
if(index !== -1){ if (index !== -1) {
a[0].ranges = [[index, index + query.length - 1]] a[0].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[0].indexes.length / 2){ } else if (rangeAmount > a[0].indexes.length / 2) {
score0 = -Infinity score0 = -Infinity
a[0].ranges = null a[0].ranges = null
}else if(rangeAmount !== 1){ } else if (rangeAmount !== 1) {
score0 -= 9000 score0 -= 9000
} }
} }
} }
if(a[1]){ if (a[1]) {
var score1 = a[1].score - 1000 var score1 = a[1].score - 1000
a[1].ranges = this.indexesToRanges(a[1].indexes) a[1].ranges = this.indexesToRanges(a[1].indexes)
if(a[1].indexes.length > 1){ if (a[1].indexes.length > 1) {
var rangeAmount = a[1].ranges.length var rangeAmount = a[1].ranges.length
var lastIdx = -3 var lastIdx = -3
a[1].ranges.forEach(range => { a[1].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){ if (range[0] - lastIdx <= 2) {
rangeAmount-- rangeAmount--
score1 -= 1000 score1 -= 1000
} }
lastIdx = range[1] lastIdx = range[1]
}) })
var index = a[1].target.indexOf(query) var index = a[1].target.indexOf(query)
if(index !== -1){ if (index !== -1) {
a[1].ranges = [[index, index + query.length - 1]] a[1].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[1].indexes.length / 2){ } else if (rangeAmount > a[1].indexes.length / 2) {
score1 = -Infinity score1 = -Infinity
a[1].ranges = null a[1].ranges = null
}else if(rangeAmount !== 1){ } else if (rangeAmount !== 1) {
score1 -= 9000 score1 -= 9000
} }
} }
} }
if(random){ if (random) {
var rand = Math.random() * -9000 var rand = Math.random() * -9000
if(score0 !== -Infinity){ if (score0 !== -Infinity) {
score0 = rand score0 = rand
} }
if(score1 !== -Infinity){ if (score1 !== -Infinity) {
score1 = rand score1 = rand
} }
} }
if(a[0]){ if (a[0]) {
return a[1] ? Math.max(score0, score1) : score0 return a[1] ? Math.max(score0, score1) : score0
}else{ } else {
return a[1] ? score1 : -Infinity return a[1] ? score1 : -Infinity
} }
} }
}) })
}else{ } else {
if(random){ if (random) {
for(var i = results.length - 1; i > 0; i--){ for (var i = results.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1)) var j = Math.floor(Math.random() * (i + 1))
var temp = results[i] var temp = results[i]
results[i] = results[j] results[i] = results[j]
@@ -267,53 +267,53 @@ class Search{
} }
} }
results = results.slice(0, maxResults).map(result => { results = results.slice(0, maxResults).map(result => {
return {obj: result} return { obj: result }
}) })
} }
return results return results
} }
createResult(result, resultWidth, fontSize){ createResult(result, resultWidth, fontSize) {
var song = result.obj var song = result.obj
var title = this.songSelect.getLocalTitle(song.title, song.title_lang) var title = this.songSelect.getLocalTitle(song.title, song.title_lang)
var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
var id = "default" var id = "default"
if(song.category_id){ if (song.category_id) {
var cat = assets.categories.find(cat => cat.id === song.category_id) var cat = assets.categories.find(cat => cat.id === song.category_id)
if(cat && "id" in cat){ if (cat && "id" in cat) {
id = "cat" + cat.id id = "cat" + cat.id
} }
} }
var resultDiv = document.createElement("div") var resultDiv = document.createElement("div")
resultDiv.classList.add("song-search-result", "song-search-" + id) resultDiv.classList.add("song-search-result", "song-search-" + id)
resultDiv.dataset.songId = song.id resultDiv.dataset.songId = song.id
var resultInfoDiv = document.createElement("div") var resultInfoDiv = document.createElement("div")
resultInfoDiv.classList.add("song-search-result-info") resultInfoDiv.classList.add("song-search-result-info")
var resultInfoTitle = document.createElement("span") var resultInfoTitle = document.createElement("span")
resultInfoTitle.classList.add("song-search-result-title") resultInfoTitle.classList.add("song-search-result-title")
resultInfoTitle.appendChild(this.highlightResult(title, result[0])) resultInfoTitle.appendChild(this.highlightResult(title, result[0]))
resultInfoTitle.setAttribute("alt", title) resultInfoTitle.setAttribute("alt", title)
resultInfoDiv.appendChild(resultInfoTitle) resultInfoDiv.appendChild(resultInfoTitle)
if(subtitle){ if (subtitle) {
resultInfoDiv.appendChild(document.createElement("br")) resultInfoDiv.appendChild(document.createElement("br"))
var resultInfoSubtitle = document.createElement("span") var resultInfoSubtitle = document.createElement("span")
resultInfoSubtitle.classList.add("song-search-result-subtitle") resultInfoSubtitle.classList.add("song-search-result-subtitle")
resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1])) resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1]))
resultInfoSubtitle.setAttribute("alt", subtitle) resultInfoSubtitle.setAttribute("alt", subtitle)
resultInfoDiv.appendChild(resultInfoSubtitle) resultInfoDiv.appendChild(resultInfoSubtitle)
} }
resultDiv.appendChild(resultInfoDiv) resultDiv.appendChild(resultInfoDiv)
var courses = ["easy", "normal", "hard", "oni", "ura"] var courses = ["easy", "normal", "hard", "oni", "ura"]
courses.forEach(course => { courses.forEach(course => {
var courseDiv = document.createElement("div") var courseDiv = document.createElement("div")
@@ -330,40 +330,41 @@ class Search{
var courseStars = document.createElement("div") var courseStars = document.createElement("div")
courseStars.classList.add("song-search-result-stars") courseStars.classList.add("song-search-result-stars")
courseStars.innerText = song.courses[course].stars + "\u2605" courseStars.innerText = song.courses[course].stars + "\u2605"
courseDiv.appendChild(courseCrown) courseDiv.appendChild(courseCrown)
courseDiv.appendChild(courseStars) courseDiv.appendChild(courseStars)
} else { } else {
courseDiv.classList.add("song-search-result-hidden") courseDiv.classList.add("song-search-result-hidden")
} }
resultDiv.appendChild(courseDiv) resultDiv.appendChild(courseDiv)
}) })
this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font
var titleWidth = this.songSelect.ctx.measureText(title).width var titleWidth = this.songSelect.ctx.measureText(title).width
var titleRatio = resultWidth / titleWidth var titleRatio = resultWidth / titleWidth
if(titleRatio < 1){ if (titleRatio < 1) {
resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)" resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)"
} }
if(subtitle){ if (subtitle) {
this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font
var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width
var subtitleRatio = resultWidth / subtitleWidth var subtitleRatio = resultWidth / subtitleWidth
if(subtitleRatio < 1){ if (subtitleRatio < 1) {
resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)" resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)"
} }
} }
return resultDiv return resultDiv
} }
highlightResult(text, result){ highlightResult(text, result) {
if (text === null || text === undefined) return document.createDocumentFragment();
var fragment = document.createDocumentFragment() var fragment = document.createDocumentFragment()
var ranges = (result ? result.ranges : null) || [] var ranges = (result ? result.ranges : null) || []
var lastIdx = 0 var lastIdx = 0
ranges.forEach(range => { ranges.forEach(range => {
if(lastIdx !== range[0]){ if (lastIdx !== range[0]) {
fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0]))) fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0])))
} }
var span = document.createElement("span") var span = document.createElement("span")
@@ -372,91 +373,91 @@ class Search{
fragment.appendChild(span) fragment.appendChild(span)
lastIdx = range[1] + 1 lastIdx = range[1] + 1
}) })
if(text.length !== lastIdx){ if (text.length !== lastIdx) {
fragment.appendChild(document.createTextNode(text.slice(lastIdx))) fragment.appendChild(document.createTextNode(text.slice(lastIdx)))
} }
return fragment return fragment
} }
setActive(idx){ setActive(idx) {
this.songSelect.playSound("se_ka") this.songSelect.playSound("se_ka")
var active = this.div.querySelector(":scope .song-search-result-active") var active = this.div.querySelector(":scope .song-search-result-active")
if(active){ if (active) {
active.classList.remove("song-search-result-active") active.classList.remove("song-search-result-active")
} }
if(idx === null){ if (idx === null) {
this.active = null this.active = null
return return
} }
var el = this.results[idx] var el = this.results[idx]
this.input.blur() this.input.blur()
el.classList.add("song-search-result-active") el.classList.add("song-search-result-active")
this.scrollTo(el) this.scrollTo(el)
this.active = idx this.active = idx
} }
display(fromButton=false){ display(fromButton = false) {
if(!this.enabled){ if (!this.enabled) {
return return
} }
if(this.opened){ if (this.opened) {
return this.remove(true) return this.remove(true)
} }
this.opened = true this.opened = true
this.results = [] this.results = []
this.div = document.createElement("div") this.div = document.createElement("div")
this.div.innerHTML = assets.pages["search"] this.div.innerHTML = assets.pages["search"]
this.container = this.div.querySelector(":scope #song-search-container") this.container = this.div.querySelector(":scope #song-search-container")
if(this.touchEnabled){ if (this.touchEnabled) {
this.container.classList.add("touch-enabled") this.container.classList.add("touch-enabled")
} }
pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this)) pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this))
this.input = this.div.querySelector(":scope #song-search-input") this.input = this.div.querySelector(":scope #song-search-input")
this.input.setAttribute("placeholder", strings.search.searchInput) this.input.setAttribute("placeholder", strings.search.searchInput)
pageEvents.add(this.input, ["input"], () => this.onInput()) pageEvents.add(this.input, ["input"], () => this.onInput())
this.songSelect.playSound("se_pause") this.songSelect.playSound("se_pause")
loader.screen.appendChild(this.div) loader.screen.appendChild(this.div)
this.setTip() this.setTip()
cancelTouch = false cancelTouch = false
noResizeRoot = true noResizeRoot = true
if(this.songSelect.songs[this.songSelect.selectedSong].courses){ if (this.songSelect.songs[this.songSelect.selectedSong].courses) {
snd.previewGain.setVolumeMul(0.5) snd.previewGain.setVolumeMul(0.5)
}else if(this.songSelect.bgmEnabled){ } else if (this.songSelect.bgmEnabled) {
snd.musicGain.setVolumeMul(0.5) snd.musicGain.setVolumeMul(0.5)
} }
setTimeout(() => { setTimeout(() => {
this.input.focus() this.input.focus()
this.input.setSelectionRange(0, this.input.value.length) this.input.setSelectionRange(0, this.input.value.length)
}, 10) }, 10)
var lastQuery = localStorage.getItem("lastSearchQuery") var lastQuery = localStorage.getItem("lastSearchQuery")
if(lastQuery){ if (lastQuery) {
this.input.value = lastQuery this.input.value = lastQuery
this.input.dispatchEvent(new Event("input", { this.input.dispatchEvent(new Event("input", {
value: lastQuery value: lastQuery
})) }))
} }
} }
remove(byUser=false){ remove(byUser = false) {
if(this.opened){ if (this.opened) {
this.opened = false this.opened = false
if(byUser){ if (byUser) {
this.songSelect.playSound("se_cancel") this.songSelect.playSound("se_cancel")
} }
pageEvents.remove(this.div.querySelector(":scope #song-search-container"), pageEvents.remove(this.div.querySelector(":scope #song-search-container"),
["mousedown", "touchstart"]) ["mousedown", "touchstart"])
pageEvents.remove(this.input, ["input"]) pageEvents.remove(this.input, ["input"])
this.div.remove() this.div.remove()
delete this.results delete this.results
delete this.div delete this.div
@@ -465,39 +466,39 @@ class Search{
delete this.active delete this.active
cancelTouch = true cancelTouch = true
noResizeRoot = false noResizeRoot = false
if(this.songSelect.songs[this.songSelect.selectedSong].courses){ if (this.songSelect.songs[this.songSelect.selectedSong].courses) {
snd.previewGain.setVolumeMul(1) snd.previewGain.setVolumeMul(1)
}else if(this.songSelect.bgmEnabled){ } else if (this.songSelect.bgmEnabled) {
snd.musicGain.setVolumeMul(1) snd.musicGain.setVolumeMul(1)
} }
} }
} }
setTip(tip, error=false){ setTip(tip, error = false) {
if(this.tip){ if (this.tip) {
this.tip.remove() this.tip.remove()
delete this.tip delete this.tip
} }
if(!tip){ if (!tip) {
tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)] tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)]
} }
var resultsDiv = this.div.querySelector(":scope #song-search-results") var resultsDiv = this.div.querySelector(":scope #song-search-results")
resultsDiv.innerHTML = "" resultsDiv.innerHTML = ""
this.results = [] this.results = []
this.tip = document.createElement("div") this.tip = document.createElement("div")
this.tip.id = "song-search-tip" this.tip.id = "song-search-tip"
this.tip.innerText = tip this.tip.innerText = tip
this.div.querySelector(":scope #song-search").appendChild(this.tip) this.div.querySelector(":scope #song-search").appendChild(this.tip)
if(error){ if (error) {
this.tip.classList.add("song-search-tip-error") this.tip.classList.add("song-search-tip-error")
} }
} }
proceed(songId){ proceed(songId) {
if (/^-?\d+$/.test(songId)) { if (/^-?\d+$/.test(songId)) {
songId = parseInt(songId) songId = parseInt(songId)
} }
@@ -505,91 +506,91 @@ class Search{
var song = this.songSelect.songs.find(song => song.id === songId) var song = this.songSelect.songs.find(song => song.id === songId)
this.remove() this.remove()
this.songSelect.playBgm(false) this.songSelect.playBgm(false)
if(this.songSelect.previewing === "muted"){ if (this.songSelect.previewing === "muted") {
this.songSelect.previewing = null this.songSelect.previewing = null
} }
var songIndex = this.songSelect.songs.findIndex(song => song.id === songId) var songIndex = this.songSelect.songs.findIndex(song => song.id === songId)
this.songSelect.setSelectedSong(songIndex) this.songSelect.setSelectedSong(songIndex)
this.songSelect.toSelectDifficulty() this.songSelect.toSelectDifficulty()
} }
scrollTo(element){ scrollTo(element) {
var parentNode = element.parentNode var parentNode = element.parentNode
var selected = element.getBoundingClientRect() var selected = element.getBoundingClientRect()
var parent = parentNode.getBoundingClientRect() var parent = parentNode.getBoundingClientRect()
var scrollY = parentNode.scrollTop var scrollY = parentNode.scrollTop
var selectedPosTop = selected.top - selected.height / 2 var selectedPosTop = selected.top - selected.height / 2
if(Math.floor(selectedPosTop) < Math.floor(parent.top)){ if (Math.floor(selectedPosTop) < Math.floor(parent.top)) {
parentNode.scrollTop += selectedPosTop - parent.top parentNode.scrollTop += selectedPosTop - parent.top
}else{ } else {
var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top
if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){ if (Math.floor(selectedPosBottom) > Math.floor(parent.height)) {
parentNode.scrollTop += selectedPosBottom - parent.height parentNode.scrollTop += selectedPosBottom - parent.height
} }
} }
} }
parseRange(string){ parseRange(string) {
var range = string.split("-") var range = string.split("-")
if(range.length == 1){ if (range.length == 1) {
var min = parseInt(range[0]) || 0 var min = parseInt(range[0]) || 0
return min > 0 ? {min: min, max: min} : false return min > 0 ? { min: min, max: min } : false
} else if(range.length == 2){ } else if (range.length == 2) {
var min = parseInt(range[0]) || 0 var min = parseInt(range[0]) || 0
var max = parseInt(range[1]) || 0 var max = parseInt(range[1]) || 0
return min > 0 && max > 0 ? {min: min, max: max} : false return min > 0 && max > 0 ? { min: min, max: max } : false
} }
} }
indexesToRanges(indexes){ indexesToRanges(indexes) {
var ranges = [] var ranges = []
var range var range
indexes.forEach(idx => { indexes.forEach(idx => {
if(range && range[1] === idx - 1){ if (range && range[1] === idx - 1) {
range[1] = idx range[1] = idx
}else{ } else {
range = [idx, idx] range = [idx, idx]
ranges.push(range) ranges.push(range)
} }
}) })
return ranges return ranges
} }
onInput(resize){ onInput(resize) {
var text = this.input.value var text = this.input.value
localStorage.setItem("lastSearchQuery", text) localStorage.setItem("lastSearchQuery", text)
text = text.toLowerCase() text = text.toLowerCase()
if(text.length === 0){ if (text.length === 0) {
if(!resize){ if (!resize) {
this.setTip() this.setTip()
} }
return return
} }
var new_results = this.perform(text) var new_results = this.perform(text)
if(new_results.length === 0){ if (new_results.length === 0) {
this.setTip(strings.search.noResults, true) this.setTip(strings.search.noResults, true)
return return
}else if(this.tip){ } else if (this.tip) {
this.tip.remove() this.tip.remove()
delete this.tip delete this.tip
} }
var resultsDiv = this.div.querySelector(":scope #song-search-results") var resultsDiv = this.div.querySelector(":scope #song-search-results")
resultsDiv.innerHTML = "" resultsDiv.innerHTML = ""
this.results = [] this.results = []
var fontSize = parseFloat(getComputedStyle(this.div.querySelector(":scope #song-search")).fontSize.slice(0, -2)) var fontSize = parseFloat(getComputedStyle(this.div.querySelector(":scope #song-search")).fontSize.slice(0, -2))
var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2)) var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2))
var vmin = Math.min(innerWidth, lastHeight) / 100 var vmin = Math.min(innerWidth, lastHeight) / 100
var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin) var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin)
var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize
this.songSelect.ctx.save() this.songSelect.ctx.save()
var fragment = document.createDocumentFragment() var fragment = document.createDocumentFragment()
new_results.forEach(result => { new_results.forEach(result => {
var result = this.createResult(result, resultWidth, fontSize) var result = this.createResult(result, resultWidth, fontSize)
@@ -597,78 +598,78 @@ class Search{
this.results.push(result) this.results.push(result)
}) })
resultsDiv.appendChild(fragment) resultsDiv.appendChild(fragment)
this.songSelect.ctx.restore() this.songSelect.ctx.restore()
} }
onClick(e){ onClick(e) {
if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){ if ((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1) {
this.remove(true) this.remove(true)
}else if(e.which === 1){ } else if (e.which === 1) {
var songEl = e.target.closest(".song-search-result") var songEl = e.target.closest(".song-search-result")
if(songEl){ if (songEl) {
var songId = songEl.dataset.songId var songId = songEl.dataset.songId
this.proceed(songId) this.proceed(songId)
} }
} }
} }
keyPress(pressed, name, event, repeat, ctrl){ keyPress(pressed, name, event, repeat, ctrl) {
if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { if (name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) {
this.remove(true) this.remove(true)
if(event){ if (event) {
event.preventDefault() event.preventDefault()
} }
}else if(name === "down" && this.results.length){ } else if (name === "down" && this.results.length) {
if(this.input == document.activeElement && this.results){ if (this.input == document.activeElement && this.results) {
this.setActive(0) this.setActive(0)
}else if(this.active === this.results.length - 1){ } else if (this.active === this.results.length - 1) {
this.setActive(null) this.setActive(null)
this.input.focus() this.input.focus()
}else if(Number.isInteger(this.active)){ } else if (Number.isInteger(this.active)) {
this.setActive(this.active + 1) this.setActive(this.active + 1)
}else{ } else {
this.setActive(0) this.setActive(0)
} }
}else if(name === "up" && this.results.length){ } else if (name === "up" && this.results.length) {
if(this.input == document.activeElement && this.results){ if (this.input == document.activeElement && this.results) {
this.setActive(this.results.length - 1) this.setActive(this.results.length - 1)
}else if(this.active === 0){ } else if (this.active === 0) {
this.setActive(null) this.setActive(null)
this.input.focus() this.input.focus()
setTimeout(() => { setTimeout(() => {
this.input.setSelectionRange(this.input.value.length, this.input.value.length) this.input.setSelectionRange(this.input.value.length, this.input.value.length)
}, 0) }, 0)
}else if(Number.isInteger(this.active)){ } else if (Number.isInteger(this.active)) {
this.setActive(this.active - 1) this.setActive(this.active - 1)
}else{ } else {
this.setActive(this.results.length - 1) this.setActive(this.results.length - 1)
} }
}else if(name === "confirm"){ } else if (name === "confirm") {
if(Number.isInteger(this.active)){ if (Number.isInteger(this.active)) {
this.proceed(this.results[this.active].dataset.songId) this.proceed(this.results[this.active].dataset.songId)
}else{ } else {
this.onInput() this.onInput()
if(event.keyCode === 13 && this.songSelect.touchEnabled){ if (event.keyCode === 13 && this.songSelect.touchEnabled) {
this.input.blur() this.input.blur()
} }
} }
} }
} }
redraw(){ redraw() {
if(this.opened && this.container){ if (this.opened && this.container) {
var vmin = Math.min(innerWidth, lastHeight) / 100 var vmin = Math.min(innerWidth, lastHeight) / 100
if(this.vmin !== vmin){ if (this.vmin !== vmin) {
this.container.style.setProperty("--vmin", vmin + "px") this.container.style.setProperty("--vmin", vmin + "px")
this.vmin = vmin this.vmin = vmin
} }
}else{ } else {
this.vmin = null this.vmin = null
} }
} }
clean(){ clean() {
loader.screen.removeChild(this.style) loader.screen.removeChild(this.style)
fuzzysort.cleanup() fuzzysort.cleanup()
delete this.container delete this.container

View File

@@ -3335,9 +3335,14 @@ class SongSelect {
}); });
} }
toLeaderboard() { toLeaderboard() {
if (!this.songs[this.selectedSong].id) { var song = this.songs[this.selectedSong]
var songId = song.id || song.hash
// Allow leaderboard for any song with an ID (numeric or hash)
if (!songId) {
return return
} }
// Default to first available difficulty if not in a valid difficulty selection // Default to first available difficulty if not in a valid difficulty selection
var selectedDiff = this.selectedDiff - this.diffOptions.length var selectedDiff = this.selectedDiff - this.diffOptions.length
if (selectedDiff < 0) { if (selectedDiff < 0) {
@@ -3347,6 +3352,7 @@ class SongSelect {
this.clean() this.clean()
this.playSound("se_don") this.playSound("se_don")
new Leaderboard().display(this.songs[this.selectedSong].id, diffId) new Leaderboard().display(songId, diffId)
} }
} }

133
reproduce_issue.py Normal file
View File

@@ -0,0 +1,133 @@
import requests
import json
import base64
BASE_URL = "http://localhost:34801"
def register_user(username, password):
url = f"{BASE_URL}/api/register"
data = {"username": username, "password": password}
try:
response = requests.post(url, json=data)
if response.status_code == 200:
return response.json()
print(f"Register failed: {response.text}")
except Exception as e:
print(f"Register error: {e}")
return None
def login_user(username, password):
url = f"{BASE_URL}/api/login"
data = {"username": username, "password": password}
session = requests.Session()
try:
response = session.post(url, json=data)
if response.status_code == 200:
return session
print(f"Login failed: {response.text}")
except Exception as e:
print(f"Login error: {e}")
return None
def get_csrf_token(session):
url = f"{BASE_URL}/api/csrftoken"
response = session.get(url)
if response.status_code == 200:
return response.json()['token']
return None
def submit_score(session, song_id, difficulty, score_obj, csrf_token):
url = f"{BASE_URL}/api/leaderboard/submit"
headers = {"X-CSRFToken": csrf_token}
data = {
"song_id": song_id,
"difficulty": difficulty,
"score": score_obj
}
response = session.post(url, json=data, headers=headers)
return response.json()
def get_leaderboard(session, song_id, difficulty):
url = f"{BASE_URL}/api/leaderboard/get"
params = {"song_id": song_id, "difficulty": difficulty}
response = session.get(url, params=params)
return response.json()
def main():
username = "debug_user_1"
password = "password123"
print("1. Registering/Logging in...")
# Try login first
session = login_user(username, password)
if not session:
# Register
reg = register_user(username, password)
if reg:
session = login_user(username, password)
else:
print("Could not register or login")
return
print("2. Getting CSRF token...")
csrf_token = get_csrf_token(session)
if not csrf_token:
print("Could not get CSRF token")
return
song_id = 1 # Assuming song 1 exists
difficulty = "oni"
# Simulate scoresheet.js submission (points only)
score_obj = {
"points": 123456,
"good": 100,
"ok": 10,
"bad": 0,
"maxCombo": 110,
"drumroll": 5,
"crown": "gold"
}
print(f"3. Submitting score: {score_obj['points']}")
submit_res = submit_score(session, song_id, difficulty, score_obj, csrf_token)
print(f"Submit response: {submit_res}")
print("4. Fetching leaderboard...")
leaderboard_res = get_leaderboard(session, song_id, difficulty)
if leaderboard_res['status'] == 'ok':
leaderboard = leaderboard_res['leaderboard']
found = False
for entry in leaderboard:
if entry['username'] == username:
found = True
print(f"Found entry: {entry}")
print(f"entry.score_value: {entry.get('score_value')}")
print(f"entry.score: {entry.get('score')}")
score_value = entry.get('score_value')
score_data = entry.get('score')
# Check what leaderboard.js would see
if score_data and 'score' in score_data:
print(f"JS would assume score: {score_data['score']}")
else:
print("JS would assume score: 0 (undefined)")
if score_value == 123456:
print("SUCCESS: score_value is correct.")
else:
print(f"FAILURE: score_value is incorrect (expected 123456, got {score_value})")
if score_data and score_data.get('points') == 123456:
print("SUCCESS: points preserved in score object.")
if not found:
print("Entry not found in leaderboard (maybe not top 50?)")
else:
print(f"Failed to get leaderboard: {leaderboard_res}")
if __name__ == "__main__":
main()

View File

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

View File

@@ -8,7 +8,7 @@ CODENAME=${VERSION_CODENAME:-}
VERSION=${VERSION_ID:-} VERSION=${VERSION_ID:-}
echo "更新系统软件源..." echo "更新系统软件源..."
apt-get update -y apt-get update -y || true
echo "安装基础依赖..." echo "安装基础依赖..."
apt-get install -y python3 python3-venv python3-pip git ffmpeg rsync curl gnupg libcap2-bin apt-get install -y python3 python3-venv python3-pip git ffmpeg rsync curl gnupg libcap2-bin

32
test_frontend_logic.js Normal file
View File

@@ -0,0 +1,32 @@
// Simulation of song objects
const songs = [
{ name: "Server Song", id: 123, hash: "hash123" },
{ name: "Custom Song (No ID)", id: undefined, hash: "hashCustom" },
{ name: "Custom Song (Null ID)", id: null, hash: "hashNull" },
{ name: "Edge Case (ID 0)", id: 0, hash: "hashZero" } // Checking if behavior is acceptable
];
console.log("=== Frontend Logic Test: Song ID Fallback ===");
songs.forEach(song => {
// Logic from scoresheet.js / songselect.js
const songId = song.id || song.hash;
console.log(`Song: ${song.name}`);
console.log(` Raw ID: ${song.id}, Hash: ${song.hash}`);
console.log(` Resolved songId: ${songId}`);
if (song.id && songId === song.id) {
console.log(" ✅ Correctly used ID");
} else if (!song.id && songId === song.hash) {
console.log(" ✅ Correctly fell back to Hash");
} else if (song.id === 0 && songId === song.hash) {
console.log(" ⚠️ ID was 0, used Hash. (Acceptable if ID > 0 always)");
} else {
console.log(" ❌ FAILED logic mismatch");
}
console.log("---");
});
console.log("Tests Completed.");