// Gachabase Data Fetcher, Parser & FreeSR Importer Module // Supports fetching datamined/beta characters, lightcones, and relics from hsr.gachabase.net // Integrates with GaC-srtool runtime metadata, IndexedDB cache, and FreeSR / persistent saves. export const GACHABASE_BASE_URL = 'https://hsr.gachabase.net'; export const GACHABASE_CDN_URL = 'https://cdn.gachabase.net'; // 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(); } /** * Parses user input into normalized search parameters * Handles full URLs or raw IDs */ export function parseGachabaseInput(input, typeHint = 'character', branch = 'beta', lang = 'en') { const trimmed = (input || '').trim(); if (!trimmed) return null; // Check if input is a full URL if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { try { const urlObj = new URL(trimmed); const pathname = urlObj.pathname.toLowerCase(); let detectedType = typeHint; if (pathname.includes('/characters/') || pathname.includes('/character/')) detectedType = 'character'; else if (pathname.includes('/lightcones/') || pathname.includes('/lightcone/')) detectedType = 'lightcone'; else if (pathname.includes('/relics/') || pathname.includes('/relic/')) detectedType = 'relic'; // Pattern: /type/:id/:slug?/:branch? const parts = pathname.split('/').filter(Boolean); // Expected: ['characters', '1511', 'aeon-aha', 'beta'] let id = null; let detectedBranch = urlObj.searchParams.get('branch') || branch; let detectedLang = urlObj.searchParams.get('lang') || lang; if (parts.length >= 2) { id = parseInt(parts[1], 10); } if (parts.length >= 4) { detectedBranch = parts[3]; } return { url: trimmed, id: id || null, type: detectedType, branch: detectedBranch || 'beta', lang: detectedLang || 'en' }; } catch (e) { // Fall through to ID parsing } } // Treat as numeric ID const numId = parseInt(trimmed, 10); if (!isNaN(numId)) { let resolvedType = typeHint; // Auto-guess type if default wasn't explicitly changed if (numId >= 20000 && numId < 30000) resolvedType = 'lightcone'; else if (numId < 1000) resolvedType = 'relic'; else if (numId >= 1000 && numId < 9000) resolvedType = 'character'; const typeSlug = resolvedType === 'lightcone' ? 'lightcones' : (resolvedType === 'relic' ? 'relics' : 'characters'); const url = `${GACHABASE_BASE_URL}/${typeSlug}/${numId}/slug/${branch}?lang=${lang}`; return { url, id: numId, type: resolvedType, branch: branch || 'beta', lang: lang || 'en' }; } return null; } /** * Fetches Gachabase HTML content * Uses GaC-hkrpg-go API proxy if available to avoid CORS, falls back to 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 (works if user has CORS unblocker or runs in local browser environment) 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 }; } /** * Extracts and parses serialized SvelteKit DTO and refs from Gachabase HTML */ export function parseGachabaseHtml(html, fallbackLang = 'en') { if (!html || typeof html !== 'string') { throw new Error('Invalid HTML content received from Gachabase'); } // Look for kit.start(...) serialized state in