409 lines
15 KiB
JavaScript
409 lines
15 KiB
JavaScript
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 is a valid number
|
|
if (!this.songId || isNaN(parseInt(this.songId))) {
|
|
console.error("Invalid song ID for leaderboard:", this.songId)
|
|
this.leaderboardData = []
|
|
return
|
|
}
|
|
|
|
try {
|
|
var response = await loader.ajax(
|
|
`${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${parseInt(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"])
|
|
}
|
|
}
|