'use client' import { useEffect, useRef, useState } from 'react' /** * Visor 3D del personaje. Usa la librería `wow-model-viewer` (generateModels), que a * su vez necesita jQuery y el ZamModelViewer de wowhead (wow.zamimg.com) cargados en * global. Se cargan bajo demanda solo en esta ficha. * * OJO: es integración cliente que depende de un CDN externo (zamimg) y no se puede * verificar sin navegador; por eso todo va envuelto en try/catch y con un temporizador * de reserva: si el modelo no aparece, se muestra un aviso en lugar de romper la ficha. * * El equipo se pasa como {slot, entry, displayid}; la librería mapea cada ítem a su * slot del visor. displayid = transmog si lo hay, si no el del ítem equipado. */ // Same-origin (proxy /modelviewer/* -> zamimg en next.config): evita el bloqueo CORS // del visor, que es lo que dejaba el modelo «no disponible». const ZAM_VIEWER = '/modelviewer/live/viewer/viewer.min.js' const JQUERY = 'https://code.jquery.com/jquery-3.7.1.min.js' const CONTENT_PATH = '/modelviewer/live/' type Win = typeof window & { jQuery?: unknown; $?: unknown; ZamModelViewer?: unknown; CONTENT_PATH?: string } function loadScript(src: string): Promise { return new Promise((resolve, reject) => { if (document.querySelector(`script[src="${src}"]`)) return resolve() const s = document.createElement('script') s.src = src s.async = true s.onload = () => resolve() s.onerror = () => reject(new Error(`no se pudo cargar ${src}`)) document.head.appendChild(s) }) } export function ArmoryModel3D({ race, gender, equip, labels, }: { race: number gender: number equip: { slot: number; entry: number; displayid: number }[] labels: { loading: string; unavailable: string } }) { const ref = useRef(null) const [state, setState] = useState<'loading' | 'ok' | 'fail'>('loading') useEffect(() => { let cancelled = false const failTimer = setTimeout(() => !cancelled && setState((s) => (s === 'loading' ? 'fail' : s)), 12000) ;(async () => { try { const w = window as Win w.CONTENT_PATH = CONTENT_PATH await loadScript(JQUERY) await loadScript(ZAM_VIEWER) // La librería es ESM y usa jQuery/ZamModelViewer globales. const mv = await import('wow-model-viewer') if (cancelled || !ref.current) return // La resolución de ítems hace consultas al CDN y puede fallar; si falla, se // renderiza al menos el personaje sin equipo en vez de dejar la ficha vacía. let items: unknown[] = [] try { items = await mv.findItemsInEquipments( equip.map((e) => ({ slot: e.slot, entry: e.entry, displayid: e.displayid })), 'live', ) } catch (err) { console.warn('[armory-3d] no se pudieron resolver los ítems del modelo:', err) } if (cancelled || !ref.current) return ref.current.id = ref.current.id || `armory-model-${race}-${gender}` await mv.generateModels(1, `#${ref.current.id}`, { race, gender, items }, 'live') if (!cancelled) { clearTimeout(failTimer) setState('ok') } } catch (err) { console.error('[armory-3d] no se pudo cargar el visor 3D:', err) if (!cancelled) { clearTimeout(failTimer) setState('fail') } } })() return () => { cancelled = true clearTimeout(failTimer) } // solo al montar / cambiar de personaje // eslint-disable-next-line react-hooks/exhaustive-deps }, [race, gender]) return (
{state === 'loading' &&

{labels.loading}

} {state === 'fail' &&

{labels.unavailable}

}
) }