Compare commits
3 Commits
6d7be5c45c
...
F-B
| 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`.
|
||||||
135
app.py
135
app.py
@@ -13,6 +13,7 @@ import requests
|
|||||||
import schema
|
import schema
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# -- カスタム --
|
# -- カスタム --
|
||||||
import traceback
|
import traceback
|
||||||
@@ -105,6 +106,13 @@ db.users.create_index('username', unique=True)
|
|||||||
db.songs.create_index('id', unique=True)
|
db.songs.create_index('id', unique=True)
|
||||||
db.songs.create_index('song_type')
|
db.songs.create_index('song_type')
|
||||||
db.scores.create_index('username')
|
db.scores.create_index('username')
|
||||||
|
db.leaderboard.create_index([
|
||||||
|
('song_id', 1),
|
||||||
|
('difficulty', 1),
|
||||||
|
('month', 1),
|
||||||
|
('score', -1)
|
||||||
|
])
|
||||||
|
db.leaderboard.create_index('username')
|
||||||
|
|
||||||
|
|
||||||
class HashException(Exception):
|
class HashException(Exception):
|
||||||
@@ -746,6 +754,133 @@ def route_api_scores_get():
|
|||||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/submit', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def route_api_leaderboard_submit():
|
||||||
|
data = request.get_json()
|
||||||
|
if not schema.validate(data, schema.leaderboard_submit):
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
username = session.get('username')
|
||||||
|
user = db.users.find_one({'username': username})
|
||||||
|
|
||||||
|
current_month = datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
# Handle song_id type
|
||||||
|
song_id = data['song_id']
|
||||||
|
|
||||||
|
query = {
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': data['difficulty'],
|
||||||
|
'month': current_month,
|
||||||
|
'username': username
|
||||||
|
}
|
||||||
|
|
||||||
|
existing = db.leaderboard.find_one(query)
|
||||||
|
|
||||||
|
new_record = False
|
||||||
|
rank_info = None
|
||||||
|
|
||||||
|
if not existing or data['score'] > existing['score']:
|
||||||
|
entry = {
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': data['difficulty'],
|
||||||
|
'month': current_month,
|
||||||
|
'username': username,
|
||||||
|
'display_name': user['display_name'],
|
||||||
|
'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
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'new_record': new_record,
|
||||||
|
'rank': rank_info
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/get')
|
||||||
|
def route_api_leaderboard_get():
|
||||||
|
song_id = request.args.get('song_id')
|
||||||
|
difficulty = request.args.get('difficulty')
|
||||||
|
|
||||||
|
if not song_id or not difficulty:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
# Try convert song_id to int if it looks like one
|
||||||
|
if re.match('^[0-9]+$', str(song_id)):
|
||||||
|
song_id = int(song_id)
|
||||||
|
|
||||||
|
current_month = datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
query = {
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get top 50
|
||||||
|
leaderboard = list(db.leaderboard.find(query, {'_id': False}).sort('score', -1).limit(50))
|
||||||
|
|
||||||
|
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')
|
@app.route(basedir + 'privacy')
|
||||||
def route_api_privacy():
|
def route_api_privacy():
|
||||||
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ var assets = {
|
|||||||
"parseosu.js",
|
"parseosu.js",
|
||||||
"titlescreen.js",
|
"titlescreen.js",
|
||||||
"scoresheet.js",
|
"scoresheet.js",
|
||||||
|
"leaderboard_ui.js",
|
||||||
"songselect.js",
|
"songselect.js",
|
||||||
"keyboard.js",
|
"keyboard.js",
|
||||||
"gameinput.js",
|
"gameinput.js",
|
||||||
@@ -98,7 +99,7 @@ var assets = {
|
|||||||
"audioSfx": [
|
"audioSfx": [
|
||||||
"se_pause.ogg",
|
"se_pause.ogg",
|
||||||
"se_calibration.ogg",
|
"se_calibration.ogg",
|
||||||
|
|
||||||
"v_results.ogg",
|
"v_results.ogg",
|
||||||
"v_sanka.ogg",
|
"v_sanka.ogg",
|
||||||
"v_songsel.ogg",
|
"v_songsel.ogg",
|
||||||
@@ -112,14 +113,14 @@ var assets = {
|
|||||||
"se_don.ogg",
|
"se_don.ogg",
|
||||||
"se_ka.ogg",
|
"se_ka.ogg",
|
||||||
"se_jump.ogg",
|
"se_jump.ogg",
|
||||||
|
|
||||||
"se_balloon.ogg",
|
"se_balloon.ogg",
|
||||||
"se_gameclear.ogg",
|
"se_gameclear.ogg",
|
||||||
"se_gamefail.ogg",
|
"se_gamefail.ogg",
|
||||||
"se_gamefullcombo.ogg",
|
"se_gamefullcombo.ogg",
|
||||||
"se_results_countup.ogg",
|
"se_results_countup.ogg",
|
||||||
"se_results_crown.ogg",
|
"se_results_crown.ogg",
|
||||||
|
|
||||||
"v_fullcombo.ogg",
|
"v_fullcombo.ogg",
|
||||||
"v_renda.ogg",
|
"v_renda.ogg",
|
||||||
"v_results_fullcombo.ogg",
|
"v_results_fullcombo.ogg",
|
||||||
@@ -153,7 +154,7 @@ var assets = {
|
|||||||
"customsongs.html",
|
"customsongs.html",
|
||||||
"search.html"
|
"search.html"
|
||||||
],
|
],
|
||||||
|
|
||||||
"songs": [],
|
"songs": [],
|
||||||
"sounds": {},
|
"sounds": {},
|
||||||
"image": {},
|
"image": {},
|
||||||
|
|||||||
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.errorMessages = []
|
||||||
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
|
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
|
||||||
|
|
||||||
|
this.initWorkers()
|
||||||
|
|
||||||
var promises = []
|
var promises = []
|
||||||
|
|
||||||
promises.push(this.ajax("src/views/loader.html").then(page => {
|
promises.push(this.ajax("src/views/loader.html").then(page => {
|
||||||
@@ -23,6 +25,78 @@ class Loader{
|
|||||||
|
|
||||||
Promise.all(promises).then(this.run.bind(this))
|
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(){
|
run(){
|
||||||
this.promises = []
|
this.promises = []
|
||||||
this.loaderDiv = document.querySelector("#loader")
|
this.loaderDiv = document.querySelector("#loader")
|
||||||
@@ -538,6 +612,17 @@ class Loader{
|
|||||||
return css.join("\n")
|
return css.join("\n")
|
||||||
}
|
}
|
||||||
ajax(url, customRequest, customResponse){
|
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()
|
var request = new XMLHttpRequest()
|
||||||
request.open("GET", url)
|
request.open("GET", url)
|
||||||
var promise = pageEvents.load(request)
|
var promise = pageEvents.load(request)
|
||||||
@@ -557,12 +642,13 @@ class Loader{
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
loadScript(url){
|
loadScript(url){
|
||||||
var script = document.createElement("script")
|
|
||||||
var url = url + this.queryString
|
var url = url + this.queryString
|
||||||
var promise = pageEvents.load(script)
|
return this.workerFetch(url, "text").then(code => {
|
||||||
script.src = url
|
var script = document.createElement("script")
|
||||||
document.head.appendChild(script)
|
code += "\n//# sourceURL=" + url
|
||||||
return promise
|
script.text = code
|
||||||
|
document.head.appendChild(script)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
getCsrfToken(){
|
getCsrfToken(){
|
||||||
return this.ajax("api/csrftoken").then(response => {
|
return this.ajax("api/csrftoken").then(response => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ var translations = {
|
|||||||
tw: "zh-Hant",
|
tw: "zh-Hant",
|
||||||
ko: "ko"
|
ko: "ko"
|
||||||
},
|
},
|
||||||
|
|
||||||
taikoWeb: {
|
taikoWeb: {
|
||||||
ja: "たいこウェブ",
|
ja: "たいこウェブ",
|
||||||
en: "Taiko Web",
|
en: "Taiko Web",
|
||||||
@@ -438,7 +438,78 @@ var translations = {
|
|||||||
tw: "連打數",
|
tw: "連打數",
|
||||||
ko: "연타 횟수"
|
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: {
|
errorOccured: {
|
||||||
ja: "エラーが発生しました。再読み込みしてください。",
|
ja: "エラーが発生しました。再読み込みしてください。",
|
||||||
en: "An error occurred, please refresh",
|
en: "An error occurred, please refresh",
|
||||||
@@ -879,7 +950,7 @@ var translations = {
|
|||||||
en: "Audio Latency Calibration",
|
en: "Audio Latency Calibration",
|
||||||
tw: "聲音延遲校正",
|
tw: "聲音延遲校正",
|
||||||
ko: "오디오 레이턴시 조절"
|
ko: "오디오 레이턴시 조절"
|
||||||
|
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
ja: "背景で鳴っている音を聴いてみましょう。\n\n音が聞こえたら、太鼓の面(%sまたは%s)をたたこう!",
|
ja: "背景で鳴っている音を聴いてみましょう。\n\n音が聞こえたら、太鼓の面(%sまたは%s)をたたこう!",
|
||||||
@@ -1472,26 +1543,26 @@ var translations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var allStrings = {}
|
var allStrings = {}
|
||||||
function separateStrings(){
|
function separateStrings() {
|
||||||
for(var j in languageList){
|
for (var j in languageList) {
|
||||||
var lang = languageList[j]
|
var lang = languageList[j]
|
||||||
allStrings[lang] = {
|
allStrings[lang] = {
|
||||||
id: lang
|
id: lang
|
||||||
}
|
}
|
||||||
var str = allStrings[lang]
|
var str = allStrings[lang]
|
||||||
var translateObj = function(obj, name, str){
|
var translateObj = function (obj, name, str) {
|
||||||
if("en" in obj){
|
if ("en" in obj) {
|
||||||
for(var i in obj){
|
for (var i in obj) {
|
||||||
str[name] = obj[lang] || obj.en
|
str[name] = obj[lang] || obj.en
|
||||||
}
|
}
|
||||||
}else if(obj){
|
} else if (obj) {
|
||||||
str[name] = {}
|
str[name] = {}
|
||||||
for(var i in obj){
|
for (var i in obj) {
|
||||||
translateObj(obj[i], i, str[name])
|
translateObj(obj[i], i, str[name])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(var i in translations){
|
for (var i in translations) {
|
||||||
translateObj(translations[i], i, str)
|
translateObj(translations[i], i, str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
schema.py
26
schema.py
@@ -80,3 +80,29 @@ scores_save = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaderboard_submit = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'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': ['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']
|
||||||
|
}
|
||||||
|
|||||||
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