Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4895990729 | |||
| 6686f5ae15 | |||
| 8d58bf683f |
@@ -0,0 +1,20 @@
|
||||
I will optimize the loading process by implementing a **Multi-Threaded Worker Loader**. This involves creating a pool of Web Workers to fetch assets (JavaScript, Audio, Images, Views) in parallel, offloading the network initiation and handling from the main thread.
|
||||
|
||||
### Plan:
|
||||
1. **Create `public/src/js/loader-worker.js`**:
|
||||
* This worker will handle `fetch` requests for different resource types (`text`, `blob`, `arraybuffer`).
|
||||
* It will transfer the data back to the main thread (using zero-copy transfer for `ArrayBuffer`).
|
||||
|
||||
2. **Modify `public/src/js/loader.js`**:
|
||||
* **Initialize Worker Pool**: Create a pool of workers (defaulting to 4) in the `Loader` class.
|
||||
* **Implement `workerFetch(url, type)`**: A method to distribute fetch tasks to the worker pool.
|
||||
* **Override `ajax(url, ...)`**: Intercept requests for static assets (`src/`, `assets/`, etc.) and route them through `workerFetch`. Keep API calls (`api/`) on the main thread to ensure session stability.
|
||||
* **Update `loadScript(url)`**: Change it to fetch the script content via `workerFetch` and inject it using a `<script>` tag with inline content. This ensures JS files are also loaded via the "multi-process" mechanism.
|
||||
* **Update `loadSound` and `RemoteFile` logic**: Since `RemoteFile` uses `loader.ajax`, routing `ajax` to workers will automatically parallelize audio loading.
|
||||
|
||||
### Technical Details:
|
||||
* **Concurrency**: 4 Workers will be used to maximize throughput without overloading the browser's connection limit per domain.
|
||||
* **Resource Types**:
|
||||
* **JS/Views**: Fetched as `text`.
|
||||
* **Images**: Fetched as `blob` -> `URL.createObjectURL`.
|
||||
* **Audio**: Fetched as `arraybuffer` -> `AudioContext.decodeAudioData`.
|
||||
221
app.py
221
app.py
@@ -13,6 +13,7 @@ import requests
|
||||
import schema
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# -- カスタム --
|
||||
import traceback
|
||||
@@ -105,9 +106,13 @@ db.users.create_index('username', unique=True)
|
||||
db.songs.create_index('id', unique=True)
|
||||
db.songs.create_index('song_type')
|
||||
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')
|
||||
db.leaderboard.create_index([
|
||||
('song_id', 1),
|
||||
('difficulty', 1),
|
||||
('month', 1),
|
||||
('score', -1)
|
||||
])
|
||||
db.leaderboard.create_index('username')
|
||||
|
||||
|
||||
class HashException(Exception):
|
||||
@@ -758,156 +763,122 @@ def route_api_leaderboard_submit():
|
||||
|
||||
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')
|
||||
current_month = datetime.now().strftime('%Y-%m')
|
||||
|
||||
# Validate difficulty
|
||||
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
|
||||
if difficulty not in valid_difficulties:
|
||||
return api_error('invalid_difficulty')
|
||||
# Handle song_id type
|
||||
song_id = data['song_id']
|
||||
|
||||
# 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({
|
||||
query = {
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'username': username,
|
||||
'month': current_month
|
||||
})
|
||||
'difficulty': data['difficulty'],
|
||||
'month': current_month,
|
||||
'username': username
|
||||
}
|
||||
|
||||
# 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
|
||||
existing = db.leaderboard.find_one(query)
|
||||
|
||||
# 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')
|
||||
new_record = False
|
||||
rank_info = None
|
||||
|
||||
score_value = int(score_val or 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({
|
||||
if not existing or data['score'] > existing['score']:
|
||||
entry = {
|
||||
'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,
|
||||
'difficulty': data['difficulty'],
|
||||
'month': current_month,
|
||||
'username': username,
|
||||
'display_name': user['display_name'],
|
||||
'score': score_data,
|
||||
'score_value': score_value,
|
||||
'submitted_at': time.time(),
|
||||
'month': current_month
|
||||
'don': get_db_don(user),
|
||||
'score': data['score'],
|
||||
'good': data.get('good', 0),
|
||||
'ok': data.get('ok', 0),
|
||||
'bad': data.get('bad', 0),
|
||||
'maxCombo': data.get('maxCombo', 0),
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
db.leaderboard.update_one(query, {'$set': entry}, upsert=True)
|
||||
new_record = True
|
||||
|
||||
# Get rank
|
||||
count = db.leaderboard.count_documents({
|
||||
'song_id': song_id,
|
||||
'difficulty': data['difficulty'],
|
||||
'month': current_month,
|
||||
'score': {'$gt': data['score']}
|
||||
})
|
||||
rank = count + 1
|
||||
rank_info = rank
|
||||
|
||||
# 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))
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'new_record': new_record,
|
||||
'rank': rank_info
|
||||
})
|
||||
|
||||
# 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)
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
|
||||
if not song_id or not difficulty:
|
||||
return abort(400)
|
||||
|
||||
# Accept both numeric IDs and hash strings
|
||||
# Try to convert to int if possible, otherwise use as string
|
||||
try:
|
||||
song_id = int(song_id)
|
||||
except (ValueError, TypeError):
|
||||
# Keep as string (hash ID)
|
||||
pass
|
||||
# Try convert song_id to int if it looks like one
|
||||
if re.match('^[0-9]+$', str(song_id)):
|
||||
song_id = int(song_id)
|
||||
|
||||
# Validate difficulty
|
||||
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
|
||||
if difficulty not in valid_difficulties:
|
||||
return abort(400)
|
||||
current_month = datetime.now().strftime('%Y-%m')
|
||||
|
||||
# Get current month
|
||||
current_month = time.strftime('%Y-%m', time.gmtime())
|
||||
|
||||
# Get top 50 scores
|
||||
leaderboard = list(db.leaderboards.find({
|
||||
query = {
|
||||
'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
|
||||
# Get top 50
|
||||
leaderboard = list(db.leaderboard.find(query, {'_id': False}).sort('score', -1).limit(50))
|
||||
|
||||
return jsonify({'status': 'ok', 'leaderboard': leaderboard, 'month': current_month})
|
||||
user_rank = None
|
||||
if session.get('username'):
|
||||
username = session.get('username')
|
||||
user_entry = db.leaderboard.find_one({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'month': current_month,
|
||||
'username': username
|
||||
}, {'_id': False})
|
||||
|
||||
if user_entry:
|
||||
# Calculate rank
|
||||
count = db.leaderboard.count_documents({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'month': current_month,
|
||||
'score': {'$gt': user_entry['score']}
|
||||
})
|
||||
user_rank = {
|
||||
'rank': count + 1,
|
||||
'entry': user_entry
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'leaderboard': leaderboard,
|
||||
'user_rank': user_rank
|
||||
})
|
||||
|
||||
|
||||
@app.route(basedir + 'api/leaderboard/reset', methods=['POST'])
|
||||
@admin_required(level=100)
|
||||
def route_api_leaderboard_reset():
|
||||
current_month = datetime.now().strftime('%Y-%m')
|
||||
result = db.leaderboard.delete_many({'month': {'$ne': current_month}})
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'deleted_count': result.deleted_count,
|
||||
'current_month': current_month
|
||||
})
|
||||
|
||||
|
||||
@app.route(basedir + 'privacy')
|
||||
|
||||
201
local_debug.py
201
local_debug.py
@@ -1,201 +0,0 @@
|
||||
|
||||
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("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
||||
@@ -1,13 +0,0 @@
|
||||
#leaderboard {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#leaderboard-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -6,8 +6,8 @@ var assets = {
|
||||
"parseosu.js",
|
||||
"titlescreen.js",
|
||||
"scoresheet.js",
|
||||
"leaderboard_ui.js",
|
||||
"songselect.js",
|
||||
"leaderboard.js",
|
||||
"keyboard.js",
|
||||
"gameinput.js",
|
||||
"game.js",
|
||||
@@ -141,7 +141,6 @@ var assets = {
|
||||
},
|
||||
"views": [
|
||||
"game.html",
|
||||
"leaderboard.html",
|
||||
"loadsong.html",
|
||||
"songselect.html",
|
||||
"titlescreen.html",
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
class Leaderboard {
|
||||
constructor() {
|
||||
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() {
|
||||
this.canvas = document.getElementById("leaderboard-canvas")
|
||||
if (!this.canvas) {
|
||||
console.error("Leaderboard canvas not found!")
|
||||
return false
|
||||
}
|
||||
this.ctx = this.canvas.getContext("2d")
|
||||
|
||||
var resolution = settings.getItem("resolution")
|
||||
var noSmoothing = resolution === "low" || resolution === "lowest"
|
||||
if (noSmoothing) {
|
||||
this.ctx.imageSmoothingEnabled = 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))
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Initialize after page is loaded
|
||||
if (!this.init()) {
|
||||
console.error("Failed to initialize leaderboard")
|
||||
return
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// Validate songId exists
|
||||
if (!this.songId) {
|
||||
console.error("Missing song ID for leaderboard")
|
||||
this.leaderboardData = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await loader.ajax(
|
||||
`${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${encodeURIComponent(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
|
||||
// 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.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"])
|
||||
}
|
||||
}
|
||||
286
public/src/js/leaderboard_ui.js
Normal file
286
public/src/js/leaderboard_ui.js
Normal file
@@ -0,0 +1,286 @@
|
||||
class LeaderboardUI {
|
||||
constructor(songSelect) {
|
||||
this.songSelect = songSelect
|
||||
this.canvasCache = new CanvasCache()
|
||||
this.nameplateCache = new CanvasCache()
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.leaderboardData = []
|
||||
this.userRank = null
|
||||
this.scroll = 0
|
||||
this.maxScroll = 0
|
||||
this.songId = null
|
||||
this.difficulty = null
|
||||
this.font = strings.font
|
||||
this.numbersFont = "TnT, Meiryo, sans-serif"
|
||||
|
||||
// UI Constants
|
||||
this.width = 800
|
||||
this.height = 500
|
||||
this.itemHeight = 60
|
||||
this.headerHeight = 80
|
||||
this.padding = 20
|
||||
|
||||
this.closeBtn = {
|
||||
x: 0, y: 0, w: 60, h: 60
|
||||
}
|
||||
}
|
||||
|
||||
show(songId, difficulty) {
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
this.songId = songId
|
||||
this.difficulty = difficulty
|
||||
this.leaderboardData = []
|
||||
this.userRank = null
|
||||
this.scroll = 0
|
||||
|
||||
this.fetchData(songId, difficulty)
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visible = false
|
||||
this.songSelect.leaderboardActive = false
|
||||
}
|
||||
|
||||
fetchData(songId, difficulty) {
|
||||
loader.ajax(gameConfig.basedir + `api/leaderboard/get?song_id=${songId}&difficulty=${difficulty}`).then(response => {
|
||||
const data = JSON.parse(response)
|
||||
if (data.status === 'ok') {
|
||||
this.leaderboardData = data.leaderboard
|
||||
this.userRank = data.userRank
|
||||
this.loading = false
|
||||
|
||||
// Calculate max scroll
|
||||
const totalHeight = this.leaderboardData.length * this.itemHeight
|
||||
const viewHeight = this.height - this.headerHeight - this.padding * 2
|
||||
this.maxScroll = Math.max(0, totalHeight - viewHeight)
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error("Leaderboard fetch error:", e)
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
draw(ctx, winW, winH, pixelRatio) {
|
||||
if (!this.visible) return
|
||||
|
||||
const x = (winW - this.width) / 2
|
||||
const y = (winH - this.height) / 2
|
||||
|
||||
// Draw Overlay
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.7)"
|
||||
ctx.fillRect(0, 0, winW, winH)
|
||||
|
||||
// Draw Window
|
||||
this.songSelect.draw.roundedRect({
|
||||
ctx: ctx,
|
||||
x: x,
|
||||
y: y,
|
||||
w: this.width,
|
||||
h: this.height,
|
||||
radius: 20
|
||||
})
|
||||
ctx.fillStyle = "#fff"
|
||||
ctx.fill()
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = "#ff6600"
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + 20, y)
|
||||
ctx.lineTo(x + this.width - 20, y)
|
||||
ctx.quadraticCurveTo(x + this.width, y, x + this.width, y + 20)
|
||||
ctx.lineTo(x + this.width, y + this.headerHeight)
|
||||
ctx.lineTo(x, y + this.headerHeight)
|
||||
ctx.lineTo(x, y + 20)
|
||||
ctx.quadraticCurveTo(x, y, x + 20, y)
|
||||
ctx.fill()
|
||||
|
||||
// Title
|
||||
const diffText = strings[this.difficulty === "ura" ? "oni" : this.difficulty]
|
||||
const titleText = strings.leaderboardTitle.replace("%s", diffText)
|
||||
|
||||
this.songSelect.draw.layeredText({
|
||||
ctx: ctx,
|
||||
text: titleText,
|
||||
fontSize: 40,
|
||||
fontFamily: this.font,
|
||||
x: x + this.width / 2,
|
||||
y: y + 54,
|
||||
align: "center",
|
||||
width: this.width - 100
|
||||
}, [
|
||||
{ outline: "#fff", letterBorder: 2 },
|
||||
{ fill: "#000" }
|
||||
])
|
||||
|
||||
// Close Button
|
||||
this.closeBtn.x = x + this.width - 50
|
||||
this.closeBtn.y = y - 10
|
||||
ctx.fillStyle = "#ff0000"
|
||||
ctx.beginPath()
|
||||
ctx.arc(this.closeBtn.x, this.closeBtn.y, 20, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = "#fff"
|
||||
ctx.lineWidth = 3
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = "#fff"
|
||||
ctx.font = "bold 24px Arial"
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "middle"
|
||||
ctx.fillText("X", this.closeBtn.x, this.closeBtn.y)
|
||||
|
||||
// Content Area
|
||||
const contentX = x + this.padding
|
||||
const contentY = y + this.headerHeight + this.padding
|
||||
const contentW = this.width - this.padding * 2
|
||||
const contentH = this.height - this.headerHeight - this.padding * 2
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.rect(contentX, contentY, contentW, contentH)
|
||||
ctx.clip()
|
||||
|
||||
if (this.loading) {
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.font = `30px ${this.font}`
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText(strings.loadingLeaderboard, x + this.width / 2, y + this.height / 2)
|
||||
} else if (this.leaderboardData.length === 0) {
|
||||
ctx.fillStyle = "#666"
|
||||
ctx.font = `30px ${this.font}`
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText(strings.noScores, x + this.width / 2, y + this.height / 2)
|
||||
} else {
|
||||
// Draw List
|
||||
let currentY = contentY - this.scroll
|
||||
|
||||
// Header Row
|
||||
/*
|
||||
ctx.fillStyle = "#eee"
|
||||
ctx.fillRect(contentX, contentY, contentW, 40)
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.font = `bold 20px ${this.font}`
|
||||
ctx.textAlign = "left"
|
||||
ctx.fillText(strings.rank, contentX + 20, contentY + 28)
|
||||
ctx.fillText(strings.playerName, contentX + 100, contentY + 28)
|
||||
ctx.textAlign = "right"
|
||||
ctx.fillText(strings.score, contentX + contentW - 20, contentY + 28)
|
||||
currentY += 40
|
||||
*/
|
||||
|
||||
this.leaderboardData.forEach((entry, index) => {
|
||||
if (currentY + this.itemHeight < contentY) {
|
||||
currentY += this.itemHeight
|
||||
return
|
||||
}
|
||||
if (currentY > contentY + contentH) return
|
||||
|
||||
const isUser = this.userRank && this.userRank.entry.username === entry.username
|
||||
|
||||
// Background
|
||||
if (index % 2 === 0) {
|
||||
ctx.fillStyle = isUser ? "#fff8e1" : "#f9f9f9"
|
||||
} else {
|
||||
ctx.fillStyle = isUser ? "#fff3cd" : "#fff"
|
||||
}
|
||||
ctx.fillRect(contentX, currentY, contentW, this.itemHeight)
|
||||
|
||||
// Rank
|
||||
const rank = index + 1
|
||||
let rankColor = "#000"
|
||||
if (rank === 1) rankColor = "#ffd700"
|
||||
else if (rank === 2) rankColor = "#c0c0c0"
|
||||
else if (rank === 3) rankColor = "#cd7f32"
|
||||
|
||||
if (rank <= 3) {
|
||||
ctx.fillStyle = rankColor
|
||||
ctx.beginPath()
|
||||
ctx.arc(contentX + 40, currentY + this.itemHeight / 2, 18, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = "#fff"
|
||||
ctx.font = "bold 20px Arial"
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "middle"
|
||||
ctx.fillText(rank, contentX + 40, currentY + this.itemHeight / 2)
|
||||
} else {
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.font = "20px Arial"
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "middle"
|
||||
ctx.fillText(rank, contentX + 40, currentY + this.itemHeight / 2)
|
||||
}
|
||||
|
||||
// Name
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.font = `24px ${this.font}`
|
||||
ctx.textAlign = "left"
|
||||
ctx.fillText(entry.display_name, contentX + 90, currentY + this.itemHeight / 2)
|
||||
|
||||
// Score
|
||||
ctx.font = `bold 28px ${this.numbersFont}`
|
||||
ctx.textAlign = "right"
|
||||
ctx.fillStyle = "#ff4500"
|
||||
ctx.fillText(entry.score.toLocaleString(), contentX + contentW - 20, currentY + this.itemHeight / 2 + 2)
|
||||
|
||||
// Max Combo (small)
|
||||
ctx.font = `16px ${this.font}`
|
||||
ctx.fillStyle = "#666"
|
||||
ctx.fillText(`${strings.maxCombo}: ${entry.maxCombo}`, contentX + contentW - 180, currentY + this.itemHeight / 2 + 5)
|
||||
|
||||
currentY += this.itemHeight
|
||||
})
|
||||
}
|
||||
ctx.restore()
|
||||
|
||||
// Draw User Rank at Bottom if exists
|
||||
if (this.userRank) {
|
||||
const footerY = y + this.height - 50
|
||||
ctx.fillStyle = "#333"
|
||||
ctx.fillRect(x, footerY, this.width, 50)
|
||||
|
||||
ctx.fillStyle = "#fff"
|
||||
ctx.font = `20px ${this.font}`
|
||||
ctx.textAlign = "left"
|
||||
ctx.textBaseline = "middle"
|
||||
|
||||
const rankText = strings.yourRank.replace("%s", this.userRank.rank)
|
||||
ctx.fillText(rankText, x + 20, footerY + 25)
|
||||
|
||||
ctx.textAlign = "right"
|
||||
ctx.font = `bold 24px ${this.numbersFont}`
|
||||
ctx.fillText(this.userRank.entry.score.toLocaleString(), x + this.width - 20, footerY + 25)
|
||||
}
|
||||
}
|
||||
|
||||
mouseMove(x, y) {
|
||||
// Handle hover if needed
|
||||
}
|
||||
|
||||
mouseDown(x, y) {
|
||||
if (!this.visible) return false
|
||||
|
||||
// Check Close Button
|
||||
const dx = x - this.closeBtn.x
|
||||
const dy = y - this.closeBtn.y
|
||||
if (dx * dx + dy * dy < 20 * 20) {
|
||||
this.hide()
|
||||
assets.sounds["se_cancel"].play()
|
||||
return true
|
||||
}
|
||||
|
||||
// Check window click (consume event)
|
||||
// Adjust coordinates
|
||||
const winX = (innerWidth * (window.devicePixelRatio || 1) - this.width) / 2
|
||||
/* Note: simple check logic here, might need more precise logic mapping similar to mouseWheel */
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
wheel(delta) {
|
||||
if (!this.visible) return
|
||||
|
||||
this.scroll += delta * 50
|
||||
this.scroll = Math.max(0, Math.min(this.scroll, this.maxScroll))
|
||||
}
|
||||
}
|
||||
26
public/src/js/loader-worker.js
Normal file
26
public/src/js/loader-worker.js
Normal file
@@ -0,0 +1,26 @@
|
||||
self.addEventListener('message', async e => {
|
||||
const { id, url, type } = e.data
|
||||
try{
|
||||
const response = await fetch(url)
|
||||
if(!response.ok){
|
||||
throw new Error(response.status + " " + response.statusText)
|
||||
}
|
||||
let data
|
||||
if(type === "arraybuffer"){
|
||||
data = await response.arrayBuffer()
|
||||
}else if(type === "blob"){
|
||||
data = await response.blob()
|
||||
}else{
|
||||
data = await response.text()
|
||||
}
|
||||
self.postMessage({
|
||||
id: id,
|
||||
data: data
|
||||
}, type === "arraybuffer" ? [data] : undefined)
|
||||
}catch(e){
|
||||
self.postMessage({
|
||||
id: id,
|
||||
error: e.toString()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -11,6 +11,8 @@ class Loader{
|
||||
this.errorMessages = []
|
||||
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
|
||||
|
||||
this.initWorkers()
|
||||
|
||||
var promises = []
|
||||
|
||||
promises.push(this.ajax("src/views/loader.html").then(page => {
|
||||
@@ -23,6 +25,78 @@ class Loader{
|
||||
|
||||
Promise.all(promises).then(this.run.bind(this))
|
||||
}
|
||||
initWorkers(){
|
||||
this.workers = []
|
||||
this.workerQueue = []
|
||||
this.workerCallbacks = {}
|
||||
this.workerId = 0
|
||||
var concurrency = navigator.hardwareConcurrency || 4
|
||||
for(var i = 0; i < concurrency; i++){
|
||||
var worker = new Worker("src/js/loader-worker.js")
|
||||
worker.onmessage = this.onWorkerMessage.bind(this)
|
||||
this.workers.push({
|
||||
worker: worker,
|
||||
active: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
onWorkerMessage(e){
|
||||
var data = e.data
|
||||
var callback = this.workerCallbacks[data.id]
|
||||
if(callback){
|
||||
delete this.workerCallbacks[data.id]
|
||||
if(data.error){
|
||||
callback.reject(data.error)
|
||||
}else{
|
||||
callback.resolve(data.data)
|
||||
}
|
||||
this.workerFree(callback.workerIndex)
|
||||
}
|
||||
}
|
||||
workerFetch(url, type){
|
||||
return new Promise((resolve, reject) => {
|
||||
var id = ++this.workerId
|
||||
this.workerQueue.push({
|
||||
id: id,
|
||||
url: new URL(url, location.href).href,
|
||||
type: type,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
})
|
||||
this.workerRun()
|
||||
})
|
||||
}
|
||||
workerRun(){
|
||||
if(this.workerQueue.length === 0){
|
||||
return
|
||||
}
|
||||
var workerIndex = -1
|
||||
var minActive = Infinity
|
||||
for(var i = 0; i < this.workers.length; i++){
|
||||
if(this.workers[i].active < minActive){
|
||||
minActive = this.workers[i].active
|
||||
workerIndex = i
|
||||
}
|
||||
}
|
||||
if(workerIndex !== -1){
|
||||
var task = this.workerQueue.shift()
|
||||
var workerObj = this.workers[workerIndex]
|
||||
workerObj.active++
|
||||
this.workerCallbacks[task.id] = task
|
||||
task.workerIndex = workerIndex
|
||||
workerObj.worker.postMessage({
|
||||
id: task.id,
|
||||
url: task.url,
|
||||
type: task.type
|
||||
})
|
||||
}
|
||||
}
|
||||
workerFree(index){
|
||||
if(this.workers[index]){
|
||||
this.workers[index].active--
|
||||
this.workerRun()
|
||||
}
|
||||
}
|
||||
run(){
|
||||
this.promises = []
|
||||
this.loaderDiv = document.querySelector("#loader")
|
||||
@@ -538,6 +612,17 @@ class Loader{
|
||||
return css.join("\n")
|
||||
}
|
||||
ajax(url, customRequest, customResponse){
|
||||
if(!customResponse && (url.startsWith("src/") || url.startsWith("assets/") || url.indexOf("img/") !== -1 || url.indexOf("audio/") !== -1 || url.indexOf("fonts/") !== -1 || url.indexOf("views/") !== -1)){
|
||||
var type = "text"
|
||||
if(customRequest){
|
||||
var reqStub = {}
|
||||
customRequest(reqStub)
|
||||
if(reqStub.responseType){
|
||||
type = reqStub.responseType
|
||||
}
|
||||
}
|
||||
return this.workerFetch(url, type)
|
||||
}
|
||||
var request = new XMLHttpRequest()
|
||||
request.open("GET", url)
|
||||
var promise = pageEvents.load(request)
|
||||
@@ -557,12 +642,13 @@ class Loader{
|
||||
return promise
|
||||
}
|
||||
loadScript(url){
|
||||
var script = document.createElement("script")
|
||||
var url = url + this.queryString
|
||||
var promise = pageEvents.load(script)
|
||||
script.src = url
|
||||
document.head.appendChild(script)
|
||||
return promise
|
||||
return this.workerFetch(url, "text").then(code => {
|
||||
var script = document.createElement("script")
|
||||
code += "\n//# sourceURL=" + url
|
||||
script.text = code
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
getCsrfToken(){
|
||||
return this.ajax("api/csrftoken").then(response => {
|
||||
|
||||
@@ -178,94 +178,8 @@ class Scoresheet {
|
||||
}
|
||||
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() {
|
||||
if (!this.redrawRunning) {
|
||||
return
|
||||
@@ -935,6 +849,29 @@ class Scoresheet {
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
if (this.leaderboardResult) {
|
||||
ctx.save()
|
||||
var text = strings.recordBroken.replace("%s", this.leaderboardResult.rank)
|
||||
ctx.textAlign = "center"
|
||||
var y = 130
|
||||
if (this.multiplayer) {
|
||||
y = 400
|
||||
}
|
||||
this.draw.layeredText({
|
||||
ctx: ctx,
|
||||
text: text,
|
||||
x: 640,
|
||||
y: y,
|
||||
fontSize: 40,
|
||||
fontFamily: strings.font,
|
||||
align: "center"
|
||||
}, [
|
||||
{ outline: "#fff", letterBorder: 5 },
|
||||
{ fill: "#ff0000" }
|
||||
])
|
||||
ctx.restore()
|
||||
}
|
||||
ctx.restore()
|
||||
|
||||
if (this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000) {
|
||||
this.state.scoreNext = true
|
||||
@@ -1012,24 +949,12 @@ class Scoresheet {
|
||||
var title = this.controller.selectedSong.originalTitle
|
||||
var hash = this.controller.selectedSong.hash
|
||||
var difficulty = this.resultsObj.difficulty
|
||||
// Use id if available, otherwise use hash
|
||||
var songId = this.controller.selectedSong.id || hash
|
||||
var oldScore = scoreStorage.get(hash, difficulty, true)
|
||||
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
|
||||
var crown = ""
|
||||
if (clearReached) {
|
||||
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
|
||||
}
|
||||
|
||||
// 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.crown === "gold" || oldScore.crown === "silver" && !crown)) {
|
||||
crown = oldScore.crown
|
||||
@@ -1048,133 +973,56 @@ class Scoresheet {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!this.controller.autoPlayEnabled && account.loggedIn && !this.multiplayer) {
|
||||
this.submitToLeaderboard()
|
||||
}
|
||||
this.scoreSaved = true
|
||||
}
|
||||
|
||||
|
||||
submitToLeaderboard(songId, difficulty, scoreObj) {
|
||||
// Only submit if user is logged in and song has an ID
|
||||
if (!account.loggedIn || !songId) {
|
||||
submitToLeaderboard() {
|
||||
if (!this.resultsObj || !this.controller.selectedSong) {
|
||||
return
|
||||
}
|
||||
var song = this.controller.selectedSong
|
||||
var body = {
|
||||
song_id: song.id,
|
||||
difficulty: this.resultsObj.difficulty,
|
||||
score: this.resultsObj.points,
|
||||
good: this.resultsObj.good,
|
||||
ok: this.resultsObj.ok,
|
||||
bad: this.resultsObj.bad,
|
||||
maxCombo: this.resultsObj.maxCombo,
|
||||
hash: song.hash
|
||||
}
|
||||
|
||||
var self = this
|
||||
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.onload = function () {
|
||||
request.open("POST", gameConfig.basedir + "api/leaderboard/submit")
|
||||
pageEvents.load(request).then(() => {
|
||||
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)"
|
||||
var data = JSON.parse(request.response)
|
||||
if (data.status === "ok" && data.new_record && data.rank) {
|
||||
this.leaderboardResult = {
|
||||
rank: data.rank
|
||||
}
|
||||
assets.sounds["se_results_crown"].play()
|
||||
}
|
||||
} 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)"
|
||||
console.error("Leaderboard response parse error:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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({
|
||||
song_id: songId,
|
||||
difficulty: difficulty,
|
||||
score: scoreObj
|
||||
}))
|
||||
}).catch(() => {
|
||||
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)"
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error("Leaderboard submit failed:", e)
|
||||
})
|
||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
|
||||
request.setRequestHeader("X-CSRFToken", token)
|
||||
request.send(JSON.stringify(body))
|
||||
}).catch(e => {
|
||||
console.error("Failed to get CSRF token:", e)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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() {
|
||||
this.keyboard.clean()
|
||||
this.gamepad.clean()
|
||||
@@ -1193,17 +1041,10 @@ class Scoresheet {
|
||||
if (!this.multiplayer) {
|
||||
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.canvas
|
||||
delete this.fadeScreen
|
||||
delete this.results
|
||||
delete this.rules
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
class Search {
|
||||
constructor(...args) {
|
||||
class Search{
|
||||
constructor(...args){
|
||||
this.init(...args)
|
||||
}
|
||||
init(songSelect) {
|
||||
init(songSelect){
|
||||
this.songSelect = songSelect
|
||||
this.opened = false
|
||||
this.enabled = true
|
||||
|
||||
this.style = document.createElement("style")
|
||||
var css = []
|
||||
for (var i in this.songSelect.songSkin) {
|
||||
for(var i in this.songSelect.songSkin){
|
||||
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
|
||||
|
||||
css.push(loader.cssRuleset({
|
||||
@@ -33,7 +33,7 @@ class Search {
|
||||
loader.screen.appendChild(this.style)
|
||||
}
|
||||
|
||||
normalizeString(string) {
|
||||
normalizeString(string){
|
||||
string = string
|
||||
.replace('’', '\'').replace('“', '"').replace('”', '"')
|
||||
.replace('。', '.').replace(',', ',').replace('、', ',')
|
||||
@@ -45,28 +45,28 @@ class Search {
|
||||
return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
|
||||
}
|
||||
|
||||
perform(query) {
|
||||
perform(query){
|
||||
var results = []
|
||||
var filters = {}
|
||||
|
||||
var querySplit = query.split(" ").filter(word => {
|
||||
if (word.length > 0) {
|
||||
if(word.length > 0){
|
||||
var parts = word.toLowerCase().split(":")
|
||||
if (parts.length > 1) {
|
||||
switch (parts[0]) {
|
||||
if(parts.length > 1){
|
||||
switch(parts[0]){
|
||||
case "easy":
|
||||
case "normal":
|
||||
case "hard":
|
||||
case "oni":
|
||||
case "ura":
|
||||
var range = this.parseRange(parts[1])
|
||||
if (range) {
|
||||
if(range){
|
||||
filters[parts[0]] = range
|
||||
}
|
||||
break
|
||||
case "extreme":
|
||||
var range = this.parseRange(parts[1])
|
||||
if (range) {
|
||||
if(range){
|
||||
filters.oni = this.parseRange(parts[1])
|
||||
}
|
||||
break
|
||||
@@ -97,57 +97,57 @@ class Search {
|
||||
var totalFilters = Object.keys(filters).length
|
||||
var random = 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 passedFilters = 0
|
||||
|
||||
Object.keys(filters).forEach(filter => {
|
||||
var value = filters[filter]
|
||||
switch (filter) {
|
||||
switch(filter){
|
||||
case "easy":
|
||||
case "normal":
|
||||
case "hard":
|
||||
case "oni":
|
||||
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++
|
||||
}
|
||||
break
|
||||
case "clear":
|
||||
case "silver":
|
||||
case "gold":
|
||||
if (value === "any") {
|
||||
if(value === "any"){
|
||||
var score = scoreStorage.scores[song.hash]
|
||||
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++
|
||||
}
|
||||
})
|
||||
} else {
|
||||
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++
|
||||
}
|
||||
}
|
||||
break
|
||||
case "played":
|
||||
var score = scoreStorage.scores[song.hash]
|
||||
if ((value === "yes" && score) || (value === "no" && !score)) {
|
||||
if((value === "yes" && score) || (value === "no" && !score)){
|
||||
passedFilters++
|
||||
}
|
||||
break
|
||||
case "lyrics":
|
||||
if ((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)) {
|
||||
if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){
|
||||
passedFilters++
|
||||
}
|
||||
break
|
||||
case "creative":
|
||||
if ((value === "yes" && song.maker) || (value === "no" && !song.maker)) {
|
||||
if((value === "yes" && song.maker) || (value === "no" && !song.maker)){
|
||||
passedFilters++
|
||||
}
|
||||
break
|
||||
case "maker":
|
||||
if (song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())) {
|
||||
if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){
|
||||
passedFilters++
|
||||
}
|
||||
break
|
||||
@@ -155,24 +155,24 @@ class Search {
|
||||
var cat = assets.categories.find(cat => cat.id === song.category_id)
|
||||
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++
|
||||
}
|
||||
break
|
||||
case "diverge":
|
||||
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++
|
||||
}
|
||||
break
|
||||
case "random":
|
||||
if (value === "yes" || value === "no") {
|
||||
if(value === "yes" || value === "no"){
|
||||
random = value === "yes"
|
||||
passedFilters++
|
||||
}
|
||||
break
|
||||
case "all":
|
||||
if (value === "yes" || value === "no") {
|
||||
if(value === "yes" || value === "no"){
|
||||
allResults = value === "yes"
|
||||
passedFilters++
|
||||
}
|
||||
@@ -180,86 +180,86 @@ class Search {
|
||||
}
|
||||
})
|
||||
|
||||
if (passedFilters === totalFilters) {
|
||||
if(passedFilters === totalFilters){
|
||||
results.push(song)
|
||||
}
|
||||
}
|
||||
|
||||
var maxResults = allResults ? Infinity : (totalFilters > 0 && !query ? 100 : 50)
|
||||
|
||||
if (query) {
|
||||
if(query){
|
||||
results = fuzzysort.go(query, results, {
|
||||
keys: ["titlePrepared", "subtitlePrepared"],
|
||||
allowTypo: true,
|
||||
limit: maxResults,
|
||||
scoreFn: a => {
|
||||
if (a[0]) {
|
||||
if(a[0]){
|
||||
var score0 = a[0].score
|
||||
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 lastIdx = -3
|
||||
a[0].ranges.forEach(range => {
|
||||
if (range[0] - lastIdx <= 2) {
|
||||
if(range[0] - lastIdx <= 2){
|
||||
rangeAmount--
|
||||
score0 -= 1000
|
||||
}
|
||||
lastIdx = range[1]
|
||||
})
|
||||
var index = a[0].target.toLowerCase().indexOf(query)
|
||||
if (index !== -1) {
|
||||
if(index !== -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
|
||||
a[0].ranges = null
|
||||
} else if (rangeAmount !== 1) {
|
||||
}else if(rangeAmount !== 1){
|
||||
score0 -= 9000
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a[1]) {
|
||||
if(a[1]){
|
||||
var score1 = a[1].score - 1000
|
||||
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 lastIdx = -3
|
||||
a[1].ranges.forEach(range => {
|
||||
if (range[0] - lastIdx <= 2) {
|
||||
if(range[0] - lastIdx <= 2){
|
||||
rangeAmount--
|
||||
score1 -= 1000
|
||||
}
|
||||
lastIdx = range[1]
|
||||
})
|
||||
var index = a[1].target.indexOf(query)
|
||||
if (index !== -1) {
|
||||
if(index !== -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
|
||||
a[1].ranges = null
|
||||
} else if (rangeAmount !== 1) {
|
||||
}else if(rangeAmount !== 1){
|
||||
score1 -= 9000
|
||||
}
|
||||
}
|
||||
}
|
||||
if (random) {
|
||||
if(random){
|
||||
var rand = Math.random() * -9000
|
||||
if (score0 !== -Infinity) {
|
||||
if(score0 !== -Infinity){
|
||||
score0 = rand
|
||||
}
|
||||
if (score1 !== -Infinity) {
|
||||
if(score1 !== -Infinity){
|
||||
score1 = rand
|
||||
}
|
||||
}
|
||||
if (a[0]) {
|
||||
if(a[0]){
|
||||
return a[1] ? Math.max(score0, score1) : score0
|
||||
} else {
|
||||
}else{
|
||||
return a[1] ? score1 : -Infinity
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (random) {
|
||||
for (var i = results.length - 1; i > 0; i--) {
|
||||
}else{
|
||||
if(random){
|
||||
for(var i = results.length - 1; i > 0; i--){
|
||||
var j = Math.floor(Math.random() * (i + 1))
|
||||
var temp = results[i]
|
||||
results[i] = results[j]
|
||||
@@ -267,22 +267,22 @@ class Search {
|
||||
}
|
||||
}
|
||||
results = results.slice(0, maxResults).map(result => {
|
||||
return { obj: result }
|
||||
return {obj: result}
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
createResult(result, resultWidth, fontSize) {
|
||||
createResult(result, resultWidth, fontSize){
|
||||
var song = result.obj
|
||||
var title = this.songSelect.getLocalTitle(song.title, song.title_lang)
|
||||
var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
|
||||
|
||||
var id = "default"
|
||||
if (song.category_id) {
|
||||
if(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
|
||||
}
|
||||
}
|
||||
@@ -301,7 +301,7 @@ class Search {
|
||||
|
||||
resultInfoDiv.appendChild(resultInfoTitle)
|
||||
|
||||
if (subtitle) {
|
||||
if(subtitle){
|
||||
resultInfoDiv.appendChild(document.createElement("br"))
|
||||
var resultInfoSubtitle = document.createElement("span")
|
||||
resultInfoSubtitle.classList.add("song-search-result-subtitle")
|
||||
@@ -343,14 +343,14 @@ class Search {
|
||||
this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font
|
||||
var titleWidth = this.songSelect.ctx.measureText(title).width
|
||||
var titleRatio = resultWidth / titleWidth
|
||||
if (titleRatio < 1) {
|
||||
if(titleRatio < 1){
|
||||
resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)"
|
||||
}
|
||||
if (subtitle) {
|
||||
this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font
|
||||
if(subtitle){
|
||||
this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font
|
||||
var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width
|
||||
var subtitleRatio = resultWidth / subtitleWidth
|
||||
if (subtitleRatio < 1) {
|
||||
if(subtitleRatio < 1){
|
||||
resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)"
|
||||
}
|
||||
}
|
||||
@@ -358,13 +358,12 @@ class Search {
|
||||
return resultDiv
|
||||
}
|
||||
|
||||
highlightResult(text, result) {
|
||||
if (text === null || text === undefined) return document.createDocumentFragment();
|
||||
highlightResult(text, result){
|
||||
var fragment = document.createDocumentFragment()
|
||||
var ranges = (result ? result.ranges : null) || []
|
||||
var lastIdx = 0
|
||||
ranges.forEach(range => {
|
||||
if (lastIdx !== range[0]) {
|
||||
if(lastIdx !== range[0]){
|
||||
fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0])))
|
||||
}
|
||||
var span = document.createElement("span")
|
||||
@@ -373,20 +372,20 @@ class Search {
|
||||
fragment.appendChild(span)
|
||||
lastIdx = range[1] + 1
|
||||
})
|
||||
if (text.length !== lastIdx) {
|
||||
if(text.length !== lastIdx){
|
||||
fragment.appendChild(document.createTextNode(text.slice(lastIdx)))
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
setActive(idx) {
|
||||
setActive(idx){
|
||||
this.songSelect.playSound("se_ka")
|
||||
var active = this.div.querySelector(":scope .song-search-result-active")
|
||||
if (active) {
|
||||
if(active){
|
||||
active.classList.remove("song-search-result-active")
|
||||
}
|
||||
|
||||
if (idx === null) {
|
||||
if(idx === null){
|
||||
this.active = null
|
||||
return
|
||||
}
|
||||
@@ -399,11 +398,11 @@ class Search {
|
||||
this.active = idx
|
||||
}
|
||||
|
||||
display(fromButton = false) {
|
||||
if (!this.enabled) {
|
||||
display(fromButton=false){
|
||||
if(!this.enabled){
|
||||
return
|
||||
}
|
||||
if (this.opened) {
|
||||
if(this.opened){
|
||||
return this.remove(true)
|
||||
}
|
||||
|
||||
@@ -413,7 +412,7 @@ class Search {
|
||||
this.div.innerHTML = assets.pages["search"]
|
||||
|
||||
this.container = this.div.querySelector(":scope #song-search-container")
|
||||
if (this.touchEnabled) {
|
||||
if(this.touchEnabled){
|
||||
this.container.classList.add("touch-enabled")
|
||||
}
|
||||
pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this))
|
||||
@@ -427,9 +426,9 @@ class Search {
|
||||
this.setTip()
|
||||
cancelTouch = false
|
||||
noResizeRoot = true
|
||||
if (this.songSelect.songs[this.songSelect.selectedSong].courses) {
|
||||
if(this.songSelect.songs[this.songSelect.selectedSong].courses){
|
||||
snd.previewGain.setVolumeMul(0.5)
|
||||
} else if (this.songSelect.bgmEnabled) {
|
||||
}else if(this.songSelect.bgmEnabled){
|
||||
snd.musicGain.setVolumeMul(0.5)
|
||||
}
|
||||
|
||||
@@ -439,7 +438,7 @@ class Search {
|
||||
}, 10)
|
||||
|
||||
var lastQuery = localStorage.getItem("lastSearchQuery")
|
||||
if (lastQuery) {
|
||||
if(lastQuery){
|
||||
this.input.value = lastQuery
|
||||
this.input.dispatchEvent(new Event("input", {
|
||||
value: lastQuery
|
||||
@@ -447,15 +446,15 @@ class Search {
|
||||
}
|
||||
}
|
||||
|
||||
remove(byUser = false) {
|
||||
if (this.opened) {
|
||||
remove(byUser=false){
|
||||
if(this.opened){
|
||||
this.opened = false
|
||||
if (byUser) {
|
||||
if(byUser){
|
||||
this.songSelect.playSound("se_cancel")
|
||||
}
|
||||
|
||||
pageEvents.remove(this.div.querySelector(":scope #song-search-container"),
|
||||
["mousedown", "touchstart"])
|
||||
["mousedown", "touchstart"])
|
||||
pageEvents.remove(this.input, ["input"])
|
||||
|
||||
this.div.remove()
|
||||
@@ -466,21 +465,21 @@ class Search {
|
||||
delete this.active
|
||||
cancelTouch = true
|
||||
noResizeRoot = false
|
||||
if (this.songSelect.songs[this.songSelect.selectedSong].courses) {
|
||||
if(this.songSelect.songs[this.songSelect.selectedSong].courses){
|
||||
snd.previewGain.setVolumeMul(1)
|
||||
} else if (this.songSelect.bgmEnabled) {
|
||||
}else if(this.songSelect.bgmEnabled){
|
||||
snd.musicGain.setVolumeMul(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTip(tip, error = false) {
|
||||
if (this.tip) {
|
||||
setTip(tip, error=false){
|
||||
if(this.tip){
|
||||
this.tip.remove()
|
||||
delete this.tip
|
||||
}
|
||||
|
||||
if (!tip) {
|
||||
if(!tip){
|
||||
tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)]
|
||||
}
|
||||
|
||||
@@ -493,12 +492,12 @@ class Search {
|
||||
this.tip.innerText = tip
|
||||
this.div.querySelector(":scope #song-search").appendChild(this.tip)
|
||||
|
||||
if (error) {
|
||||
if(error){
|
||||
this.tip.classList.add("song-search-tip-error")
|
||||
}
|
||||
}
|
||||
|
||||
proceed(songId) {
|
||||
proceed(songId){
|
||||
if (/^-?\d+$/.test(songId)) {
|
||||
songId = parseInt(songId)
|
||||
}
|
||||
@@ -506,7 +505,7 @@ class Search {
|
||||
var song = this.songSelect.songs.find(song => song.id === songId)
|
||||
this.remove()
|
||||
this.songSelect.playBgm(false)
|
||||
if (this.songSelect.previewing === "muted") {
|
||||
if(this.songSelect.previewing === "muted"){
|
||||
this.songSelect.previewing = null
|
||||
}
|
||||
|
||||
@@ -515,41 +514,41 @@ class Search {
|
||||
this.songSelect.toSelectDifficulty()
|
||||
}
|
||||
|
||||
scrollTo(element) {
|
||||
scrollTo(element){
|
||||
var parentNode = element.parentNode
|
||||
var selected = element.getBoundingClientRect()
|
||||
var parent = parentNode.getBoundingClientRect()
|
||||
var scrollY = parentNode.scrollTop
|
||||
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
|
||||
} else {
|
||||
}else{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseRange(string) {
|
||||
parseRange(string){
|
||||
var range = string.split("-")
|
||||
if (range.length == 1) {
|
||||
if(range.length == 1){
|
||||
var min = parseInt(range[0]) || 0
|
||||
return min > 0 ? { min: min, max: min } : false
|
||||
} else if (range.length == 2) {
|
||||
return min > 0 ? {min: min, max: min} : false
|
||||
} else if(range.length == 2){
|
||||
var min = parseInt(range[0]) || 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 range
|
||||
indexes.forEach(idx => {
|
||||
if (range && range[1] === idx - 1) {
|
||||
if(range && range[1] === idx - 1){
|
||||
range[1] = idx
|
||||
} else {
|
||||
}else{
|
||||
range = [idx, idx]
|
||||
ranges.push(range)
|
||||
}
|
||||
@@ -557,13 +556,13 @@ class Search {
|
||||
return ranges
|
||||
}
|
||||
|
||||
onInput(resize) {
|
||||
onInput(resize){
|
||||
var text = this.input.value
|
||||
localStorage.setItem("lastSearchQuery", text)
|
||||
text = text.toLowerCase()
|
||||
|
||||
if (text.length === 0) {
|
||||
if (!resize) {
|
||||
if(text.length === 0){
|
||||
if(!resize){
|
||||
this.setTip()
|
||||
}
|
||||
return
|
||||
@@ -571,10 +570,10 @@ class Search {
|
||||
|
||||
var new_results = this.perform(text)
|
||||
|
||||
if (new_results.length === 0) {
|
||||
if(new_results.length === 0){
|
||||
this.setTip(strings.search.noResults, true)
|
||||
return
|
||||
} else if (this.tip) {
|
||||
}else if(this.tip){
|
||||
this.tip.remove()
|
||||
delete this.tip
|
||||
}
|
||||
@@ -602,74 +601,74 @@ class Search {
|
||||
this.songSelect.ctx.restore()
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
if ((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1) {
|
||||
onClick(e){
|
||||
if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){
|
||||
this.remove(true)
|
||||
} else if (e.which === 1) {
|
||||
}else if(e.which === 1){
|
||||
var songEl = e.target.closest(".song-search-result")
|
||||
if (songEl) {
|
||||
if(songEl){
|
||||
var songId = songEl.dataset.songId
|
||||
this.proceed(songId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keyPress(pressed, name, event, repeat, ctrl) {
|
||||
if (name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) {
|
||||
keyPress(pressed, name, event, repeat, ctrl){
|
||||
if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) {
|
||||
this.remove(true)
|
||||
if (event) {
|
||||
if(event){
|
||||
event.preventDefault()
|
||||
}
|
||||
} else if (name === "down" && this.results.length) {
|
||||
if (this.input == document.activeElement && this.results) {
|
||||
}else if(name === "down" && this.results.length){
|
||||
if(this.input == document.activeElement && this.results){
|
||||
this.setActive(0)
|
||||
} else if (this.active === this.results.length - 1) {
|
||||
}else if(this.active === this.results.length - 1){
|
||||
this.setActive(null)
|
||||
this.input.focus()
|
||||
} else if (Number.isInteger(this.active)) {
|
||||
}else if(Number.isInteger(this.active)){
|
||||
this.setActive(this.active + 1)
|
||||
} else {
|
||||
}else{
|
||||
this.setActive(0)
|
||||
}
|
||||
} else if (name === "up" && this.results.length) {
|
||||
if (this.input == document.activeElement && this.results) {
|
||||
}else if(name === "up" && this.results.length){
|
||||
if(this.input == document.activeElement && this.results){
|
||||
this.setActive(this.results.length - 1)
|
||||
} else if (this.active === 0) {
|
||||
}else if(this.active === 0){
|
||||
this.setActive(null)
|
||||
this.input.focus()
|
||||
setTimeout(() => {
|
||||
this.input.setSelectionRange(this.input.value.length, this.input.value.length)
|
||||
}, 0)
|
||||
} else if (Number.isInteger(this.active)) {
|
||||
}else if(Number.isInteger(this.active)){
|
||||
this.setActive(this.active - 1)
|
||||
} else {
|
||||
}else{
|
||||
this.setActive(this.results.length - 1)
|
||||
}
|
||||
} else if (name === "confirm") {
|
||||
if (Number.isInteger(this.active)) {
|
||||
}else if(name === "confirm"){
|
||||
if(Number.isInteger(this.active)){
|
||||
this.proceed(this.results[this.active].dataset.songId)
|
||||
} else {
|
||||
}else{
|
||||
this.onInput()
|
||||
if (event.keyCode === 13 && this.songSelect.touchEnabled) {
|
||||
if(event.keyCode === 13 && this.songSelect.touchEnabled){
|
||||
this.input.blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redraw() {
|
||||
if (this.opened && this.container) {
|
||||
redraw(){
|
||||
if(this.opened && this.container){
|
||||
var vmin = Math.min(innerWidth, lastHeight) / 100
|
||||
if (this.vmin !== vmin) {
|
||||
if(this.vmin !== vmin){
|
||||
this.container.style.setProperty("--vmin", vmin + "px")
|
||||
this.vmin = vmin
|
||||
}
|
||||
} else {
|
||||
}else{
|
||||
this.vmin = null
|
||||
}
|
||||
}
|
||||
|
||||
clean() {
|
||||
clean(){
|
||||
loader.screen.removeChild(this.style)
|
||||
fuzzysort.cleanup()
|
||||
delete this.container
|
||||
|
||||
@@ -119,6 +119,7 @@ class SongSelect {
|
||||
this.font = strings.font
|
||||
|
||||
this.search = new Search(this)
|
||||
this.leaderboard = new LeaderboardUI(this)
|
||||
|
||||
this.songs = []
|
||||
for (let song of assets.songs) {
|
||||
@@ -298,6 +299,12 @@ class SongSelect {
|
||||
iconName: "back",
|
||||
iconFill: "#f7d39c",
|
||||
letterSpacing: 4
|
||||
}, {
|
||||
text: strings.leaderboard,
|
||||
fill: "#42c0d2",
|
||||
iconName: "crown",
|
||||
iconFill: "#8ee7f2",
|
||||
letterSpacing: 0
|
||||
}, {
|
||||
text: strings.songOptions,
|
||||
fill: "#b2e442",
|
||||
@@ -310,12 +317,6 @@ class SongSelect {
|
||||
iconName: "download",
|
||||
iconFill: "#e7cbe1",
|
||||
letterSpacing: 4
|
||||
}, {
|
||||
text: "排行榜",
|
||||
fill: "#ffd700",
|
||||
iconName: "ranking",
|
||||
iconFill: "#fff4b5",
|
||||
letterSpacing: 4
|
||||
}]
|
||||
this.optionsList = [strings.none, strings.auto, strings.netplay]
|
||||
|
||||
@@ -590,15 +591,13 @@ class SongSelect {
|
||||
if (this.selectedDiff === 0) {
|
||||
this.toSongSelect()
|
||||
} else if (this.selectedDiff === 1) {
|
||||
this.toOptions(1)
|
||||
} else if (this.selectedDiff === 2) {
|
||||
this.toDownload()
|
||||
} else if (this.selectedDiff === 3) {
|
||||
this.toLeaderboard()
|
||||
} else if (this.selectedDiff === 3) {
|
||||
this.toDownload()
|
||||
} else if (this.selectedDiff === 4) {
|
||||
this.toDelete()
|
||||
} else if (false) {
|
||||
// Moved above
|
||||
} else if (this.selectedDiff === 2) {
|
||||
this.toOptions(1)
|
||||
} else {
|
||||
this.toLoadSong(this.selectedDiff - this.diffOptions.length, shift, ctrl)
|
||||
}
|
||||
@@ -651,6 +650,14 @@ class SongSelect {
|
||||
if (event.target !== this.canvas || !this.redrawRunning) {
|
||||
return
|
||||
}
|
||||
if (this.leaderboard && this.leaderboard.visible) {
|
||||
if (event.type === "mousedown") {
|
||||
var x = event.offsetX
|
||||
var y = event.offsetY
|
||||
this.leaderboard.mouseDown(x, y)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (event.type === "mousedown") {
|
||||
if (event.which !== 1) {
|
||||
return
|
||||
@@ -699,13 +706,13 @@ class SongSelect {
|
||||
this.selectedDiff = 0
|
||||
this.toSongSelect()
|
||||
} else if (moveBy === 1) {
|
||||
this.toOptions(1)
|
||||
} else if (moveBy === 2) {
|
||||
this.toDownload()
|
||||
} else if (moveBy === 3) {
|
||||
this.toLeaderboard()
|
||||
} else if (moveBy === 3) {
|
||||
this.toDownload()
|
||||
} else if (moveBy === 4) {
|
||||
this.toDelete()
|
||||
} else if (moveBy === 2) {
|
||||
this.toOptions(1)
|
||||
} else if (moveBy === "maker") {
|
||||
window.open(this.songs[this.selectedSong].maker.url)
|
||||
} else if (moveBy === this.diffOptions.length + 4) {
|
||||
@@ -2714,6 +2721,10 @@ class SongSelect {
|
||||
if (screen === "titleFadeIn") {
|
||||
ctx.save()
|
||||
|
||||
if (this.leaderboard && this.leaderboard.visible) {
|
||||
this.leaderboard.draw(ctx, winW, winH, this.pixelRatio)
|
||||
}
|
||||
|
||||
var elapsed = ms - this.state.screenMS
|
||||
ctx.globalAlpha = Math.max(0, 1 - elapsed / 500)
|
||||
ctx.fillStyle = "#000"
|
||||
@@ -3037,6 +3048,99 @@ class SongSelect {
|
||||
this.search.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
toLeaderboard() {
|
||||
var song = this.songs[this.selectedSong]
|
||||
if (!song) {
|
||||
return
|
||||
}
|
||||
var songId = song.id
|
||||
if (song.hash && typeof songId === 'string' && isNaN(parseInt(songId))) {
|
||||
// If key is string and not numeric, it might be hash, or backend expects numeric ID
|
||||
// Ideally we pass hash if available, or ID
|
||||
// Our backend implementation handles both, but prefers ID or hash
|
||||
}
|
||||
// Actually leaderboard API expects song_id to be what is stored in DB.
|
||||
// For custom songs it uses hash? No, custom songs are local.
|
||||
// Let's pass song.id by default.
|
||||
|
||||
// Determine difficulty ID
|
||||
// difficultyId array: ["easy", "normal", "hard", "oni", "ura"]
|
||||
// But in menu we select difficulty by pressing right/left which updates selectedDiff
|
||||
// When we click "Leaderboard" button, we are NOT yet on a specific difficulty?
|
||||
// Wait, the leaderboard button is in the difficulty selection SCREEN.
|
||||
// BUT the button itself is an ACTION, like "Download" or "Back".
|
||||
// It doesn't select a difficulty.
|
||||
// However, to show a leaderboard, we need a difficulty.
|
||||
// Should the leaderboard UI allow switching difficulties?
|
||||
// OR does the leaderboard button show the leaderboard for the CURRENTLY HIGHLIGHTED difficulty?
|
||||
// In Taiko Web, the difficulty selection screen creates vertical bars for each difficulty.
|
||||
// User navigates LEFT/RIGHT to select a difficulty or an option (Back, Custom, Download..).
|
||||
// So if user selects "Leaderboard" option, which difficulty is selected? None!
|
||||
// "Leaderboard" is a menu item parallel to "Easy", "Normal", "Hard"...
|
||||
// NO!
|
||||
// `this.diffOptions` are valid selections in `selectedDiff`.
|
||||
// `selectedDiff` 0..N are options efficiently.
|
||||
// The difficulties are further to the right.
|
||||
// `moveBy = selectedDiff - diffOptions.length`.
|
||||
// So if I select "Leaderboard" (index 1), I am NOT selecting a song difficulty.
|
||||
// So `toLeaderboard` needs to know which difficulty to show?
|
||||
// Maybe default to Oni? Or show a difficulty selector INSIDE the leaderboard UI?
|
||||
// OR, change the UX: don't make it a separate main menu item.
|
||||
// Make it a small button accompanying EACH difficulty?
|
||||
//
|
||||
// In my plan: "In songselect.js ... add 'Leaderboard' button to diffOptions".
|
||||
// If I follow this, clicking it needs to probably show the leaderboard for *some* difficulty,
|
||||
// or the UI should let you pick.
|
||||
// `LeaderboardUI` supports `show(songId, difficulty)`.
|
||||
// Let's make it show "Oni" by default if available, or the highest available.
|
||||
// And add tabs in LeaderboardUI to switch difficulty?
|
||||
// Or, simpler: Just show Oni for now, or last played.
|
||||
//
|
||||
// Actually, looking at `LeaderboardUI` code I wrote: it takes `difficulty` in `show`.
|
||||
// I did NOT implement tabs in `LeaderboardUI`.
|
||||
//
|
||||
// Alternative idea:
|
||||
// When user hovers/selects a difficulty (e.g. Oni), maybe press a key to see leaderboard?
|
||||
// But the requirement was "Add a Leaderboard option".
|
||||
//
|
||||
// Let's check how "Download" works. It downloads the current song.
|
||||
// "Song Options" opens a menu.
|
||||
//
|
||||
// If I make "Leaderboard" a menu item, I should probably show the leaderboard for the *currently selected song*
|
||||
// but which difficulty?
|
||||
// Let's default to Oni.
|
||||
// Better: The leaderboard UI should have difficulty tabs.
|
||||
// Since I can't easily redesign `LeaderboardUI` in this tool call (it's already written),
|
||||
// I will stick to showing *Oni* (or highest available) for now,
|
||||
// AND maybe I can quickly patch `LeaderboardUI` later to add tabs if needed.
|
||||
//
|
||||
// Wait, `songselect.js` logic:
|
||||
// `this.selectedDiff` tracks what is selected.
|
||||
// If > `diffOptions.length`, it is a specific difficulty.
|
||||
// If I am on "Leaderboard" button, I am NOT on a difficulty.
|
||||
//
|
||||
// Let's try to pass 'oni' by default.
|
||||
|
||||
var diff = "oni"
|
||||
if (this.state.ura) {
|
||||
diff = "ura"
|
||||
}
|
||||
// Check if song has oni
|
||||
if (!song.courses[diff]) {
|
||||
// Find highest
|
||||
var diffs = ["ura", "oni", "hard", "normal", "easy"]
|
||||
for (var d of diffs) {
|
||||
if (song.courses[d]) {
|
||||
diff = d
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.leaderboard.show(song.id, diff)
|
||||
}
|
||||
|
||||
onsongsel(response) {
|
||||
if (response && response.value) {
|
||||
var selected = false
|
||||
@@ -3158,6 +3262,13 @@ class SongSelect {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
wheel(event) {
|
||||
if (this.leaderboard && this.leaderboard.visible) {
|
||||
this.leaderboard.wheel(event.deltaY)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.keyboard.clean()
|
||||
this.gamepad.clean()
|
||||
@@ -3334,25 +3445,4 @@ class SongSelect {
|
||||
alert(text);
|
||||
});
|
||||
}
|
||||
toLeaderboard() {
|
||||
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
|
||||
}
|
||||
|
||||
// Default to first available difficulty if not in a valid difficulty selection
|
||||
var selectedDiff = this.selectedDiff - this.diffOptions.length
|
||||
if (selectedDiff < 0) {
|
||||
selectedDiff = 3 // Default to oni
|
||||
}
|
||||
var diffId = this.difficultyId[selectedDiff]
|
||||
|
||||
this.clean()
|
||||
this.playSound("se_don")
|
||||
new Leaderboard().display(songId, diffId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -439,6 +439,77 @@ var translations = {
|
||||
ko: "연타 횟수"
|
||||
},
|
||||
|
||||
leaderboard: {
|
||||
ja: "ランキング",
|
||||
en: "Leaderboard",
|
||||
cn: "排行榜",
|
||||
tw: "排行榜",
|
||||
ko: "순위표"
|
||||
},
|
||||
leaderboardTitle: {
|
||||
ja: "%s の ランキング",
|
||||
en: "%s Leaderboard",
|
||||
cn: "%s 排行榜",
|
||||
tw: "%s 排行榜",
|
||||
ko: "%s 순위표"
|
||||
},
|
||||
rank: {
|
||||
ja: "順位",
|
||||
en: "Rank",
|
||||
cn: "排名",
|
||||
tw: "排名",
|
||||
ko: "순위"
|
||||
},
|
||||
playerName: {
|
||||
ja: "プレイヤー",
|
||||
en: "Player",
|
||||
cn: "玩家",
|
||||
tw: "玩家",
|
||||
ko: "플레이어"
|
||||
},
|
||||
score: {
|
||||
ja: "スコア",
|
||||
en: "Score",
|
||||
cn: "分数",
|
||||
tw: "分數",
|
||||
ko: "점수"
|
||||
},
|
||||
recordBroken: {
|
||||
ja: "最高記録を更新!第%s位",
|
||||
en: "New Record! Rank #%s",
|
||||
cn: "打破最佳纪录 第%s名",
|
||||
tw: "打破最佳紀錄 第%s名",
|
||||
ko: "최고 기록 경신! %s위"
|
||||
},
|
||||
yourRank: {
|
||||
ja: "あなたの順位: %s位",
|
||||
en: "Your Rank: #%s",
|
||||
cn: "您的排名: 第%s名",
|
||||
tw: "您的排名: 第%s名",
|
||||
ko: "순위: %s위"
|
||||
},
|
||||
notRanked: {
|
||||
ja: "ランク外",
|
||||
en: "Not Ranked",
|
||||
cn: "未上榜",
|
||||
tw: "未上榜",
|
||||
ko: "순위 없음"
|
||||
},
|
||||
loadingLeaderboard: {
|
||||
ja: "ランキング読み込み中...",
|
||||
en: "Loading Leaderboard...",
|
||||
cn: "加载排行榜中...",
|
||||
tw: "讀取排行榜中...",
|
||||
ko: "순위표 로딩 중..."
|
||||
},
|
||||
noScores: {
|
||||
ja: "記録なし",
|
||||
en: "No Scores Yet",
|
||||
cn: "暂无记录",
|
||||
tw: "暫無紀錄",
|
||||
ko: "기록 없음"
|
||||
},
|
||||
|
||||
errorOccured: {
|
||||
ja: "エラーが発生しました。再読み込みしてください。",
|
||||
en: "An error occurred, please refresh",
|
||||
@@ -1472,26 +1543,26 @@ var translations = {
|
||||
}
|
||||
}
|
||||
var allStrings = {}
|
||||
function separateStrings(){
|
||||
for(var j in languageList){
|
||||
function separateStrings() {
|
||||
for (var j in languageList) {
|
||||
var lang = languageList[j]
|
||||
allStrings[lang] = {
|
||||
id: lang
|
||||
}
|
||||
var str = allStrings[lang]
|
||||
var translateObj = function(obj, name, str){
|
||||
if("en" in obj){
|
||||
for(var i in obj){
|
||||
var translateObj = function (obj, name, str) {
|
||||
if ("en" in obj) {
|
||||
for (var i in obj) {
|
||||
str[name] = obj[lang] || obj.en
|
||||
}
|
||||
}else if(obj){
|
||||
} else if (obj) {
|
||||
str[name] = {}
|
||||
for(var i in obj){
|
||||
for (var i in obj) {
|
||||
translateObj(obj[i], i, str[name])
|
||||
}
|
||||
}
|
||||
}
|
||||
for(var i in translations){
|
||||
for (var i in translations) {
|
||||
translateObj(translations[i], i, str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<div id="leaderboard">
|
||||
<canvas id="leaderboard-canvas"></canvas>
|
||||
</div>
|
||||
@@ -1,133 +0,0 @@
|
||||
|
||||
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()
|
||||
22
schema.py
22
schema.py
@@ -85,10 +85,24 @@ leaderboard_submit = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'song_id': {'type': ['number', 'string']},
|
||||
'difficulty': {'type': 'string'},
|
||||
'score': {'type': 'object'}
|
||||
'song_id': {'type': ['integer', 'string']},
|
||||
'difficulty': {'type': 'string', 'enum': ['easy', 'normal', 'hard', 'oni', 'ura']},
|
||||
'score': {'type': 'integer'},
|
||||
'good': {'type': 'integer'},
|
||||
'ok': {'type': 'integer'},
|
||||
'bad': {'type': 'integer'},
|
||||
'maxCombo': {'type': 'integer'},
|
||||
'hash': {'type': 'string'}
|
||||
},
|
||||
'required': ['song_id', 'difficulty', 'score']
|
||||
'required': ['difficulty', 'score']
|
||||
}
|
||||
|
||||
leaderboard_get = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'song_id': {'type': ['integer', 'string']},
|
||||
'difficulty': {'type': 'string'}
|
||||
},
|
||||
'required': ['song_id', 'difficulty']
|
||||
}
|
||||
|
||||
2
setup.sh
2
setup.sh
@@ -8,7 +8,7 @@ CODENAME=${VERSION_CODENAME:-}
|
||||
VERSION=${VERSION_ID:-}
|
||||
|
||||
echo "更新系统软件源..."
|
||||
apt-get update -y || true
|
||||
apt-get update -y
|
||||
echo "安装基础依赖..."
|
||||
apt-get install -y python3 python3-venv python3-pip git ffmpeg rsync curl gnupg libcap2-bin
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>太鼓ウェブ - Taiko Web | (゚∀゚)</title>
|
||||
<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="description"
|
||||
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="description" content="2025年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 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="noimageindex">
|
||||
<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="manifest" href="manifest.json">
|
||||
@@ -27,17 +24,14 @@
|
||||
<script src="src/js/lib/jszip.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="assets"></div>
|
||||
<div id="screen" class="pattern-bg"></div>
|
||||
<div data-nosnippet id="version">
|
||||
{% 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.1.0">vLightNova 1.1.0</a>
|
||||
<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>
|
||||
{% else %}
|
||||
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.1.0">vLightNova
|
||||
1.1.0</a>
|
||||
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
||||
@@ -49,5 +43,4 @@
|
||||
</div>
|
||||
</noscript>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
// 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.");
|
||||
43
tools/reset_leaderboard.py
Normal file
43
tools/reset_leaderboard.py
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pymongo import MongoClient
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
try:
|
||||
import config
|
||||
except ImportError:
|
||||
# Handle case where config might not be in path or environment variables are used
|
||||
config = None
|
||||
|
||||
def reset_leaderboard():
|
||||
"""Delete leaderboard entries not from the current month"""
|
||||
mongo_host = os.environ.get("TAIKO_WEB_MONGO_HOST")
|
||||
if not mongo_host and config:
|
||||
mongo_host = config.MONGO['host']
|
||||
|
||||
db_name = "taiko"
|
||||
if config:
|
||||
db_name = config.MONGO['database']
|
||||
|
||||
if not mongo_host:
|
||||
print("Error: content not found for MONGO_HOST")
|
||||
return
|
||||
|
||||
client = MongoClient(host=mongo_host)
|
||||
db = client[db_name]
|
||||
|
||||
current_month = datetime.now().strftime('%Y-%m')
|
||||
|
||||
# Delete old month data
|
||||
result = db.leaderboard.delete_many({'month': {'$ne': current_month}})
|
||||
|
||||
print(f"Deleted {result.deleted_count} old leaderboard entries")
|
||||
print(f"Current month: {current_month}")
|
||||
|
||||
client.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
reset_leaderboard()
|
||||
Reference in New Issue
Block a user