Foro: autor con personaje aleatorio de la cuenta

- El nombre del autor muestra un personaje ALEATORIO de la cuenta (no la id).
- Nombre con <Hermandad> en dorado (oculto si el personaje no tiene guild).
- Avatar = imagen de raza del personaje (raza+género).
- Cajas nuevas: puntos de logros, nivel y clase (con su color de clase).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 07:57:41 +00:00
parent beba7301a1
commit 24bd28fcca
93 changed files with 249 additions and 28 deletions
+77 -17
View File
@@ -87,6 +87,23 @@ export interface PosterInfo {
joindate: string | null
postCount: number
avatar: string
charName: string | null
race: number
gender: number
classId: number
level: number
guildName: string | null
achPoints: number
}
// Imagen de raza (CDN wowhead) según raza+género del personaje elegido.
const FORUM_RACE_SLUG: Record<number, string> = {
1: 'human', 2: 'orc', 3: 'dwarf', 4: 'nightelf', 5: 'scourge', 6: 'tauren',
7: 'gnome', 8: 'troll', 9: 'goblin', 10: 'bloodelf', 11: 'draenei', 22: 'worgen',
}
function raceAvatar(race: number, gender: number): string {
const slug = FORUM_RACE_SLUG[race] || 'human'
return `https://wow.zamimg.com/images/wow/icons/large/race_${slug}_${gender === 1 ? 'female' : 'male'}.jpg`
}
// Avatar por clase (FusionCMS) del personaje de más nivel de la cuenta. Solo clases
@@ -383,20 +400,74 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
// 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>()
// Personaje ALEATORIO por cuenta (nombre, raza, género, clase, nivel).
const charByAccount = new Map<number, { guid: number; name: string; race: number; gender: number; class: number; level: 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`,
`SELECT account, guid, name, race, gender, class, level FROM characters WHERE account IN (${placeholders})`,
unique,
)
const byAcc = new Map<number, RowDataPacket[]>()
for (const c of chars) {
const acc = Number(c.account)
if (!classByAccount.has(acc)) classByAccount.set(acc, Number(c.class))
if (!byAcc.has(acc)) byAcc.set(acc, [])
byAcc.get(acc)!.push(c)
}
for (const [acc, list] of byAcc) {
const c = list[Math.floor(Math.random() * list.length)]
charByAccount.set(acc, {
guid: Number(c.guid), name: String(c.name), race: Number(c.race),
gender: Number(c.gender), class: Number(c.class), level: Number(c.level),
})
}
} catch {
/* sin personajes: avatar por defecto */
}
// Hermandad y puntos de logros de los personajes elegidos.
const chosenGuids = [...charByAccount.values()].map((c) => c.guid)
const guildByGuid = new Map<number, string>()
const ptsByGuid = new Map<number, number>()
if (chosenGuids.length) {
const gp = chosenGuids.map(() => '?').join(',')
try {
const [gr] = await db(DB.characters).query<RowDataPacket[]>(
`SELECT gm.guid, g.name FROM guild_member gm JOIN guild g ON g.guildid = gm.guildid WHERE gm.guid IN (${gp})`,
chosenGuids,
)
for (const r of gr) guildByGuid.set(Number(r.guid), String(r.name))
} catch { /* */ }
try {
const [pr] = await db(DB.characters).query<RowDataPacket[]>(
`SELECT ca.guid, COALESCE(SUM(ap.points),0) AS pts
FROM character_achievement ca JOIN acore_world.achievement_points ap ON ap.id = ca.achievement
WHERE ca.guid IN (${gp}) GROUP BY ca.guid`,
chosenGuids,
)
for (const r of pr) ptsByGuid.set(Number(r.guid), Number(r.pts))
} catch { /* */ }
}
const buildPoster = (id: number, base: Partial<PosterInfo>): PosterInfo => {
const ch = charByAccount.get(id)
return {
id,
name: base.name ?? `#${id}`,
gmlevel: base.gmlevel ?? 0,
isStaff: base.isStaff ?? false,
joindate: base.joindate ?? null,
postCount: 0,
avatar: ch ? raceAvatar(ch.race, ch.gender) : avatarForClass(undefined),
charName: ch?.name ?? null,
race: ch?.race ?? 0,
gender: ch?.gender ?? 0,
classId: ch?.class ?? 0,
level: ch?.level ?? 0,
guildName: ch ? (guildByGuid.get(ch.guid) ?? null) : null,
achPoints: ch ? (ptsByGuid.get(ch.guid) ?? 0) : 0,
}
}
try {
const [accs] = await db(DB.auth).query<RowDataPacket[]>(
`SELECT id, username, joindate FROM account WHERE id IN (${placeholders})`,
@@ -411,15 +482,12 @@ export async function resolvePosters(ids: number[]): Promise<Map<number, PosterI
for (const a of accs) {
const gmlevel = gm.get(Number(a.id)) ?? 0
map.set(Number(a.id), {
id: Number(a.id),
map.set(Number(a.id), buildPoster(Number(a.id), {
name: String(a.username || '').replace(/#\d+$/, '') || `#${a.id}`,
gmlevel,
isStaff: gmlevel >= STAFF_GMLEVEL,
joindate: a.joindate ? new Date(a.joindate).toISOString() : null,
postCount: 0,
avatar: avatarForClass(classByAccount.get(Number(a.id))),
})
}))
}
} catch {
/* acore_auth no disponible: se devuelven placeholders abajo */
@@ -427,15 +495,7 @@ 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,
avatar: avatarForClass(classByAccount.get(id)),
})
map.set(id, buildPoster(id, {}))
}
}
return map