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
+66 -26
View File
@@ -1,39 +1,43 @@
import { wowheadUrl, wowheadData, wowheadIcon } from '@/lib/wowhead'
import { wowheadIcon, wowheadUrl, wowheadData } from '@/lib/wowhead'
export type TalentCell = { tier: number; col: number; rank: number; max: number; spell: number }
export type TalentTreeData = { index: number; name: string; points: number; talents: TalentCell[] }
/**
* Árbol de talentos estilo calculador: 3 árboles, cada uno una rejilla de casillas
* posicionadas por fila (tier) y columna. Cada casilla muestra el icono del talento
* (del hechizo de rango 1) y su rango actual/máximo; las no aprendidas van atenuadas.
* El icono va en un <span> hijo porque wowhead sobrescribe el background del <a>.
*/
export function ArmoryTalents({
trees,
spec,
total,
maxPoints,
locale,
spellIcons,
}: {
trees: TalentTreeData[]
export type GlyphInfo = { name: string; icon: string }
export type TalentGroupView = {
spec: string | null
total: number
maxPoints: number
locale: string
dist: number[]
trees: TalentTreeData[]
glyphs: { major: GlyphInfo[]; minor: GlyphInfo[] }
}
/**
* Renderiza UN grupo de talentos: cabecera de distribución + los 3 árboles (rejilla de
* iconos) + una 4ª columna con los Glifos (mayores/menores). Componente puro sin estado,
* usado por ArmoryTalentTab (que conmuta Primaria/Secundaria).
*/
export function ArmoryTalents({
group,
spellIcons,
locale,
labels,
}: {
group: TalentGroupView
spellIcons: Record<number, string>
locale: string
labels: { distribution: string; groupLabel: string; glyphs: string; majorGlyphs: string; minorGlyphs: string }
}) {
const { trees, glyphs, dist } = group
return (
<div className="armory-talents">
<div className="armory-spec">
<span className="armory-spec-name">{spec}</span>
<span className="armory-spec-dist">{trees.map((tr) => tr.points).join(' / ')}</span>
<span className="armory-spec-total">
{total} / {maxPoints}
</span>
<div className="armory-tdist">
<div className="armory-tdist-label">
{labels.distribution} · {labels.groupLabel.toUpperCase()}
</div>
<div className="armory-tdist-value">{dist.join(' / ')}</div>
</div>
<div className="armory-talent-trees">
<div className="armory-talent-cols">
{trees.map((tree) => {
const maxTier = tree.talents.reduce((m, t) => Math.max(m, t.tier), 0)
return (
@@ -76,6 +80,42 @@ export function ArmoryTalents({
</div>
)
})}
<div className="armory-talent-tree armory-glyph-col">
<div className="armory-talent-tree-head">
<span>{labels.glyphs}</span>
<span className="armory-talent-tree-pts">
{glyphs.major.length} · {glyphs.minor.length}
</span>
</div>
{glyphs.major.length > 0 && (
<div className="armory-glyph-group">
<h3 className="armory-prof-grouptitle">{labels.majorGlyphs}</h3>
{glyphs.major.map((g, i) => (
<div className="armory-glyph" key={i}>
<span className="armory-glyph-icon" style={{ backgroundImage: `url(${wowheadIcon(g.icon, 'small')})` }} />
<span className="armory-glyph-name">{g.name}</span>
</div>
))}
</div>
)}
{glyphs.minor.length > 0 && (
<div className="armory-glyph-group">
<h3 className="armory-prof-grouptitle">{labels.minorGlyphs}</h3>
{glyphs.minor.map((g, i) => (
<div className="armory-glyph" key={i}>
<span className="armory-glyph-icon" style={{ backgroundImage: `url(${wowheadIcon(g.icon, 'small')})` }} />
<span className="armory-glyph-name">{g.name}</span>
</div>
))}
</div>
)}
{glyphs.major.length === 0 && glyphs.minor.length === 0 && (
<p className="armory-hint" style={{ padding: '8px 0' }}>
</p>
)}
</div>
</div>
</div>
)