177 lines
4.4 KiB
JavaScript
177 lines
4.4 KiB
JavaScript
const express = require("express");
|
|
const { pool } = require("../db");
|
|
const { GAMEMODES, avatarUrl, normalizeGamemode } = require("../config");
|
|
const { ValidationError, validateRequest } = require("../middleware/validation");
|
|
const { sendError } = require("../utils/http");
|
|
|
|
const router = express.Router();
|
|
const PER_PAGE = 20;
|
|
|
|
const TIER_POINTS_SQL = `
|
|
CASE pr.tier
|
|
WHEN 'HT1' THEN 60
|
|
WHEN 'LT1' THEN 45
|
|
WHEN 'HT2' THEN 30
|
|
WHEN 'LT2' THEN 20
|
|
WHEN 'HT3' THEN 10
|
|
WHEN 'LT3' THEN 6
|
|
WHEN 'HT4' THEN 4
|
|
WHEN 'LT4' THEN 3
|
|
WHEN 'HT5' THEN 2
|
|
WHEN 'LT5' THEN 1
|
|
ELSE 0
|
|
END`;
|
|
|
|
const GM_POINTS_SQL = `
|
|
CASE gm.tier
|
|
WHEN 'HT1' THEN 60
|
|
WHEN 'LT1' THEN 45
|
|
WHEN 'HT2' THEN 30
|
|
WHEN 'LT2' THEN 20
|
|
WHEN 'HT3' THEN 10
|
|
WHEN 'LT3' THEN 6
|
|
WHEN 'HT4' THEN 4
|
|
WHEN 'LT4' THEN 3
|
|
WHEN 'HT5' THEN 2
|
|
WHEN 'LT5' THEN 1
|
|
ELSE 0
|
|
END`;
|
|
|
|
const schema = {
|
|
params: {
|
|
fields: {
|
|
gamemode: {
|
|
type: "string",
|
|
required: true,
|
|
minLength: 3,
|
|
maxLength: 32,
|
|
toLowerCase: true,
|
|
},
|
|
},
|
|
allowUnknown: false,
|
|
},
|
|
query: {
|
|
fields: {
|
|
page: { type: "int", min: 1, max: 50000 },
|
|
perPage: { type: "int", min: 1, max: 500 },
|
|
},
|
|
allowUnknown: false,
|
|
},
|
|
};
|
|
|
|
function validateOrReply(req, res) {
|
|
try {
|
|
return validateRequest(req, schema);
|
|
} catch (error) {
|
|
if (error instanceof ValidationError) {
|
|
sendError(res, 400, "VALIDATION_ERROR", error.message, error.details);
|
|
return null;
|
|
}
|
|
|
|
sendError(res, 400, "VALIDATION_ERROR", "Invalid request payload.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function buildDefaultTiers() {
|
|
return Object.fromEntries(GAMEMODES.map((mode) => [mode, null]));
|
|
}
|
|
|
|
router.get("/rankings/:gamemode", async (req, res) => {
|
|
const validated = validateOrReply(req, res);
|
|
if (!validated) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const gamemode = normalizeGamemode(validated.params.gamemode);
|
|
const page = validated.query.page || 1;
|
|
const perPage = validated.query.perPage || PER_PAGE;
|
|
|
|
if (!GAMEMODES.includes(gamemode)) {
|
|
return sendError(res, 400, "VALIDATION_ERROR", "Invalid gamemode");
|
|
}
|
|
|
|
const [countRows] = await pool.query(
|
|
"SELECT COUNT(*) AS total FROM player_ranks WHERE gamemode = ?",
|
|
[gamemode]
|
|
);
|
|
|
|
const total = Number(countRows[0]?.total || 0);
|
|
const totalPages = Math.max(Math.ceil(total / perPage), 1);
|
|
const start = (page - 1) * perPage;
|
|
|
|
if (total === 0) {
|
|
return res.json({
|
|
gamemode,
|
|
data: [],
|
|
pagination: { page, perPage, total, totalPages },
|
|
});
|
|
}
|
|
|
|
const [pageRows] = await pool.query(
|
|
`SELECT
|
|
p.id,
|
|
p.username,
|
|
p.region,
|
|
gm.tier AS gm_tier,
|
|
COALESCE(SUM(${TIER_POINTS_SQL}), 0) AS total_points,
|
|
${GM_POINTS_SQL} AS gamemode_points
|
|
FROM players p
|
|
JOIN player_ranks gm ON gm.player_id = p.id AND gm.gamemode = ?
|
|
LEFT JOIN player_ranks pr ON pr.player_id = p.id
|
|
GROUP BY p.id, p.username, p.region, gm.tier
|
|
ORDER BY gamemode_points DESC, total_points DESC, p.username ASC
|
|
LIMIT ? OFFSET ?`,
|
|
[gamemode, perPage, start]
|
|
);
|
|
|
|
const playerIds = pageRows.map((row) => row.id);
|
|
const [allTierRows] = playerIds.length
|
|
? await pool.query(
|
|
"SELECT player_id, gamemode, tier FROM player_ranks WHERE player_id IN (?)",
|
|
[playerIds]
|
|
)
|
|
: [[]];
|
|
|
|
const byId = new Map();
|
|
for (let i = 0; i < pageRows.length; i += 1) {
|
|
const row = pageRows[i];
|
|
byId.set(row.id, {
|
|
id: row.id,
|
|
username: row.username,
|
|
region: row.region,
|
|
avatarUrl: avatarUrl(row.username),
|
|
tiers: buildDefaultTiers(),
|
|
totalPoints: Number(row.total_points || 0),
|
|
gamemodePoints: Number(row.gamemode_points || 0),
|
|
position: start + i + 1,
|
|
});
|
|
}
|
|
|
|
for (const row of allTierRows) {
|
|
const target = byId.get(row.player_id);
|
|
if (!target) {
|
|
continue;
|
|
}
|
|
target.tiers[row.gamemode] = row.tier;
|
|
}
|
|
|
|
return res.json({
|
|
gamemode,
|
|
data: Array.from(byId.values()),
|
|
pagination: {
|
|
page,
|
|
perPage,
|
|
total,
|
|
totalPages,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("GET /api/rankings/:gamemode failed:", error?.message || error);
|
|
return sendError(res, 500, "RANKINGS_FETCH_FAILED", "Failed to fetch rankings");
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|