c0f53deede
Copias de los ficheros de la armería tal y como estaban antes del rediseño (paperdoll, talentos y actividad), por si hace falta consultarlas o volver atrás.
143 lines
5.5 KiB
TypeScript
143 lines
5.5 KiB
TypeScript
'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 }
|
|
|
|
/**
|
|
* Garantiza que window.WH está completo. El viewer.min.js de zamimg crea un WH
|
|
* propio SIN `debug`, y entonces el setup.js de la librería (que hace `if (!WH)`)
|
|
* se lo salta y nunca define WH.debug → el visor casca con «WH.debug is not a
|
|
* function». Aquí se rellena cada propiedad que falte, con fallback, tras cargar
|
|
* los scripts y antes de generar el modelo.
|
|
*/
|
|
function ensureWH() {
|
|
const w = window as unknown as { WH?: Record<string, unknown> }
|
|
const WH = (w.WH = w.WH || {})
|
|
if (typeof WH.debug !== 'function') WH.debug = () => {}
|
|
if (!WH.defaultAnimation) WH.defaultAnimation = 'Stand'
|
|
if (!WH.WebP) WH.WebP = { getImageExtension: () => '.webp' }
|
|
if (!WH.Wow) {
|
|
WH.Wow = {
|
|
Item: {
|
|
INVENTORY_TYPE_HEAD: 1, INVENTORY_TYPE_NECK: 2, INVENTORY_TYPE_SHOULDERS: 3,
|
|
INVENTORY_TYPE_SHIRT: 4, INVENTORY_TYPE_CHEST: 5, INVENTORY_TYPE_WAIST: 6,
|
|
INVENTORY_TYPE_LEGS: 7, INVENTORY_TYPE_FEET: 8, INVENTORY_TYPE_WRISTS: 9,
|
|
INVENTORY_TYPE_HANDS: 10, INVENTORY_TYPE_FINGER: 11, INVENTORY_TYPE_TRINKET: 12,
|
|
INVENTORY_TYPE_ONE_HAND: 13, INVENTORY_TYPE_SHIELD: 14, INVENTORY_TYPE_RANGED: 15,
|
|
INVENTORY_TYPE_BACK: 16, INVENTORY_TYPE_TWO_HAND: 17, INVENTORY_TYPE_BAG: 18,
|
|
INVENTORY_TYPE_TABARD: 19, INVENTORY_TYPE_ROBE: 20, INVENTORY_TYPE_MAIN_HAND: 21,
|
|
INVENTORY_TYPE_OFF_HAND: 22, INVENTORY_TYPE_HELD_IN_OFF_HAND: 23,
|
|
INVENTORY_TYPE_PROJECTILE: 24, INVENTORY_TYPE_THROWN: 25, INVENTORY_TYPE_RANGED_RIGHT: 26,
|
|
INVENTORY_TYPE_QUIVER: 27, INVENTORY_TYPE_RELIC: 28, INVENTORY_TYPE_PROFESSION_TOOL: 29,
|
|
INVENTORY_TYPE_PROFESSION_ACCESSORY: 30,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadScript(src: string): Promise<void> {
|
|
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<HTMLDivElement>(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')
|
|
ensureWH() // completar WH.debug/WebP/Wow tras cargar viewer.min.js + setup.js
|
|
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 (
|
|
<div className="armory-model">
|
|
<div ref={ref} className="armory-model-canvas" />
|
|
{state === 'loading' && <p className="armory-hint">{labels.loading}</p>}
|
|
{state === 'fail' && <p className="armory-hint">{labels.unavailable}</p>}
|
|
</div>
|
|
)
|
|
}
|