5d724afb8d
Extiende la ruta dinámica [realm] para servir también /<slug>-players (slug del reino en realmlist). PlayersBoard (server) consulta acore_characters: - Personajes creados por clase (conteo por clase y facción) + totales. - Top de muertes con honor (characters.totalKills/todayKills, LIMIT 10). - Top de logros (nº de logros por personaje vía character_achievement; los «puntos» no existen en la BD del servidor, vienen del DBC del cliente). - Primeros del reino - Nivel 80 (primer personaje por fecha del logro 20 = Nivel 80, overall/por clase/por raza; vacío si aún no hay nivel 80). Imágenes reales del tema (nw-classes/nw-races/nw-icons). lib/players.ts con los mapas de clase/raza/facción. RealmInfo extraído a su componente. Enlace JUGADORES de la cabecera ahora dinámico (/<slug>-players), como REINO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
6.5 KiB
TypeScript
140 lines
6.5 KiB
TypeScript
import type { RowDataPacket } from 'mysql2'
|
|
import { db, DB } from './db'
|
|
|
|
const IMG = '/nw-themes/nw-ryu/nw-images'
|
|
|
|
// Clases (orden del original) → slug de color + etiqueta.
|
|
export const CLASS_ORDER = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
|
|
export const CLASS_INFO: Record<number, { key: string; label: string }> = {
|
|
1: { key: 'warrior', label: 'Guerrero' },
|
|
2: { key: 'paladin', label: 'Paladín' },
|
|
3: { key: 'hunter', label: 'Cazador' },
|
|
4: { key: 'rogue', label: 'Pícaro' },
|
|
5: { key: 'priest', label: 'Sacerdote' },
|
|
6: { key: 'death-knight', label: 'C. de la M.' },
|
|
7: { key: 'shaman', label: 'Chamán' },
|
|
8: { key: 'mage', label: 'Mago' },
|
|
9: { key: 'warlock', label: 'Brujo' },
|
|
11: { key: 'druid', label: 'Druida' },
|
|
}
|
|
// Razas → slug + etiqueta + facción. Orden del original para «Primeros del reino».
|
|
export const RACE_ORDER = [7, 10, 11, 3, 1, 4, 2, 6, 8, 5]
|
|
export const RACE_INFO: Record<number, { key: string; label: string; faction: 'alliance' | 'horde' }> = {
|
|
1: { key: 'human', label: 'Humano', faction: 'alliance' },
|
|
2: { key: 'orc', label: 'Orco', faction: 'horde' },
|
|
3: { key: 'dwarf', label: 'Enano', faction: 'alliance' },
|
|
4: { key: 'night-elf', label: 'Elfo de la noche', faction: 'alliance' },
|
|
5: { key: 'undead', label: 'No-muerto', faction: 'horde' },
|
|
6: { key: 'tauren', label: 'Tauren', faction: 'horde' },
|
|
7: { key: 'gnome', label: 'Gnomo', faction: 'alliance' },
|
|
8: { key: 'troll', label: 'Trol', faction: 'horde' },
|
|
10: { key: 'blood-elf', label: 'Elfo de sangre', faction: 'horde' },
|
|
11: { key: 'draenei', label: 'Draenei', faction: 'alliance' },
|
|
}
|
|
|
|
export function classChest(id: number) {
|
|
return `${IMG}/nw-classes/${CLASS_INFO[id]?.key ?? 'warrior'}-chest.webp`
|
|
}
|
|
export function classMedium(id: number) {
|
|
return `${IMG}/nw-classes/${CLASS_INFO[id]?.key ?? 'warrior'}-medium.jpg`
|
|
}
|
|
export function raceBig(race: number, gender: number) {
|
|
return `${IMG}/nw-races/big-${RACE_INFO[race]?.key ?? 'human'}-${gender === 0 ? 'male' : 'female'}.webp`
|
|
}
|
|
export const ALLIANCE_ICON = `${IMG}/nw-icons/alliance-no-border.webp`
|
|
export const HORDE_ICON = `${IMG}/nw-icons/horde-no-border.webp`
|
|
export const ACH80_ICON = `${IMG}/nw-icons/achievement-level-80.jpg`
|
|
|
|
// Logro de «Nivel 80» (secuencia estándar de logros de nivel de WotLK: 6=10 … 20=80).
|
|
const ACH_LEVEL80 = 20
|
|
|
|
export interface ClassStat { id: number; key: string; total: number; alliance: number; horde: number }
|
|
export interface FirstEntry { icon: string; label: string; name: string; classKey: string; date: string }
|
|
export interface AchRow { name: string; race: number; class: number; classKey: string; value: number }
|
|
export interface HonorRow { name: string; race: number; class: number; classKey: string; total: number; today: number }
|
|
|
|
export interface PlayersData {
|
|
classStats: ClassStat[]
|
|
totals: { total: number; alliance: number; horde: number }
|
|
firsts: FirstEntry[]
|
|
topAch: AchRow[]
|
|
topHonor: HonorRow[]
|
|
}
|
|
|
|
function fmtTs(ts: number): string {
|
|
const d = new Date(ts * 1000)
|
|
const p = (n: number) => String(n).padStart(2, '0')
|
|
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())} ${p(d.getDate())}-${p(d.getMonth() + 1)}-${d.getFullYear()}`
|
|
}
|
|
|
|
export async function getPlayersData(): Promise<PlayersData> {
|
|
const c = db(DB.characters)
|
|
|
|
// 1) Personajes creados por clase (y facción).
|
|
const classStats: ClassStat[] = CLASS_ORDER.map((id) => ({ id, key: CLASS_INFO[id].key, total: 0, alliance: 0, horde: 0 }))
|
|
const totals = { total: 0, alliance: 0, horde: 0 }
|
|
try {
|
|
const [rows] = await c.query<RowDataPacket[]>('SELECT class, race, COUNT(*) AS n FROM characters GROUP BY class, race')
|
|
for (const r of rows) {
|
|
const stat = classStats.find((s) => s.id === r.class)
|
|
const n = Number(r.n)
|
|
const faction = RACE_INFO[r.race]?.faction
|
|
totals.total += n
|
|
if (faction === 'alliance') totals.alliance += n
|
|
else if (faction === 'horde') totals.horde += n
|
|
if (stat) {
|
|
stat.total += n
|
|
if (faction === 'alliance') stat.alliance += n
|
|
else if (faction === 'horde') stat.horde += n
|
|
}
|
|
}
|
|
} catch { /* BD de personajes puede no estar poblada */ }
|
|
|
|
// 2) Primeros del reino - Nivel 80 (logro 20, por fecha).
|
|
const firsts: FirstEntry[] = []
|
|
try {
|
|
const [rows] = await c.query<RowDataPacket[]>(
|
|
`SELECT ca.date AS date, ch.name AS name, ch.class AS class, ch.race AS race, ch.gender AS gender
|
|
FROM character_achievement ca JOIN characters ch ON ch.guid = ca.guid
|
|
WHERE ca.achievement = ? ORDER BY ca.date ASC`,
|
|
[ACH_LEVEL80],
|
|
)
|
|
if (rows.length) {
|
|
const overall = rows[0]
|
|
firsts.push({ icon: ACH80_ICON, label: 'Nivel 80', name: overall.name, classKey: CLASS_INFO[overall.class]?.key ?? '', date: fmtTs(Number(overall.date)) })
|
|
// Primero por clase.
|
|
for (const id of CLASS_ORDER) {
|
|
const row = rows.find((r) => r.class === id)
|
|
if (row) firsts.push({ icon: classMedium(id), label: `${CLASS_INFO[id].label} nivel 80`, name: row.name, classKey: CLASS_INFO[id].key, date: fmtTs(Number(row.date)) })
|
|
}
|
|
// Primero por raza.
|
|
for (const race of RACE_ORDER) {
|
|
const row = rows.find((r) => r.race === race)
|
|
if (row) firsts.push({ icon: raceBig(race, Number(row.gender)), label: `${RACE_INFO[race].label} nivel 80`, name: row.name, classKey: CLASS_INFO[row.class]?.key ?? '', date: fmtTs(Number(row.date)) })
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
// 3) Top de logros (nº de logros; los «puntos» no están en la BD del servidor).
|
|
let topAch: AchRow[] = []
|
|
try {
|
|
const [rows] = await c.query<RowDataPacket[]>(
|
|
`SELECT ch.name AS name, ch.race AS race, ch.class AS class, COUNT(*) AS n
|
|
FROM character_achievement ca JOIN characters ch ON ch.guid = ca.guid
|
|
GROUP BY ca.guid, ch.name, ch.race, ch.class ORDER BY n DESC LIMIT 10`,
|
|
)
|
|
topAch = rows.map((r) => ({ name: r.name, race: r.race, class: r.class, classKey: CLASS_INFO[r.class]?.key ?? '', value: Number(r.n) }))
|
|
} catch { /* ignore */ }
|
|
|
|
// 4) Top de muertes con honor.
|
|
let topHonor: HonorRow[] = []
|
|
try {
|
|
const [rows] = await c.query<RowDataPacket[]>(
|
|
'SELECT name, race, class, totalKills, todayKills FROM characters ORDER BY totalKills DESC LIMIT 10',
|
|
)
|
|
topHonor = rows.map((r) => ({ name: r.name, race: r.race, class: r.class, classKey: CLASS_INFO[r.class]?.key ?? '', total: Number(r.totalKills), today: Number(r.todayKills) }))
|
|
} catch { /* ignore */ }
|
|
|
|
return { classStats, totals, firsts, topAch, topHonor }
|
|
}
|