65f6776969
Tres arreglos: 1) Búsqueda insensible a mayúsculas/minúsculas. Las consultas usaban LIKE directo (sensible según la colación de la tabla), así que 'test' no encontraba 'Test'. Ahora comparan con LOWER(col) LIKE LOWER(?). 2) Autocompletar tipo wowhead. Nuevo /api/armory/suggest + ArmorySearchBox reescrito: al teclear (≥1 carácter, con debounce) muestra un desplegable con personajes, ítems y hermandades que coinciden; clic va directo a la ficha/wowhead. Enter o el botón siguen haciendo la búsqueda completa. 3) Visor 3D «no disponible». Causa: el contenido de wow.zamimg.com/modelviewer no envía cabeceras CORS (el propio README de wow-model-viewer pide un proxy). Se añade un rewrite en next.config (/modelviewer/* -> zamimg) para servirlo same-origin, y el visor usa esas rutas. Además, si falla la resolución de ítems se renderiza el personaje sin equipo, y los errores se registran en consola para diagnóstico. Verificado: 'test' encuentra 'Test'; el autocompletar responde; el proxy /modelviewer/live/viewer/viewer.min.js sirve 200 same-origin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
237 lines
7.1 KiB
TypeScript
237 lines
7.1 KiB
TypeScript
import type { RowDataPacket } from 'mysql2'
|
|
import { db, DB } from './db'
|
|
|
|
// Armería: búsqueda de personajes/ítems/hermandades y ficha de personaje con equipo.
|
|
// Datos de personaje/equipo en acore_characters; datos de ítem (nombre/calidad/
|
|
// display_id) en django_wow.item_data (importado de los db2; item_template no existe
|
|
// en 3.4.3). Se resuelve en dos pasos para no depender de joins entre esquemas.
|
|
|
|
export interface CharacterResult {
|
|
guid: number
|
|
name: string
|
|
race: number
|
|
gender: number
|
|
class: number
|
|
level: number
|
|
}
|
|
|
|
export interface ItemResult {
|
|
entry: number
|
|
name: string
|
|
quality: number
|
|
itemLevel: number
|
|
inventoryType: number
|
|
}
|
|
|
|
export interface GuildResult {
|
|
guildid: number
|
|
name: string
|
|
members: number
|
|
}
|
|
|
|
export interface EquipItem {
|
|
slot: number
|
|
entry: number
|
|
name: string
|
|
quality: number
|
|
itemLevel: number
|
|
inventoryType: number
|
|
displayId: number | null
|
|
transmogEntry: number | null
|
|
transmogDisplayId: number | null
|
|
}
|
|
|
|
const LIMIT = 25
|
|
|
|
export async function searchCharacters(q: string, limit = LIMIT): Promise<CharacterResult[]> {
|
|
try {
|
|
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT guid, name, race, gender, class, level FROM characters WHERE LOWER(name) LIKE ? ORDER BY level DESC, name LIMIT ?',
|
|
[`%${q.toLowerCase()}%`, limit],
|
|
)
|
|
return rows.map((r) => ({
|
|
guid: r.guid,
|
|
name: r.name,
|
|
race: r.race,
|
|
gender: r.gender,
|
|
class: r.class,
|
|
level: r.level,
|
|
}))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function searchGuilds(q: string, limit = LIMIT): Promise<GuildResult[]> {
|
|
try {
|
|
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
|
|
`SELECT g.guildid, g.name, (SELECT COUNT(*) FROM guild_member gm WHERE gm.guildid = g.guildid) AS members
|
|
FROM guild g WHERE LOWER(g.name) LIKE ? ORDER BY members DESC LIMIT ?`,
|
|
[`%${q.toLowerCase()}%`, limit],
|
|
)
|
|
return rows.map((r) => ({ guildid: r.guildid, name: r.name, members: Number(r.members ?? 0) }))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function searchItems(q: string, locale: string, limit = LIMIT): Promise<ItemResult[]> {
|
|
const col = locale === 'en' ? 'name_en' : 'name_es'
|
|
try {
|
|
const [rows] = await db(DB.default).query<RowDataPacket[]>(
|
|
`SELECT entry, COALESCE(${col}, name_en) AS name, quality, item_level, inventory_type
|
|
FROM item_data WHERE LOWER(${col}) LIKE ? OR LOWER(name_en) LIKE ? ORDER BY item_level DESC LIMIT ?`,
|
|
[`%${q.toLowerCase()}%`, `%${q.toLowerCase()}%`, limit],
|
|
)
|
|
return rows.map((r) => ({
|
|
entry: r.entry,
|
|
name: r.name,
|
|
quality: r.quality,
|
|
itemLevel: r.item_level,
|
|
inventoryType: r.inventory_type,
|
|
}))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/** Sugerencias para el autocompletar: pocos resultados de cada tipo. */
|
|
export async function suggest(
|
|
q: string,
|
|
locale: string,
|
|
): Promise<{ characters: CharacterResult[]; items: ItemResult[]; guilds: GuildResult[] }> {
|
|
const [characters, items, guilds] = await Promise.all([
|
|
searchCharacters(q, 6),
|
|
searchItems(q, locale, 6),
|
|
searchGuilds(q, 5),
|
|
])
|
|
return { characters, items, guilds }
|
|
}
|
|
|
|
export async function getCharacter(guid: number): Promise<
|
|
(CharacterResult & { guildName: string | null; guildId: number | null; online: boolean }) | null
|
|
> {
|
|
try {
|
|
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT guid, name, race, gender, class, level, online FROM characters WHERE guid = ?',
|
|
[guid],
|
|
)
|
|
const c = rows[0]
|
|
if (!c) return null
|
|
let guildName: string | null = null
|
|
let guildId: number | null = null
|
|
try {
|
|
const [g] = await db(DB.characters).query<RowDataPacket[]>(
|
|
`SELECT gu.guildid, gu.name FROM guild_member gm JOIN guild gu ON gu.guildid = gm.guildid WHERE gm.guid = ?`,
|
|
[guid],
|
|
)
|
|
if (g[0]) {
|
|
guildId = g[0].guildid
|
|
guildName = g[0].name
|
|
}
|
|
} catch {
|
|
/* sin hermandad */
|
|
}
|
|
return {
|
|
guid: c.guid,
|
|
name: c.name,
|
|
race: c.race,
|
|
gender: c.gender,
|
|
class: c.class,
|
|
level: c.level,
|
|
online: c.online === 1,
|
|
guildName,
|
|
guildId,
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Ítems equipados (slots 0-18) del personaje, resueltos con item_data. */
|
|
export async function getCharacterEquipment(guid: number, locale: string): Promise<EquipItem[]> {
|
|
const col = locale === 'en' ? 'name_en' : 'name_es'
|
|
try {
|
|
const [inv] = await db(DB.characters).query<RowDataPacket[]>(
|
|
`SELECT ci.slot, ii.itemEntry, ii.transmogrification
|
|
FROM character_inventory ci JOIN item_instance ii ON ii.guid = ci.item
|
|
WHERE ci.guid = ? AND ci.bag = 0 AND ci.slot <= 18 ORDER BY ci.slot`,
|
|
[guid],
|
|
)
|
|
if (inv.length === 0) return []
|
|
|
|
// Resolver todos los entries (equipados + transmog) en una consulta a item_data.
|
|
const entries = [
|
|
...new Set(inv.flatMap((r) => [Number(r.itemEntry), Number(r.transmogrification)]).filter((n) => n > 0)),
|
|
]
|
|
const meta = new Map<number, RowDataPacket>()
|
|
if (entries.length) {
|
|
const [items] = await db(DB.default).query<RowDataPacket[]>(
|
|
`SELECT entry, COALESCE(${col}, name_en) AS name, quality, item_level, inventory_type, display_id
|
|
FROM item_data WHERE entry IN (${entries.map(() => '?').join(',')})`,
|
|
entries,
|
|
)
|
|
for (const it of items) meta.set(Number(it.entry), it)
|
|
}
|
|
|
|
return inv.map((r) => {
|
|
const entry = Number(r.itemEntry)
|
|
const tmog = Number(r.transmogrification) || null
|
|
const m = meta.get(entry)
|
|
const tm = tmog ? meta.get(tmog) : undefined
|
|
return {
|
|
slot: r.slot,
|
|
entry,
|
|
name: m?.name ?? `#${entry}`,
|
|
quality: m ? Number(m.quality) : 0,
|
|
itemLevel: m ? Number(m.item_level) : 0,
|
|
inventoryType: m ? Number(m.inventory_type) : 0,
|
|
displayId: m && m.display_id != null ? Number(m.display_id) : null,
|
|
transmogEntry: tmog,
|
|
transmogDisplayId: tm && tm.display_id != null ? Number(tm.display_id) : null,
|
|
}
|
|
})
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export interface GuildMember {
|
|
guid: number
|
|
name: string
|
|
race: number
|
|
class: number
|
|
level: number
|
|
rank: number
|
|
}
|
|
|
|
export async function getGuild(
|
|
guildid: number,
|
|
): Promise<{ guildid: number; name: string; members: GuildMember[]; leaderName: string | null } | null> {
|
|
try {
|
|
const [g] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT guildid, name, leaderguid FROM guild WHERE guildid = ?',
|
|
[guildid],
|
|
)
|
|
if (!g[0]) return null
|
|
const [members] = await db(DB.characters).query<RowDataPacket[]>(
|
|
`SELECT c.guid, c.name, c.race, c.class, c.level, gm.rank
|
|
FROM guild_member gm JOIN characters c ON c.guid = gm.guid
|
|
WHERE gm.guildid = ? ORDER BY gm.rank, c.level DESC`,
|
|
[guildid],
|
|
)
|
|
const list = members.map((m) => ({
|
|
guid: m.guid,
|
|
name: m.name,
|
|
race: m.race,
|
|
class: m.class,
|
|
level: m.level,
|
|
rank: m.rank,
|
|
}))
|
|
const leader = list.find((m) => m.guid === g[0].leaderguid)
|
|
return { guildid: g[0].guildid, name: g[0].name, members: list, leaderName: leader?.name ?? null }
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|