diff --git a/index.html b/index.html index 1b6425e..896a49f 100644 --- a/index.html +++ b/index.html @@ -47,10 +47,6 @@ - - @@ -308,7 +304,6 @@
- @@ -361,7 +356,6 @@
-
@@ -761,30 +755,7 @@
- -
-
- 🌐 อัปเดตข้อมูลเกม & รูปภาพ (Gachabase Sync) - พร้อมใช้งาน -
-

- ดึงข้อมูลตัวละคร, กรวยแสง, และรูปภาพความละเอียดสูงจาก Gachabase อัตโนมัติเมื่อเกมมีแพตช์ใหม่หรือมีคอนเทนต์เบต้า -

-
-
-
-
ฐานข้อมูลตัวละครและกรวยแสง
-
- ตัวละครในระบบ: 124+ ตัว | กรวยแสง: 172+ ชิ้น (รองรับเนื้อหา 3.0+ และเบต้า) -
-
- -
-
-
diff --git a/js/app.js b/js/app.js index afdf341..1116f5f 100644 --- a/js/app.js +++ b/js/app.js @@ -2,8 +2,6 @@ import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js'; import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetInfo } from './equipment-data.js'; import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js'; import { - syncGachabaseDatabase, - loadCachedGachabaseData, getAvatarIconUrl, getLightconeArtUrl, getLightconeThumbUrl, @@ -120,14 +118,6 @@ const el = { consoleCmdInput: document.getElementById('console-cmd-input'), btnConsoleCmdSend: document.getElementById('btn-console-cmd-send'), lblConsoleConnStatus: document.getElementById('lbl-console-conn-status'), - // Gachabase Sync - btnHeaderGachabaseSync: document.getElementById('btn-header-gachabase-sync'), - btnQuickGachabaseSync: document.getElementById('btn-quick-gachabase-sync'), - btnSyncGachabaseAvatars: document.getElementById('btn-sync-gachabase-avatars'), - btnSyncGachabaseEq: document.getElementById('btn-sync-gachabase-eq'), - btnSyncGachabaseNow: document.getElementById('btn-sync-gachabase-now'), - badgeGachabaseStatus: document.getElementById('badge-gachabase-status'), - lblGachabaseInfo: document.getElementById('lbl-gachabase-info'), // Avatars avatarsGrid: document.getElementById('avatars-grid'), @@ -6766,144 +6756,12 @@ function setupTabs() { loadJsonData(state.currentJsonMode); } else if (targetId === 'tab-settings') { updateCacheUsageDisplay(); - updateGachabaseSyncUI(); } } }); }); } -// ========================================== -// Gachabase Catalog Sync & Dynamic Image Engine -// ========================================== -let isGachabaseSyncing = false; - -async function updateGachabaseSyncUI() { - if (!el.badgeGachabaseStatus && !el.lblGachabaseInfo) return; - try { - const lastSync = await idbGet('gac_gachabase_last_sync'); - if (lastSync && lastSync.timestamp) { - const dateStr = new Date(lastSync.timestamp).toLocaleString(); - if (el.badgeGachabaseStatus) { - el.badgeGachabaseStatus.textContent = 'ซิงค์แล้ว'; - el.badgeGachabaseStatus.style.color = 'var(--accent-green)'; - el.badgeGachabaseStatus.style.borderColor = 'rgba(52, 211, 153, 0.4)'; - } - if (el.lblGachabaseInfo) { - el.lblGachabaseInfo.innerHTML = `ซิงค์ล่าสุดเมื่อ: ${dateStr} (ตัวละครในระบบ: ${lastSync.totalAvatarsCount || Object.keys(AVATAR_METADATA).length}, กรวยแสง: ${lastSync.totalLcsCount || Object.keys(LIGHTCONE_METADATA).length})`; - } - } else { - if (el.badgeGachabaseStatus) { - el.badgeGachabaseStatus.textContent = 'ยังไม่เคยซิงค์'; - el.badgeGachabaseStatus.style.color = 'var(--accent-gold)'; - el.badgeGachabaseStatus.style.borderColor = 'rgba(245, 158, 11, 0.4)'; - } - if (el.lblGachabaseInfo) { - el.lblGachabaseInfo.textContent = 'ยังไม่ได้ซิงค์แคตตาล็อกเกมจาก Gachabase กดปุ่มด้านล่างเพื่ออัปเดต'; - } - } - } catch (err) { - console.warn('Error reading gachabase sync status:', err); - } -} - -async function triggerGachabaseSync(showToastOnDone = true) { - if (isGachabaseSyncing) { - showToast('กำลังซิงค์ข้อมูลจาก Gachabase อยู่แล้ว กรุณารอสักครู่...', 'warning'); - return; - } - - isGachabaseSyncing = true; - const syncButtons = [ - el.btnHeaderGachabaseSync, - el.btnQuickGachabaseSync, - el.btnSyncGachabaseAvatars, - el.btnSyncGachabaseEq, - el.btnSyncGachabaseNow - ].filter(Boolean); - - syncButtons.forEach(btn => { - btn.disabled = true; - btn.classList.add('loading'); - }); - - if (el.badgeGachabaseStatus) { - el.badgeGachabaseStatus.textContent = 'กำลังซิงค์...'; - el.badgeGachabaseStatus.style.color = 'var(--accent-cyan)'; - el.badgeGachabaseStatus.style.borderColor = 'rgba(56, 189, 248, 0.4)'; - } - if (el.lblGachabaseInfo) { - el.lblGachabaseInfo.textContent = 'กำลังดึงฐานข้อมูลตัวละครและกรวยแสงจาก Gachabase CDN...'; - } - - showToast('🌐 เริ่มซิงค์ฐานข้อมูลตัวละครและรูปภาพจาก Gachabase...', 'info'); - - try { - const currentLang = getCurrentLanguage(); - const result = await syncGachabaseDatabase({ - apiUrl: state.apiUrl, - branch: 'beta', - lang: currentLang === 'th' ? 'th' : 'en', - metadataContext: { - AVATAR_METADATA, - LIGHTCONE_METADATA, - SIGNATURE_LIGHTCONES, - idbSet, - idbGet - } - }); - - initAvatarsList(true); - renderAvatarsGrid(); - loadEquipment(); - if (state.selectedAvatarDetail) { - renderModalSwitcher(state.selectedAvatarDetail.avatar_id); - } - await updateGachabaseSyncUI(); - - if (showToastOnDone) { - showToast(`✅ ซิงค์สำเร็จ! ตรวจพบตัวละครทั้งหมด ${result.totalAvatarsCount} ตัว (+ใหม่ ${result.newAvatarsCount}), กรวยแสง ${result.totalLcsCount} ชิ้น (+ใหม่ ${result.newLcsCount}) พร้อมผูกรูป Gachabase CDN เรียบร้อย`, 'success'); - } - } catch (err) { - console.error('Gachabase sync failed:', err); - if (el.badgeGachabaseStatus) { - el.badgeGachabaseStatus.textContent = 'ซิงค์ล้มเหลว'; - el.badgeGachabaseStatus.style.color = 'var(--accent-red)'; - el.badgeGachabaseStatus.style.borderColor = 'rgba(248, 113, 113, 0.4)'; - } - if (el.lblGachabaseInfo) { - el.lblGachabaseInfo.textContent = `ข้อผิดพลาด: ${err.message}`; - } - showToast(`❌ ซิงค์ข้อมูล Gachabase ไม่สำเร็จ: ${err.message}`, 'error'); - } finally { - isGachabaseSyncing = false; - syncButtons.forEach(btn => { - btn.disabled = false; - btn.classList.remove('loading'); - }); - } -} - - - - - -function setupGachabaseSyncEvents() { - const syncButtons = [ - el.btnHeaderGachabaseSync, - el.btnQuickGachabaseSync, - el.btnSyncGachabaseAvatars, - el.btnSyncGachabaseEq, - el.btnSyncGachabaseNow - ].filter(Boolean); - - syncButtons.forEach(btn => { - btn.addEventListener('click', () => { - triggerGachabaseSync(true); - }); - }); -} - // Setup Event Listeners function setupEvents() { // Quick Ping & Refresh @@ -8462,12 +8320,6 @@ function startPingLoop() { // Initialize Application async function init() { await initGitBranchDisplay(); - await loadCachedGachabaseData({ - idbGet, - idbSet, - AVATAR_METADATA, - LIGHTCONE_METADATA - }); initAvatarsList(); await loadCachedStorageData(); setupServerModes(); @@ -8476,14 +8328,12 @@ async function init() { setupModalSubtabs(); setupEvents(); setupDamageAnalystEvents(); - setupGachabaseSyncEvents(); setupGMConsoleEvents(); applyTranslations(); renderLineup(state.statusData?.lineups || DEFAULT_LINEUP); renderAvatarsGrid(); loadEquipment(); renderEmptyDamageAnalyst(); - updateGachabaseSyncUI(); refreshStatus(); fetchLatestBattleReport(true); startPingLoop(); diff --git a/js/gachabase.js b/js/gachabase.js index 98c84ec..e695769 100644 --- a/js/gachabase.js +++ b/js/gachabase.js @@ -36,323 +36,6 @@ export const GACHABASE_IMAGE_CACHE = { lightconeThumbs: { ...KNOWN_GACHABASE_ASSETS.lightconeThumbs } }; -// Path ID mappings according to HSR engine / Gachabase -export const GACHABASE_PATHS = { - 1: 'Destruction', - 2: 'Hunt', - 3: 'Erudition', - 4: 'Harmony', - 5: 'Nihility', - 6: 'Preservation', - 7: 'Abundance', - 8: 'Remembrance', - 9: 'Elation' -}; - -// Element / Type ID mappings -export const GACHABASE_ELEMENTS = { - 1: 'Physical', - 2: 'Fire', - 4: 'Ice', - 8: 'Thunder', // Lightning - 16: 'Wind', - 32: 'Quantum', - 64: 'Imaginary' -}; - -// Stat ID mappings -export const GACHABASE_STATS = { - 1: 'HP', - 2: 'ATK', - 3: 'DEF', - 21: 'HP%', - 22: 'ATK%', - 23: 'DEF%', - 31: 'Physical DMG%', - 32: 'Fire DMG%', - 33: 'Ice DMG%', - 34: 'Lightning DMG%', - 35: 'Wind DMG%', - 36: 'Quantum DMG%', - 37: 'Imaginary DMG%', - 41: 'Break Effect%', - 42: 'Outgoing Healing Boost%', - 43: 'Max Energy', - 44: 'Energy Regeneration Rate%', - 45: 'Effect Hit Rate%', - 46: 'Effect RES%', - 51: 'SPD', - 52: 'CRIT Rate%', - 53: 'CRIT DMG%' -}; - -/** - * Strips rich-text tags like , , , from text strings - */ -export function cleanRichText(str) { - if (!str || typeof str !== 'string') return ''; - return str - .replace(/]+>/gi, '') - .replace(/<\/color>/gi, '') - .replace(/<[^>]+>/g, '') - .replace(/\\n/g, '\n') - .trim(); -} - -/** - * Fetches HTML from targetUrl via backend API proxy or direct fetch - */ -export async function fetchGachabaseHtml(targetUrl, apiUrl = 'http://localhost:21000') { - if (!targetUrl) throw new Error('Target URL is required for Gachabase fetch'); - - // 1. Try Backend Proxy - if (apiUrl) { - try { - const cleanApi = apiUrl.replace(/\/+$/, ''); - const proxyUrl = `${cleanApi}/api/gachabase/fetch?url=${encodeURIComponent(targetUrl)}`; - const resp = await fetch(proxyUrl, { method: 'GET', headers: { 'Accept': 'application/json' } }); - if (resp.ok) { - const json = await resp.json(); - if (json && json.html) { - return { html: json.html, finalUrl: json.url || targetUrl }; - } - } - } catch (err) { - console.warn('Backend Gachabase proxy unreachable, attempting direct fetch...', err); - } - } - - // 2. Direct fetch fallback - const directResp = await fetch(targetUrl, { - method: 'GET', - headers: { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' - } - }); - if (!directResp.ok) { - throw new Error(`Direct fetch failed with status ${directResp.status}`); - } - const html = await directResp.text(); - return { html, finalUrl: targetUrl }; -} - -/** - * Parses SvelteKit streamed data payload from Gachabase catalog listing pages - */ -export function parseSvelteKitCatalog(html) { - if (!html || typeof html !== 'string') return null; - const scripts = [...html.matchAll(/]*>([\s\S]*?)<\/script>/gi)]; - for (const s of scripts) { - const text = s[1].trim(); - if (text.includes('resolve') && text.includes('entries')) { - const startIdx = text.indexOf('() => '); - if (startIdx !== -1) { - let code = text.substring(startIdx + 6).trim(); - if (code.endsWith(');')) code = code.slice(0, -2); - else if (code.endsWith(')')) code = code.slice(0, -1); - try { - const fn = new Function('return (' + code + ')'); - const res = fn(); - return res[0]?.data; - } catch (e) { - console.warn('Failed to parse catalog script:', e); - } - } - } - } - return null; -} - -/** - * Automatically synchronizes the full game database from Gachabase - * Pulls all characters and lightcones, adds missing ones into metadata, - * and caches all Gachabase CDN asset URLs for seamless rendering. - */ -export async function syncGachabaseDatabase({ apiUrl, branch = 'beta', lang = 'en', metadataContext }) { - const { - AVATAR_METADATA, - LIGHTCONE_METADATA, - SIGNATURE_LIGHTCONES, - idbSet, - idbGet - } = metadataContext; - - const charactersUrl = `${GACHABASE_BASE_URL}/characters?lang=${lang}&branch=${branch}`; - const lightconesUrl = `${GACHABASE_BASE_URL}/lightcones?lang=${lang}&branch=${branch}`; - - let newAvatarsCount = 0; - let newLcsCount = 0; - let totalAvatarsCount = 0; - let totalLcsCount = 0; - - // 1. Fetch & Parse Characters Catalog - try { - const { html: charHtml } = await fetchGachabaseHtml(charactersUrl, apiUrl); - const charData = parseSvelteKitCatalog(charHtml); - - if (charData && Array.isArray(charData.entries)) { - totalAvatarsCount = charData.entries.length; - const assetsMap = charData.refs?.assets || {}; - - for (const entry of charData.entries) { - const aid = entry.id; - const hash = entry.assets?.square_icon_path_hash || entry.assets?.round_icon_path_hash; - const iconUrl = assetsMap[hash]?.url || KNOWN_GACHABASE_ASSETS.avatars[aid] || `${GACHABASE_CDN_URL}/hsr/assets/${hash}.png`; - - if (iconUrl) { - GACHABASE_IMAGE_CACHE.avatars[aid] = iconUrl; - } - - // Register new / missing characters into AVATAR_METADATA - if (AVATAR_METADATA && !AVATAR_METADATA[aid]) { - const charName = entry.name?.text || entry.name || `Avatar ${aid}`; - const pathName = GACHABASE_PATHS[entry.path_id] || 'Unknown'; - const elementName = GACHABASE_ELEMENTS[entry.type_id] || 'Unknown'; - - AVATAR_METADATA[aid] = { - names: { - th: charName, - en: charName, - 'zh-CN': charName - }, - path: pathName, - element: elementName, - rarity: entry.rarity || 5, - hasExRank: false, - isGachabaseSync: true, - images: { - icon: iconUrl, - square: iconUrl - } - }; - newAvatarsCount++; - } else if (AVATAR_METADATA && AVATAR_METADATA[aid]) { - // Enrich existing metadata with Gachabase CDN image - AVATAR_METADATA[aid].images = AVATAR_METADATA[aid].images || {}; - if (iconUrl) AVATAR_METADATA[aid].images.icon = iconUrl; - } - } - } - } catch (err) { - console.error('Failed to sync characters from Gachabase:', err); - throw new Error('ไม่สามารถซิงค์ตัวละครจาก Gachabase ได้: ' + err.message); - } - - // 2. Fetch & Parse Lightcones Catalog - try { - const { html: lcHtml } = await fetchGachabaseHtml(lightconesUrl, apiUrl); - const lcData = parseSvelteKitCatalog(lcHtml); - - if (lcData && Array.isArray(lcData.entries)) { - totalLcsCount = lcData.entries.length; - const assetsMap = lcData.refs?.assets || {}; - - for (const entry of lcData.entries) { - const lcid = entry.id; - const thumbHash = entry.assets?.thumbnail_art_path_hash || entry.assets?.small_icon_path_hash; - const fullArtHash = entry.assets?.full_art_path_hash || entry.assets?.big_icon_path_hash; - - const thumbUrl = assetsMap[thumbHash]?.url || KNOWN_GACHABASE_ASSETS.lightconeThumbs[lcid]; - const fullArtUrl = assetsMap[fullArtHash]?.url || KNOWN_GACHABASE_ASSETS.lightcones[lcid]; - - if (thumbUrl) GACHABASE_IMAGE_CACHE.lightconeThumbs[lcid] = thumbUrl; - if (fullArtUrl) GACHABASE_IMAGE_CACHE.lightcones[lcid] = fullArtUrl; - - // Register new / missing lightcones into LIGHTCONE_METADATA - if (LIGHTCONE_METADATA && !LIGHTCONE_METADATA[lcid]) { - const lcName = entry.name?.text || entry.name || `Lightcone ${lcid}`; - const pathName = GACHABASE_PATHS[entry.path_id] || 'Unknown'; - - LIGHTCONE_METADATA[lcid] = { - names: { - th: lcName, - en: lcName, - 'zh-CN': lcName - }, - rarity: entry.rarity || 5, - path: pathName, - isGachabaseSync: true, - images: { - thumbnail: thumbUrl, - fullArt: fullArtUrl - } - }; - newLcsCount++; - } else if (LIGHTCONE_METADATA && LIGHTCONE_METADATA[lcid]) { - LIGHTCONE_METADATA[lcid].images = LIGHTCONE_METADATA[lcid].images || {}; - if (thumbUrl) LIGHTCONE_METADATA[lcid].images.thumbnail = thumbUrl; - if (fullArtUrl) LIGHTCONE_METADATA[lcid].images.fullArt = fullArtUrl; - } - } - } - } catch (err) { - console.error('Failed to sync lightcones from Gachabase:', err); - throw new Error('ไม่สามารถซิงค์กรวยแสงจาก Gachabase ได้: ' + err.message); - } - - // 3. Save cache to IndexedDB - if (idbSet) { - try { - await idbSet('gac_gachabase_image_cache', GACHABASE_IMAGE_CACHE); - await idbSet('gac_gachabase_last_sync', { - timestamp: Date.now(), - newAvatarsCount, - newLcsCount, - totalAvatarsCount, - totalLcsCount - }); - } catch (e) { - console.warn('Failed to cache Gachabase database to IndexedDB:', e); - } - } - - return { - newAvatarsCount, - newLcsCount, - totalAvatarsCount, - totalLcsCount - }; -} - -/** - * Loads cached Gachabase images and metadata from IndexedDB - */ -export async function loadCachedGachabaseData(metadataContext = {}) { - const { idbGet, AVATAR_METADATA, LIGHTCONE_METADATA } = metadataContext; - if (!idbGet) return; - - try { - const cachedImages = await idbGet('gac_gachabase_image_cache'); - if (cachedImages) { - if (cachedImages.avatars) Object.assign(GACHABASE_IMAGE_CACHE.avatars, cachedImages.avatars); - if (cachedImages.avatarSplash) Object.assign(GACHABASE_IMAGE_CACHE.avatarSplash, cachedImages.avatarSplash); - if (cachedImages.lightcones) Object.assign(GACHABASE_IMAGE_CACHE.lightcones, cachedImages.lightcones); - if (cachedImages.lightconeThumbs) Object.assign(GACHABASE_IMAGE_CACHE.lightconeThumbs, cachedImages.lightconeThumbs); - } - - // Attach cached image links to metadata - if (AVATAR_METADATA) { - for (const aid in GACHABASE_IMAGE_CACHE.avatars) { - if (AVATAR_METADATA[aid]) { - AVATAR_METADATA[aid].images = AVATAR_METADATA[aid].images || {}; - AVATAR_METADATA[aid].images.icon = GACHABASE_IMAGE_CACHE.avatars[aid]; - } - } - } - if (LIGHTCONE_METADATA) { - for (const lcid in GACHABASE_IMAGE_CACHE.lightcones) { - if (LIGHTCONE_METADATA[lcid]) { - LIGHTCONE_METADATA[lcid].images = LIGHTCONE_METADATA[lcid].images || {}; - LIGHTCONE_METADATA[lcid].images.fullArt = GACHABASE_IMAGE_CACHE.lightcones[lcid]; - LIGHTCONE_METADATA[lcid].images.thumbnail = GACHABASE_IMAGE_CACHE.lightconeThumbs[lcid]; - } - } - } - } catch (err) { - console.warn('Failed to restore Gachabase cache from IndexedDB:', err); - } -} - /** * Image resolver for avatars * Prioritizes local asset, falls back to Gachabase CDN