import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.8';
const CATALOG_URL = new URL('../assets/item-catalog.json?v=2', import.meta.url);
const PAGE_SIZE = 48;
let catalog = null;
let catalogLoading = null;
let page = 0;
let activeDetailId = null;
const elements = () => ({
tab: document.querySelector('.tab-btn[data-tab="tab-inventory"]'),
browseButton: document.getElementById('btn-inventory-catalog'),
manageButton: document.getElementById('btn-inventory-manage'),
catalogPanel: document.getElementById('item-catalog-panel'),
managePanel: document.getElementById('inventory-manage-panel'),
search: document.getElementById('item-catalog-search'),
filter: document.getElementById('item-catalog-filter'),
grid: document.getElementById('item-catalog-grid'),
status: document.getElementById('item-catalog-status'),
count: document.getElementById('item-catalog-count'),
pageLabel: document.getElementById('item-catalog-page-label'),
previous: document.getElementById('item-catalog-prev'),
next: document.getElementById('item-catalog-next'),
detail: document.getElementById('item-catalog-detail'),
detailArt: document.getElementById('item-catalog-detail-art'),
detailName: document.getElementById('item-catalog-detail-name'),
detailId: document.getElementById('item-catalog-detail-id'),
detailSource: document.getElementById('item-catalog-detail-source'),
detailMeta: document.getElementById('item-catalog-detail-meta'),
detailDescription: document.getElementById('item-catalog-detail-description'),
detailLore: document.getElementById('item-catalog-detail-lore'),
detailLoreSection: document.getElementById('item-catalog-detail-lore-section'),
amount: document.getElementById('item-catalog-detail-amount'),
amountLimit: document.getElementById('item-catalog-amount-limit')
});
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
})[char]);
}
function plainGameText(value) {
return String(value ?? '').replace(/<[^>]*>/g, '').replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\t/g, ' ').trim();
}
function displayName(item) {
const names = item.names || {};
return plainGameText(names[getCurrentLanguage()] || names.en || t('itemCatalogUnknownName', { id: item.id }));
}
function assetUrl(path, base) {
if (typeof path !== 'string' || !/^(icon|image)\/[A-Za-z0-9_./-]+\.png$/.test(path) || path.includes('..')) return '';
try { return new URL(path, base).href; } catch { return ''; }
}
function itemCategoryLabel(category) {
const key = {
item: 'itemCatalogFilterItems',
lightcone: 'itemCatalogFilterLightCones',
relic: 'itemCatalogFilterRelics'
}[category];
return key ? t(key) : t('itemCatalogFilterItems');
}
function visibleItems() {
if (!catalog) return [];
const query = String(elements().search?.value || '').trim().toLocaleLowerCase();
const category = elements().filter?.value || 'all';
return catalog.items.filter(item => {
if (category === 'supported' && !item.supported) return false;
if (category !== 'all' && category !== 'supported' && item.category !== category) return false;
if (!query) return true;
if (String(item.id).includes(query)) return true;
return Object.values(item.names || {}).some(name => plainGameText(name).toLocaleLowerCase().includes(query));
});
}
function imageMarkup(item, source) {
const src = assetUrl(item.icon, source.assetBaseUrl);
if (!src) {
return '';
}
const fallback = assetUrl(item.icon, source.assetFallbackBaseUrl);
return '';
}
function localizedItemText(item, field) {
const values = item[field] || {};
return values[getCurrentLanguage()] || values.en || '';
}
function openItemDetails(itemId) {
const el = elements();
const item = catalog?.items.find(row => row.id === Number(itemId));
if (!item || !el.detail) return;
activeDetailId = item.id;
const name = displayName(item);
const src = assetUrl(item.icon, catalog.source.assetBaseUrl);
const fallback = assetUrl(item.icon, catalog.source.assetFallbackBaseUrl);
const description = localizedItemText(item, 'descriptions');
const lore = localizedItemText(item, 'backgroundDescriptions');
const rarity = Number(item.rarity);
const rarityText = Number.isInteger(rarity) && rarity > 0 && rarity <= 5 ? '★'.repeat(rarity) : '';
const category = itemCategoryLabel(item.category);
const type = [item.type, item.subType].filter((value, index, values) => value && values.indexOf(value) === index).join(' · ');
const path = item.path ? translatePath(item.path) : '';
const slot = item.slot ? translatePath(item.slot) : '';
const meta = [category, type, slot, path, rarityText].filter(Boolean).join(' · ');
el.detailName.textContent = name;
el.detailId.textContent = t('itemCatalogItemId', { id: item.id });
el.detailSource.textContent = (item.sourceTables || []).map(path => path.split('/').pop()).join(', ') || t('itemCatalogNoDetails');
el.detailMeta.textContent = meta || t('itemCatalogNoDetails');
el.detailMeta.dataset.rarity = rarityText ? String(rarity) : '';
el.detailDescription.textContent = plainGameText(description || lore || t('itemCatalogNoDescription'));
el.detailLoreSection.hidden = !description || !lore || description === lore;
el.detailLore.textContent = el.detailLoreSection.hidden ? '' : plainGameText(lore);
if (src) {
el.detailArt.innerHTML = '';
const image = el.detailArt.querySelector('img');
image.addEventListener('error', () => {
if (image.dataset.fallback && !image.dataset.fallbackTried) {
image.dataset.fallbackTried = 'true';
image.src = image.dataset.fallback;
} else {
el.detailArt.textContent = t('itemCatalogNoImage');
}
});
} else {
el.detailArt.textContent = t('itemCatalogNoImage');
}
const limit = Number(item.pileLimit);
const hasLimit = Number.isSafeInteger(limit) && limit > 0;
el.amount.min = '1';
if (hasLimit) {
el.amount.max = String(limit);
el.amountLimit.textContent = t('itemCatalogMaxQuantity', { count: limit.toLocaleString() });
} else {
el.amount.removeAttribute('max');
el.amountLimit.textContent = '';
}
el.amount.value = '1';
if (!el.detail.open) el.detail.showModal();
}
function updateSelectedAmount(delta = 0) {
const input = elements().amount;
if (!input) return;
const max = Number(input.max);
const hasMax = Number.isSafeInteger(max) && max > 0;
const current = Number.parseInt(input.value, 10);
const next = Math.max(1, (Number.isFinite(current) ? current : 1) + delta);
input.value = String(hasMax ? Math.min(max, next) : next);
}
function renderCatalog() {
const el = elements();
if (!el.grid || !catalog) return;
el.filter.setAttribute('aria-label', t('itemCatalogFilterLabel'));
const filtered = visibleItems();
const pages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
page = Math.min(page, pages - 1);
const start = page * PAGE_SIZE;
const rows = filtered.slice(start, start + PAGE_SIZE);
el.count.textContent = t('inventoryItemTotal', { count: filtered.length });
el.pageLabel.textContent = t('inventoryPage', { page: page + 1, pages, count: filtered.length });
el.previous.disabled = page === 0;
el.next.disabled = page >= pages - 1;
if (!rows.length) {
el.grid.innerHTML = '