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>
@@ -101,7 +101,12 @@ export default async function TopicPage({
|
|||||||
<span className="username">{author?.name ?? `#${post.poster}`}</span>
|
<span className="username">{author?.name ?? `#${post.poster}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`user_avatar${isStaff ? ' isStaff' : ''}`}>
|
<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>
|
||||||
<div className="user_info">
|
<div className="user_info">
|
||||||
<h3 className="post_field">
|
<h3 className="post_field">
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ export interface PosterInfo {
|
|||||||
isStaff: boolean
|
isStaff: boolean
|
||||||
joindate: string | null
|
joindate: string | null
|
||||||
postCount: number
|
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). */
|
/** 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
|
if (unique.length === 0) return map
|
||||||
|
|
||||||
const placeholders = unique.map(() => '?').join(',')
|
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 {
|
try {
|
||||||
const [accs] = await db(DB.auth).query<RowDataPacket[]>(
|
const [accs] = await db(DB.auth).query<RowDataPacket[]>(
|
||||||
`SELECT id, username, joindate FROM account WHERE id IN (${placeholders})`,
|
`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,
|
isStaff: gmlevel >= STAFF_GMLEVEL,
|
||||||
joindate: a.joindate ? new Date(a.joindate).toISOString() : null,
|
joindate: a.joindate ? new Date(a.joindate).toISOString() : null,
|
||||||
postCount: 0,
|
postCount: 0,
|
||||||
|
avatar: avatarForClass(classByAccount.get(Number(a.id))),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -391,7 +419,15 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
|
|||||||
// Cualquier id no resuelto (cuenta borrada) recibe un placeholder.
|
// Cualquier id no resuelto (cuenta borrada) recibe un placeholder.
|
||||||
for (const id of unique) {
|
for (const id of unique) {
|
||||||
if (!map.has(id)) {
|
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
|
return map
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |