Foro: avatares por clase del personaje principal

Los avatares salían vacíos (el original usaba getAvatar, que no teníamos). Ahora
cada post muestra el avatar de FusionCMS correspondiente a la CLASE del personaje
de más nivel de la cuenta que postea, usando datos reales de acore_characters.

- Se descargan 10 avatares (uno por clase jugable en WotLK 3.4.3: 1..9 y 11) a
  public/forum/avatars/class-<id>.gif.
- resolvePosters() consulta la clase del personaje top de cada cuenta y mapea a su
  avatar; sin personaje (o acore_characters vacía) cae a guerrero por defecto.
- La página de tema pinta el avatar en el recuadro lateral.

Nota: la tabla characters de AzerothCore usa `guid` como clave (no `id`); ordenar
por `id` lanzaba error y devolvía siempre el avatar por defecto. Corregido.

Verificado en producción: una cuenta con Paladín de nivel máximo muestra class-2.gif.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 12:38:21 +00:00
parent 9b44af9675
commit 18648c6a7e
12 changed files with 43 additions and 2 deletions
@@ -101,7 +101,12 @@ export default async function TopicPage({
<span className="username">{author?.name ?? `#${post.poster}`}</span>
</div>
<div className={`user_avatar${isStaff ? ' isStaff' : ''}`}>
<span />
<span
style={{
background: `url(${author?.avatar ?? '/forum/avatars/class-1.gif'}) no-repeat center`,
backgroundSize: 'cover',
}}
/>
</div>
<div className="user_info">
<h3 className="post_field">
+37 -1
View File
@@ -85,6 +85,16 @@ export interface PosterInfo {
isStaff: boolean
joindate: string | null
postCount: number
avatar: string
}
// Avatar por clase (FusionCMS) del personaje de más nivel de la cuenta. Solo clases
// jugables en WotLK 3.4.3 (1..9 y 11). Sin personaje → guerrero por defecto.
const AVATAR_DIR = '/forum/avatars'
const AVATAR_CLASSES = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 11])
function avatarForClass(classId: number | undefined): string {
const c = classId && AVATAR_CLASSES.has(classId) ? classId : 1
return `${AVATAR_DIR}/class-${c}.gif`
}
/** Nivel GM mínimo para considerarse «staff» en el foro (badge y color). */
@@ -362,6 +372,23 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
if (unique.length === 0) return map
const placeholders = unique.map(() => '?').join(',')
// Clase del personaje de más nivel de cada cuenta (para el avatar). Se tolera que
// acore_characters no exista o esté vacía: el avatar cae a guerrero por defecto.
const classByAccount = new Map<number, number>()
try {
const [chars] = await db(DB.characters).query<RowDataPacket[]>(
`SELECT account, class FROM characters WHERE account IN (${placeholders}) ORDER BY level DESC, guid ASC`,
unique,
)
for (const c of chars) {
const acc = Number(c.account)
if (!classByAccount.has(acc)) classByAccount.set(acc, Number(c.class))
}
} catch {
/* sin personajes: avatar por defecto */
}
try {
const [accs] = await db(DB.auth).query<RowDataPacket[]>(
`SELECT id, username, joindate FROM account WHERE id IN (${placeholders})`,
@@ -383,6 +410,7 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
isStaff: gmlevel >= STAFF_GMLEVEL,
joindate: a.joindate ? new Date(a.joindate).toISOString() : null,
postCount: 0,
avatar: avatarForClass(classByAccount.get(Number(a.id))),
})
}
} catch {
@@ -391,7 +419,15 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
// Cualquier id no resuelto (cuenta borrada) recibe un placeholder.
for (const id of unique) {
if (!map.has(id)) {
map.set(id, { id, name: `#${id}`, gmlevel: 0, isStaff: false, joindate: null, postCount: 0 })
map.set(id, {
id,
name: `#${id}`,
gmlevel: 0,
isStaff: false,
joindate: null,
postCount: 0,
avatar: avatarForClass(classByAccount.get(id)),
})
}
}
return map
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB