'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. */ const ZAM_VIEWER = 'https://wow.zamimg.com/modelviewer/live/viewer/viewer.min.js' const JQUERY = 'https://code.jquery.com/jquery-3.7.1.min.js' const CONTENT_PATH = 'https://wow.zamimg.com/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 const items = await mv.findItemsInEquipments( equip.map((e) => ({ slot: e.slot, entry: e.entry, displayid: e.displayid })), 'live', ) 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 { 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}

}
) }