// 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