From 5d724afb8d2908e02a22d349d0c3bc935a99e6a7 Mon Sep 17 00:00:00 2001 From: adevopg Date: Mon, 13 Jul 2026 12:53:50 +0000 Subject: [PATCH] =?UTF-8?q?web-next:=20p=C3=A1gina=20de=20jugadores=20/-players=20con=20datos=20reales=20de=20la=20BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extiende la ruta dinámica [realm] para servir también /-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 (/-players), como REINO. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-next/app/[locale]/[realm]/page.tsx | 54 ++++----- web-next/components/Header.tsx | 8 +- web-next/components/PlayersBoard.tsx | 146 +++++++++++++++++++++++++ web-next/components/RealmInfo.tsx | 29 +++++ web-next/components/SiteHeader.tsx | 4 +- web-next/lib/players.ts | 139 +++++++++++++++++++++++ 6 files changed, 343 insertions(+), 37 deletions(-) create mode 100644 web-next/components/PlayersBoard.tsx create mode 100644 web-next/components/RealmInfo.tsx create mode 100644 web-next/lib/players.ts diff --git a/web-next/app/[locale]/[realm]/page.tsx b/web-next/app/[locale]/[realm]/page.tsx index 302bd34..2dc3592 100644 --- a/web-next/app/[locale]/[realm]/page.tsx +++ b/web-next/app/[locale]/[realm]/page.tsx @@ -2,14 +2,22 @@ import type { Metadata } from 'next' import { setRequestLocale } from 'next-intl/server' import { notFound } from 'next/navigation' import { getRealmName, realmSlug } from '@/lib/realm' +import { RealmInfo } from '@/components/RealmInfo' +import { PlayersBoard } from '@/components/PlayersBoard' export const dynamic = 'force-dynamic' -// Ruta dinámica del reino: /-realm, donde viene del nombre del -// reino en acore_auth.realmlist (p. ej. «Trinity» → /trinity-realm). -async function resolveRealm(param: string): Promise { +type Kind = 'realm' | 'players' + +// Rutas dinámicas del reino (el slug viene de acore_auth.realmlist): +// /-realm → información del reino (Rates/Horarios) +// /-players → estadísticas de personajes (datos reales de la BD) +async function resolve(param: string): Promise<{ name: string; kind: Kind } | null> { const name = await getRealmName() - return param === `${realmSlug(name)}-realm` ? name : null + const slug = realmSlug(name) + if (param === `${slug}-realm`) return { name, kind: 'realm' } + if (param === `${slug}-players`) return { name, kind: 'players' } + return null } export async function generateMetadata({ @@ -18,8 +26,9 @@ export async function generateMetadata({ params: Promise<{ realm: string }> }): Promise { const { realm } = await params - const name = await resolveRealm(realm) - return name ? { title: `Reino - ${name}` } : {} + const r = await resolve(realm) + if (!r) return {} + return { title: `${r.kind === 'players' ? 'Personajes' : 'Reino'} - ${r.name}` } } export default async function RealmPage({ @@ -29,40 +38,19 @@ export default async function RealmPage({ }) { const { locale, realm } = await params setRequestLocale(locale) - const name = await resolveRealm(realm) - if (!name) notFound() + const r = await resolve(realm) + if (!r) notFound() + + const heading = r.kind === 'players' ? 'Personajes' : 'Reino' return (
-

Reino - {name.toUpperCase()}

-
-
-
-

Rates

-

Experiencia: x8 modificable con .xp (Con Recluta a un Amigo x16)

-

Drop de objetos verdes y azules: x4

-

Drop de oro en NPCs: x2

-

Drop de oro en misiones: x1

-

Habilidades: x3

-

Profesiones: x3

-

Reputaciones: x3 (Bonus con Recluta a un amigo)

-

Honor: x3

-
-
-

Horarios

-

Los siguientes horarios son basados en la hora del reino.

-
-

Misiones diarias: 7 am

-

Reinicio de mazmorras diarias: 9 am

-

Reinicio de bandas: 9 am (Miércoles)

-

Campos de Batalla: 10 am

-

Reparto de Puntos de Arena: 11 am (Viernes)

-
-
+

{heading} - {r.name.toUpperCase()}

