refactor(gachabase): remove gachabase sync system, UI buttons, and catalog fetching
This commit is contained in:
1 parent
8e57a297d0
commit
1c10928849
3 files changed
-496
No files matched your search
-317
@@ -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 <color=#f29e38ff>, </color>, <b>, </b> from text strings
|
||||
*/
|
||||
export function cleanRichText(str) {
|
||||
if (!str || typeof str !== 'string') return '';
|
||||
return str
|
||||
.replace(/<color=[^>]+>/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(/<script[^>]*>([\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
|
||||
|
||||
Reference in new issue
Block a user