4 Commits
F-B ... Bang2

15 changed files with 613 additions and 868 deletions

View File

@@ -1,20 +0,0 @@
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`.

210
app.py
View File

@@ -13,7 +13,6 @@ import requests
import schema
import os
import time
from datetime import datetime
# -- カスタム --
import traceback
@@ -106,13 +105,9 @@ 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.leaderboard.create_index([
('song_id', 1),
('difficulty', 1),
('month', 1),
('score', -1)
])
db.leaderboard.create_index('username')
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('month', 1), ('score_value', -1)])
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('username', 1), ('month', 1)], unique=True)
db.leaderboards.create_index('month')
class HashException(Exception):
@@ -763,122 +758,147 @@ 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')
current_month = datetime.now().strftime('%Y-%m')
song_id = data.get('song_id')
difficulty = data.get('difficulty')
score_data = data.get('score')
# Handle song_id type
song_id = data['song_id']
# Validate difficulty
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
if difficulty not in valid_difficulties:
return api_error('invalid_difficulty')
query = {
# Get current month (YYYY-MM format)
current_month = time.strftime('%Y-%m', time.gmtime())
# Check if user already has a record for this song/difficulty/month
existing = db.leaderboards.find_one({
'song_id': song_id,
'difficulty': data['difficulty'],
'month': current_month,
'username': username
}
'difficulty': difficulty,
'username': username,
'month': current_month
})
existing = db.leaderboard.find_one(query)
# 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
new_record = False
rank_info = None
score_value = int(score_obj.get('score', 0))
except:
return api_error('invalid_score_format')
if not existing or data['score'] > existing['score']:
entry = {
if existing:
# Update only if new score is higher
existing_score = existing.get('score_value', 0)
if score_value > existing_score:
db.leaderboards.update_one(
{'_id': existing['_id']},
{'$set': {
'score': score_data,
'score_value': score_value,
'display_name': user['display_name'],
'submitted_at': time.time()
}}
)
return jsonify({'status': 'ok', 'message': 'score_updated'})
else:
return jsonify({'status': 'ok', 'message': 'score_not_higher'})
else:
# Check if this score would be in top 50
count = db.leaderboards.count_documents({
'song_id': song_id,
'difficulty': data['difficulty'],
'month': current_month,
'difficulty': difficulty,
'month': current_month
})
if count >= 50:
# Find the 50th score
leaderboard = list(db.leaderboards.find({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
}).sort('score_value', -1).limit(50))
if len(leaderboard) >= 50:
last_score = leaderboard[49].get('score_value', 0)
if score_value <= last_score:
return jsonify({'status': 'ok', 'message': 'score_too_low'})
# Insert new record
db.leaderboards.insert_one({
'song_id': song_id,
'difficulty': difficulty,
'username': username,
'display_name': user['display_name'],
'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
'score': score_data,
'score_value': score_value,
'submitted_at': time.time(),
'month': current_month
})
# Get rank
count = db.leaderboard.count_documents({
# 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': data['difficulty'],
'month': current_month,
'score': {'$gt': data['score']}
})
rank = count + 1
rank_info = rank
'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')
difficulty = request.args.get('difficulty')
song_id = request.args.get('song_id', None)
difficulty = request.args.get('difficulty', None)
if not song_id or not difficulty:
return abort(400)
# Try convert song_id to int if it looks like one
if re.match('^[0-9]+$', str(song_id)):
try:
song_id = int(song_id)
except:
return abort(400)
current_month = datetime.now().strftime('%Y-%m')
# Validate difficulty
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
if difficulty not in valid_difficulties:
return abort(400)
query = {
# Get current month
current_month = time.strftime('%Y-%m', time.gmtime())
# Get top 50 scores
leaderboard = list(db.leaderboards.find({
'song_id': song_id,
'difficulty': difficulty,
'month': current_month
}
}, {
'_id': False,
'username': True,
'display_name': True,
'score': True,
'score_value': True,
'submitted_at': True
}).sort('score_value', -1).limit(50))
# Get top 50
leaderboard = list(db.leaderboard.find(query, {'_id': False}).sort('score', -1).limit(50))
# Add rank to each entry
for i, entry in enumerate(leaderboard):
entry['rank'] = i + 1
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
})
return jsonify({'status': 'ok', 'leaderboard': leaderboard, 'month': current_month})
@app.route(basedir + 'privacy')

View File

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

View File

@@ -6,7 +6,6 @@ var assets = {
"parseosu.js",
"titlescreen.js",
"scoresheet.js",
"leaderboard_ui.js",
"songselect.js",
"keyboard.js",
"gameinput.js",

View File

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

View File

@@ -1,286 +0,0 @@
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))
}
}

View File

@@ -1,26 +0,0 @@
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()
})
}
})

View File

@@ -11,8 +11,6 @@ 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 => {
@@ -25,78 +23,6 @@ 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")
@@ -612,17 +538,6 @@ 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)
@@ -642,13 +557,12 @@ class Loader{
return promise
}
loadScript(url){
var url = url + this.queryString
return this.workerFetch(url, "text").then(code => {
var script = document.createElement("script")
code += "\n//# sourceURL=" + url
script.text = code
var url = url + this.queryString
var promise = pageEvents.load(script)
script.src = url
document.head.appendChild(script)
})
return promise
}
getCsrfToken(){
return this.ajax("api/csrftoken").then(response => {

View File

@@ -849,29 +849,6 @@ 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
@@ -949,6 +926,7 @@ class Scoresheet {
var title = this.controller.selectedSong.originalTitle
var hash = this.controller.selectedSong.hash
var difficulty = this.resultsObj.difficulty
var songId = this.controller.selectedSong.id
var oldScore = scoreStorage.get(hash, difficulty, true)
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
var crown = ""
@@ -963,7 +941,10 @@ class Scoresheet {
delete this.resultsObj.title
delete this.resultsObj.difficulty
delete this.resultsObj.gauge
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).then(() => {
// Auto-submit to leaderboard if logged in and has song ID
this.submitToLeaderboard(songId, difficulty, this.resultsObj)
}).catch(() => {
this.showWarning = { name: "scoreSaveFailed" }
})
} else if (oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)) {
@@ -973,53 +954,28 @@ class Scoresheet {
})
}
}
if (!this.controller.autoPlayEnabled && account.loggedIn && !this.multiplayer) {
this.submitToLeaderboard()
}
this.scoreSaved = true
}
submitToLeaderboard() {
if (!this.resultsObj || !this.controller.selectedSong) {
submitToLeaderboard(songId, difficulty, scoreObj) {
// Only submit if user is logged in and song has valid ID
if (!account.loggedIn || !songId) {
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
}
loader.getCsrfToken().then(token => {
var request = new XMLHttpRequest()
request.open("POST", gameConfig.basedir + "api/leaderboard/submit")
pageEvents.load(request).then(() => {
if (request.status === 200) {
try {
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("Leaderboard response parse error:", e)
}
}
}).catch(e => {
console.error("Leaderboard submit failed:", e)
})
request.open("POST", "api/leaderboard/submit")
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)
request.send(JSON.stringify({
song_id: songId,
difficulty: difficulty,
score: scoreObj
}))
}).catch(() => {
// Silently fail - leaderboard submission is optional
console.log("Leaderboard submission failed")
})
}

View File

@@ -119,7 +119,6 @@ class SongSelect {
this.font = strings.font
this.search = new Search(this)
this.leaderboard = new LeaderboardUI(this)
this.songs = []
for (let song of assets.songs) {
@@ -299,12 +298,6 @@ class SongSelect {
iconName: "back",
iconFill: "#f7d39c",
letterSpacing: 4
}, {
text: strings.leaderboard,
fill: "#42c0d2",
iconName: "crown",
iconFill: "#8ee7f2",
letterSpacing: 0
}, {
text: strings.songOptions,
fill: "#b2e442",
@@ -317,6 +310,12 @@ 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]
@@ -591,13 +590,15 @@ class SongSelect {
if (this.selectedDiff === 0) {
this.toSongSelect()
} else if (this.selectedDiff === 1) {
this.toLeaderboard()
} else if (this.selectedDiff === 3) {
this.toOptions(1)
} else if (this.selectedDiff === 2) {
this.toDownload()
} else if (this.selectedDiff === 3) {
this.toLeaderboard()
} else if (this.selectedDiff === 4) {
this.toDelete()
} else if (this.selectedDiff === 2) {
this.toOptions(1)
} else if (false) {
// Moved above
} else {
this.toLoadSong(this.selectedDiff - this.diffOptions.length, shift, ctrl)
}
@@ -650,14 +651,6 @@ 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
@@ -706,13 +699,13 @@ class SongSelect {
this.selectedDiff = 0
this.toSongSelect()
} else if (moveBy === 1) {
this.toLeaderboard()
} else if (moveBy === 3) {
this.toOptions(1)
} else if (moveBy === 2) {
this.toDownload()
} else if (moveBy === 3) {
this.toLeaderboard()
} 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) {
@@ -2721,10 +2714,6 @@ 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"
@@ -3048,99 +3037,6 @@ 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
@@ -3262,13 +3158,6 @@ 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()
@@ -3445,4 +3334,19 @@ class SongSelect {
alert(text);
});
}
toLeaderboard() {
if (!this.songs[this.selectedSong].id) {
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(this.songs[this.selectedSong].id, diffId)
}
}

View File

@@ -439,77 +439,6 @@ 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",

View File

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

View File

@@ -85,24 +85,9 @@ 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'}
'song_id': {'type': 'number'},
'difficulty': {'type': 'string'},
'score': {'type': 'object'}
},
'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']
'required': ['song_id', 'difficulty', 'score']
}

View File

@@ -1,12 +1,15 @@
<!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="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="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="robots" content="notranslate">
<meta name="robots" content="noimageindex">
<meta name="color-scheme" content="only light">
@@ -24,14 +27,17 @@
<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.0.0">vLightNova 1.0.0</a>
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link"
class="stroke-sub" alt="vLightNova 1.1.0">vLightNova 1.1.0</a>
{% else %}
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.1.0">vLightNova
1.1.0</a>
{% endif %}
</div>
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
@@ -43,4 +49,5 @@
</div>
</noscript>
</body>
</html>

View File

@@ -1,43 +0,0 @@
#!/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()