+ {r.kind === 'realm' ? : }
diff --git a/web-next/components/Header.tsx b/web-next/components/Header.tsx index e949803..6470485 100644 --- a/web-next/components/Header.tsx +++ b/web-next/components/Header.tsx @@ -7,8 +7,10 @@ export async function Header() { const loggedIn = Boolean(session.username) // Django muestra bnet_email si existe, si no el username. const accountLabel = session.bnetEmail || session.username || '' - // Enlace al reino según el nombre en realmlist (p. ej. /trinity-realm). - const realmHref = `/${realmSlug(await getRealmName())}-realm` + // Enlaces al reino/jugadores según el nombre en realmlist (p. ej. /trinity-realm, /trinity-players). + const slug = realmSlug(await getRealmName()) + const realmHref = `/${slug}-realm` + const playersHref = `/${slug}-players` - return + return } diff --git a/web-next/components/PlayersBoard.tsx b/web-next/components/PlayersBoard.tsx new file mode 100644 index 0000000..ca2e078 --- /dev/null +++ b/web-next/components/PlayersBoard.tsx @@ -0,0 +1,146 @@ +import { + getPlayersData, + CLASS_INFO, + raceBig, + classMedium, + classChest, + ALLIANCE_ICON, + HORDE_ICON, +} from '@/lib/players' + +/* eslint-disable @next/next/no-img-element */ + +export async function PlayersBoard() { + const data = await getPlayersData() + + return ( + <> +
+
+

La información de esta página se actualiza 1 vez al día automáticamente

+
+
+ + {/* Personajes creados por clase */} +
+

Personajes creados por clase

+
+
+ {data.classStats.map((s) => ( +
+ + + + + + + + +
{CLASS_INFO[s.id].label}
Total: {s.total}

Alianza Alianzas: {s.alliance}
Horda Hordas: {s.horde}
+
+ ))} + + + + + + + + +
Personajes totales: {data.totals.total}Personajes alianzas: {data.totals.alliance}Personajes hordas: {data.totals.horde}
+
+
+
+ +


+ + {/* Primeros del Reino - Nivel 80 */} +
+

Primeros del Reino - Nivel 80

+
+ {data.firsts.length === 0 ? ( +

Aún no hay personajes de nivel 80 registrados.

+ ) : ( + + + + + + + + {data.firsts.map((f, i) => ( + + + + + + ))} + +
LogroPersonajeFecha
+ {' '} + ¡Primero del reino! {f.label} + {f.name}{f.date}
+ )} +
+
+ +


+ + {/* Top de logros */} +
+

Top de logros

+
+

Los 10 jugadores con más logros

+ + + + + + + + + {data.topAch.map((r, i) => ( + + + + + + + ))} + +
NombreRazaClaseLogros
{r.name}{r.value}
+
+
+ +


+ + {/* Top de muertes con honor */} +
+

Top de muertes con honor

+
+

Los 10 jugadores con más muertes con honor

+ + + + + + + + + + {data.topHonor.map((r, i) => ( + + + + + + + + ))} + +
NombreRazaClaseTotal de muertesTotal de muertes hoy
{r.name}{r.total}{r.today}
+
+
+ + ) +} diff --git a/web-next/components/RealmInfo.tsx b/web-next/components/RealmInfo.tsx new file mode 100644 index 0000000..1d004ec --- /dev/null +++ b/web-next/components/RealmInfo.tsx @@ -0,0 +1,29 @@ +/** Información del reino (Rates + Horarios) — contenido de /-realm. */ +export function RealmInfo() { + return ( +
+
+

Rates

+

Experiencia: x8 modificable con .xp (Con Recluta a un Amigo x16)

+

Drop de objetos verdes y azules: x4

+

Drop de oro en NPCs: x2

+

Drop de oro en misiones: x1

+

Habilidades: x3

+

Profesiones: x3

+

Reputaciones: x3 (Bonus con Recluta a un amigo)

+

Honor: x3

+
+
+

Horarios

+

Los siguientes horarios son basados en la hora del reino.

+
+

Misiones diarias: 7 am

+

Reinicio de mazmorras diarias: 9 am

+

Reinicio de bandas: 9 am (Miércoles)

+

Campos de Batalla: 10 am

+

Reparto de Puntos de Arena: 11 am (Viernes)

+
+
+
+ ) +} diff --git a/web-next/components/SiteHeader.tsx b/web-next/components/SiteHeader.tsx index 4939b88..8731d2e 100644 --- a/web-next/components/SiteHeader.tsx +++ b/web-next/components/SiteHeader.tsx @@ -17,10 +17,12 @@ export function SiteHeader({ loggedIn, accountLabel, realmHref, + playersHref, }: { loggedIn: boolean accountLabel: string realmHref: string + playersHref: string }) { const router = useRouter() const [open, setOpen] = useState(false) @@ -89,7 +91,7 @@ export function SiteHeader({

{loggedIn && (

- JUGADORES + JUGADORES

)} diff --git a/web-next/lib/players.ts b/web-next/lib/players.ts new file mode 100644 index 0000000..201cbfc --- /dev/null +++ b/web-next/lib/players.ts @@ -0,0 +1,139 @@ +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 = { + 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 = { + 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 { + 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('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( + `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( + `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( + '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 } +}