Armería: PvP campos de batalla, logros por categoría, bandas/mazmorras y página de hermandad

- PvP: pestaña Campos de batalla (pvpstats) con resultado, stats por partida y fecha/hora.
- Logros: rejilla por categoría con anillos de progreso + detalle por categoría
  (datos de Achievement.db2 + Achievement_Category.db2, iconos vía wowhead).
- Bandas: progreso por dificultad (10/25/heroico) con jefes reales de DungeonEncounter.db2
  y tooltip de jefes; agrupado por expansión (Clásico/TBC/WotLK).
- Mazmorras: mismo sistema, Normal + Heroico, Clásico/TBC/WotLK.
- Página de hermandad estilo armería: cabecera (emblema, puntos = unión de logros de
  miembros sin duplicar, nº miembros, fundada), pestañas Hermandad/Logros, roster con
  iconos de clase/raza, columna Función (rol por spec), filtros clase/función,
  orden por rango/puntos/nivel de objeto/nivel/nombre, paginación.
- Barra lateral: mensaje diario con login (iron-session, gated por pertenencia),
  logros recientes y desgloses de clases y funciones.
- Cabecera de personaje: enlace a la hermandad + nombre del reino.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 01:18:30 +00:00
parent 476e5dba87
commit beba7301a1
203 changed files with 31304 additions and 269 deletions
+120
View File
@@ -0,0 +1,120 @@
export type RaidBossState = { name: string; killed: boolean }
export type RaidDifficulty = { diff: number; label: string; killed: number; total: number; bosses: RaidBossState[] }
export type RaidProgressItem = { map: number; name: string; exp: number; total: number; difficulties: RaidDifficulty[] }
/** Gradiente de cabecera por banda (sin arte del cliente). */
const RAID_THEME: Record<number, string> = {
// Classic
409: 'linear-gradient(135deg, #8a3a1a, #2e1208)',
469: 'linear-gradient(135deg, #7a2a2a, #250d0d)',
531: 'linear-gradient(135deg, #8a7a2a, #2a2408)',
509: 'linear-gradient(135deg, #6b7a2a, #202808)',
309: 'linear-gradient(135deg, #2f7a5a, #0d281c)',
// TBC
532: 'linear-gradient(135deg, #4a2f6b, #1a1028)',
565: 'linear-gradient(135deg, #6b3a2f, #281410)',
544: 'linear-gradient(135deg, #7a2f4a, #2a1018)',
548: 'linear-gradient(135deg, #2a6b6b, #0d2828)',
550: 'linear-gradient(135deg, #3a5c8a, #101f2f)',
534: 'linear-gradient(135deg, #2f6b46, #0d281a)',
564: 'linear-gradient(135deg, #5a2f6b, #1c1028)',
580: 'linear-gradient(135deg, #8a7a2f, #2a2410)',
568: 'linear-gradient(135deg, #6b5a2a, #282008)',
// WotLK
249: 'linear-gradient(135deg, #7a2d1a, #3a1710)',
533: 'linear-gradient(135deg, #2f6b46, #10281a)',
616: 'linear-gradient(135deg, #2a5c8a, #10202f)',
615: 'linear-gradient(135deg, #4a2f6b, #1a1028)',
624: 'linear-gradient(135deg, #6b5a2f, #282010)',
603: 'linear-gradient(135deg, #35566b, #101c28)',
649: 'linear-gradient(135deg, #6b4a2f, #281a10)',
631: 'linear-gradient(135deg, #3a6b8a, #0f2230)',
724: 'linear-gradient(135deg, #7a1f2f, #2a0f14)',
}
function barClass(killed: number, total: number) {
if (total > 0 && killed >= total) return 'full'
if (killed > 0) return 'partial'
return 'empty'
}
/** Gradiente determinista por mapId cuando no hay tema fijo (mazmorras). */
function themeFor(map: number) {
if (RAID_THEME[map]) return RAID_THEME[map]
const h = (map * 47) % 360
return `linear-gradient(135deg, hsl(${h} 42% 30%), hsl(${h} 48% 11%))`
}
export function ArmoryRaids({
locale,
raids,
labels,
}: {
locale: string
raids: RaidProgressItem[]
labels: { bosses: string; empty: string; classic: string; tbc: string; wotlk: string }
}) {
if (raids.length === 0) {
return (
<div className="armory-panel armory-raids">
<p className="armory-hint">{labels.empty}</p>
</div>
)
}
const groups: { exp: number; title: string; items: RaidProgressItem[] }[] = [
{ exp: 2, title: labels.wotlk, items: raids.filter((r) => r.exp === 2) },
{ exp: 1, title: labels.tbc, items: raids.filter((r) => r.exp === 1) },
{ exp: 0, title: labels.classic, items: raids.filter((r) => r.exp === 0) },
].filter((g) => g.items.length > 0)
return (
<div className="armory-panel armory-raids">
{groups.map((g) => (
<section className="armory-raid-exp" key={g.exp}>
<h3 className="armory-raid-exp-title">{g.title}</h3>
<div className="armory-raid-grid">
{g.items.map((r) => (
<div className="armory-raid-card" key={r.map}>
<div className="armory-raid-banner" style={{ background: themeFor(r.map) }}>
<span className="armory-raid-name">{r.name}</span>
<span className="armory-raid-count">
{r.total} {labels.bosses}
</span>
</div>
<div className="armory-raid-diffs">
{r.difficulties.map((d) => (
<div className="armory-raid-row" key={d.diff}>
<span className="armory-raid-diff-lbl">{d.label}</span>
<div className={`armory-raid-bar ${barClass(d.killed, d.total)}`}>
<div
className="armory-raid-bar-fill"
style={{ width: `${d.total ? Math.round((d.killed / d.total) * 100) : 0}%` }}
/>
<span className="armory-raid-bar-txt">
{d.killed}/{d.total}
</span>
</div>
<div className="armory-raid-tip" role="tooltip">
<div className="armory-raid-tip-head">
{r.name} · {d.label}
</div>
<ul>
{d.bosses.map((b, i) => (
<li key={i} className={b.killed ? 'killed' : 'alive'}>
<span className="armory-raid-tip-x">{b.killed ? '1' : '0'} ×</span> {b.name}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
))}
</div>
</section>
))}
</div>
)
}