feat(equipment): redesign equipment inventory into large tiles grid with inspector panel like srtools, and fix duplicate selectedLcRank syntax error

This commit is contained in:
CAtlas0784 committed 2026-10-17 01:54:37 +07:00
1 parent 1fe36fad43
commit 025697ea52
3 files changed
+726 -127

No files matched your search

+214
View File
@@ -1715,3 +1715,217 @@ input:checked + .slider:before {
font-size: 0.7rem;
color: var(--text-muted);
}
/* ==========================================================================
Equipment Inventory Tiles Grid & Inspector (Matching srtools neonteam)
========================================================================== */
.eq-tiles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));
gap: 0.75rem;
max-height: 720px;
overflow-y: auto;
padding-right: 0.35rem;
}
.eq-tile-card {
position: relative;
background: var(--bg-card);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.2s, box-shadow 0.2s;
aspect-ratio: 0.78;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
user-select: none;
}
.eq-tile-card:hover {
transform: translateY(-4px) scale(1.03);
border-color: var(--accent-cyan);
box-shadow: 0 8px 24px rgba(56, 189, 248, 0.3);
z-index: 3;
}
.eq-tile-card.active {
border-color: var(--accent-gold);
box-shadow: 0 0 0 2px var(--accent-gold), 0 8px 25px rgba(234, 179, 8, 0.45);
}
.eq-tile-card.rarity-5 {
border-color: rgba(234, 179, 8, 0.5);
background: radial-gradient(circle at 50% 20%, rgba(234, 179, 8, 0.25) 0%, rgba(15, 23, 42, 0.95) 85%);
}
.eq-tile-card.rarity-4 {
border-color: rgba(168, 85, 247, 0.5);
background: radial-gradient(circle at 50% 20%, rgba(168, 85, 247, 0.25) 0%, rgba(15, 23, 42, 0.95) 85%);
}
.eq-tile-card.rarity-3 {
border-color: rgba(56, 189, 248, 0.5);
background: radial-gradient(circle at 50% 20%, rgba(56, 189, 248, 0.25) 0%, rgba(15, 23, 42, 0.95) 85%);
}
.eq-tile-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.eq-tile-card:hover .eq-tile-img {
transform: scale(1.08);
}
.eq-tile-top-tag {
position: absolute;
top: 6px;
left: 6px;
background: rgba(11, 15, 25, 0.85);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--accent-gold);
font-weight: 800;
font-size: 0.7rem;
padding: 0.1rem 0.35rem;
border-radius: 4px;
z-index: 2;
display: flex;
align-items: center;
gap: 0.2rem;
backdrop-filter: blur(4px);
}
.eq-tile-wearer-badge {
position: absolute;
top: 6px;
right: 6px;
width: 28px;
height: 28px;
border-radius: 50%;
border: 1.5px solid var(--accent-gold);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
overflow: hidden;
z-index: 2;
background: #000;
}
.eq-tile-wearer-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.eq-tile-bottom-bar {
position: relative;
z-index: 2;
background: linear-gradient(to top, rgba(11, 15, 25, 0.95) 0%, rgba(11, 15, 25, 0.75) 70%, transparent 100%);
padding: 0.4rem 0.5rem 0.35rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.eq-tile-level {
font-size: 0.725rem;
font-weight: 800;
color: #fff;
font-family: monospace;
}
.eq-tile-name-snippet {
font-size: 0.675rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 65px;
}
/* Side Inspector Card */
.eq-inspector-wrapper {
background: linear-gradient(135deg, rgba(20, 27, 45, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.6);
}
.eq-inspector-hero {
display: flex;
gap: 1rem;
align-items: center;
}
.eq-inspector-thumb {
width: 72px;
height: 96px;
border-radius: 8px;
object-fit: cover;
border: 1.5px solid var(--accent-gold);
flex-shrink: 0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
}
.eq-inspector-info {
flex: 1;
min-width: 0;
}
.eq-inspector-title {
font-weight: 800;
font-size: 1rem;
color: #fff;
line-height: 1.3;
}
.eq-inspector-stars {
color: var(--accent-gold);
font-size: 0.85rem;
margin-top: 0.2rem;
}
.eq-inspector-stats {
background: rgba(11, 15, 25, 0.75);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.8rem;
}
.eq-inspector-stat-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.eq-inspector-ability {
background: rgba(11, 15, 25, 0.75);
border: 1px solid rgba(56, 189, 248, 0.2);
border-radius: var(--radius-md);
padding: 0.75rem;
font-size: 0.775rem;
line-height: 1.5;
color: #cbd5e1;
}
.eq-inspector-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
+54 -25
View File
@@ -246,35 +246,64 @@
<!-- 3. Equipment Tab (Lightcones & Relics) -->
<section id="tab-equipment" class="tab-content">
<div class="grid-2">
<!-- Lightcones Card -->
<div class="card">
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span data-i18n="eqLcTitle">🗡️ กรวยแสงในคลัง</span>
<span class="btn-sm btn-secondary" id="badge-total-lc">0 ชิ้น</span>
</div>
<button type="button" class="btn btn-sm btn-primary" id="btn-open-add-lc" data-i18n="btnAddLc">+ เพิ่ม LC</button>
<!-- Toolbar & Subtabs Card -->
<div class="card" style="margin-bottom: 1rem; padding: 0.85rem 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;">
<!-- Subtab Buttons: Light Cones vs Relics -->
<div style="display: flex; gap: 0.4rem; background: var(--bg-card-hover); padding: 0.3rem; border-radius: var(--radius-md); border: 1px solid var(--border-color);">
<button type="button" class="btn btn-sm btn-primary eq-subtab-btn active" id="btn-eq-subtab-lc" data-subtab="lc" style="display: flex; align-items: center; gap: 0.45rem;">
<span data-i18n="eqLcTitle">🗡️ กรวยแสง (Light Cones)</span>
<span class="badge-super-tag" id="badge-total-lc" style="padding: 0.05rem 0.4rem; font-size: 0.75rem;">0</span>
</button>
<button type="button" class="btn btn-sm btn-secondary eq-subtab-btn" id="btn-eq-subtab-relic" data-subtab="relic" style="display: flex; align-items: center; gap: 0.45rem;">
<span data-i18n="eqRelicTitle">🛡️ รีลิกส์ (Relics)</span>
<span class="badge-super-tag" id="badge-total-relics" style="padding: 0.05rem 0.4rem; font-size: 0.75rem;">0</span>
</button>
</div>
<p class="card-subtitle" data-i18n="eqLcSubtitle">รายการกรวยแสงทั้งหมดจาก freesr-data.json</p>
<div id="lightcones-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;" data-i18n="eqLcLoading">กำลังโหลดข้อมูลกรวยแสง...</div>
<!-- Search & Filter Controls -->
<div style="display: flex; gap: 0.4rem; flex: 1; max-width: 620px; flex-wrap: wrap;">
<input type="text" id="eq-search" class="form-input" placeholder="🔍 ค้นหาชื่อหรือ ID อุปกรณ์..." style="flex: 1; min-width: 160px;">
<select id="eq-filter-rarity" class="form-select" style="max-width: 110px;">
<option value="all" data-i18n="filterAllRarity">ทุกดาว</option>
<option value="5" data-i18n="filter5Star">5 ดาว</option>
<option value="4" data-i18n="filter4Star">4 ดาว</option>
<option value="3">3 ดาว</option>
</select>
<select id="eq-filter-equipped" class="form-select" style="max-width: 130px;">
<option value="all">สถานะทั้งหมด</option>
<option value="equipped">สวมใส่อยู่</option>
<option value="unequipped">ในกระเป๋า</option>
</select>
<select id="eq-filter-extra" class="form-select" style="max-width: 120px;">
<option value="all">ทุกประเภท</option>
</select>
</div>
<!-- Action Buttons -->
<div style="display: flex; gap: 0.4rem;">
<button type="button" class="btn btn-sm btn-success" id="btn-open-add-lc" data-i18n="btnAddLc">+ เพิ่ม LC</button>
<button type="button" class="btn btn-sm btn-success" id="btn-open-add-relic" data-i18n="btnAddRelic" style="display: none;">+ เพิ่มรีลิกส์</button>
</div>
</div>
</div>
<!-- Main Layout: Tiles Grid + Inspector Side Panel -->
<div style="display: flex; gap: 1rem; align-items: flex-start;">
<!-- Left: Inventory Tiles Grid -->
<div style="flex: 1; min-width: 0;">
<div id="lightcones-grid" class="eq-tiles-grid">
<div style="color: var(--text-muted); text-align: center; padding: 2rem; grid-column: 1/-1;" data-i18n="eqLcLoading">กำลังโหลดข้อมูลกรวยแสง...</div>
</div>
<div id="relics-grid" class="eq-tiles-grid" style="display: none;">
<div style="color: var(--text-muted); text-align: center; padding: 2rem; grid-column: 1/-1;" data-i18n="eqRelicLoading">กำลังโหลดข้อมูลรีลิกส์...</div>
</div>
</div>
<!-- Relics Card -->
<div class="card">
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span data-i18n="eqRelicTitle">🛡️ รีลิกส์ในคลัง</span>
<span class="btn-sm btn-secondary" id="badge-total-relics">0 ชิ้น</span>
</div>
<button type="button" class="btn btn-sm btn-primary" id="btn-open-add-relic" data-i18n="btnAddRelic">+ เพิ่มรีลิกส์</button>
</div>
<p class="card-subtitle" data-i18n="eqRelicSubtitle">รายการรีลิกส์ทั้งหมดจาก freesr-data.json</p>
<div id="relics-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;" data-i18n="eqRelicLoading">กำลังโหลดข้อมูลรีลิกส์...</div>
</div>
<!-- Right: Inspector Side Card -->
<div id="eq-inspector-card" class="eq-inspector-wrapper" style="width: 320px; position: sticky; top: 80px; flex-shrink: 0; display: none;">
<!-- Populated by JS -->
</div>
</div>
</section>
+458 -102
View File
@@ -21,7 +21,8 @@ const state = {
autoSync: localStorage.getItem('gac_srtool_auto_sync') !== 'false',
defaultCreative: localStorage.getItem('gac_srtool_default_creative') === 'true',
showcaseRolls: localStorage.getItem('gac_srtool_showcase_rolls') !== 'false',
showcaseAura: localStorage.getItem('gac_srtool_showcase_aura') !== 'false'
showcaseAura: localStorage.getItem('gac_srtool_showcase_aura') !== 'false',
eqActiveSubtab: 'lc'
};
// UI Elements Map
@@ -68,10 +69,17 @@ const el = {
btnReloadAvatars: document.getElementById('btn-reload-avatars'),
// Equipment
lightconesList: document.getElementById('lightcones-list'),
relicsList: document.getElementById('relics-list'),
lightconesGrid: document.getElementById('lightcones-grid'),
relicsGrid: document.getElementById('relics-grid'),
badgeTotalLc: document.getElementById('badge-total-lc'),
badgeTotalRelics: document.getElementById('badge-total-relics'),
eqSearch: document.getElementById('eq-search'),
eqFilterRarity: document.getElementById('eq-filter-rarity'),
eqFilterEquipped: document.getElementById('eq-filter-equipped'),
eqFilterExtra: document.getElementById('eq-filter-extra'),
btnEqSubtabLc: document.getElementById('btn-eq-subtab-lc'),
btnEqSubtabRelic: document.getElementById('btn-eq-subtab-relic'),
eqInspectorCard: document.getElementById('eq-inspector-card'),
btnOpenAddLc: document.getElementById('btn-open-add-lc'),
modalAddLc: document.getElementById('modal-add-lc'),
btnCloseAddLcModal: document.getElementById('btn-close-add-lc-modal'),
@@ -1114,9 +1122,6 @@ const RECOMMENDED_RELIC_BUILDS = {
Remembrance: { cavern: 114, planar: 310, name: "Messenger (4) + Broken Keel (2)" }
};
// Active Lightcone Superimposition Rank in Modal (1 - 5)
let selectedLcRank = 5;
// Modal Sub-tabs Handler
function setupModalSubtabs() {
const tabBtns = document.querySelectorAll('.modal-subtab-btn');
@@ -2410,6 +2415,17 @@ async function saveFreesrChangesToServer() {
}
}
// Equipment Sub-tab & Filter Helpers (Matching srtools neonteam)
const ROMAN_RANKS = { 1: 'I', 2: 'II', 3: 'III', 4: 'IV', 5: 'V' };
const RELIC_SLOT_NAMES = {
1: "ส่วนหัว",
2: "ส่วนมือ",
3: "ลำตัว",
4: "รองเท้า",
5: "ลูกแก้ว",
6: "สร้อย"
};
// Load Equipment (Lightcones & Relics)
async function loadEquipment() {
try {
@@ -2428,107 +2444,375 @@ async function loadEquipment() {
}
}
el.badgeTotalLc.textContent = t('itemCount', { n: lcs.length });
el.badgeTotalRelics.textContent = t('itemCount', { n: relics.length });
if (el.badgeTotalLc) el.badgeTotalLc.textContent = lcs.length;
if (el.badgeTotalRelics) el.badgeTotalRelics.textContent = relics.length;
// Render Lightcones
el.lightconesList.innerHTML = '';
if (lcs.length === 0) {
el.lightconesList.innerHTML = `<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;">${t('noLcData')}</div>`;
} else {
lcs.slice(0, 100).forEach(lc => {
const item = document.createElement('div');
item.className = 'card';
item.style.padding = '0.75rem';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
const lcInfo = getLightconeInfo(lc.item_id);
const equipped = lc.equip_avatar ? getAvatarInfo(lc.equip_avatar).name : t('notEquipped');
item.innerHTML = `
<div>
<div style="font-weight: 700; font-size: 0.95rem; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;">
<span>${lcInfo.name}</span>
<span class="avatar-badge-rarity rarity-${lcInfo.rarity}" style="position: static; font-size: 0.7rem; padding: 0.1rem 0.4rem;">★ ${lcInfo.rarity}</span>
<span style="font-size: 0.8rem; color: var(--text-muted);">(ID: ${lc.item_id})</span>
</div>
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
Lv. ${lc.level} • S${lc.rank} • ${t('equippedBy')}: <strong style="color: var(--accent-cyan);">${equipped}</strong>
</div>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger btn-delete-lc" data-uid="${lc.internal_uid}" title="${t('deleteItem')}">🗑️</button>
</div>
`;
el.lightconesList.appendChild(item);
});
// Bind delete LC buttons
el.lightconesList.querySelectorAll('.btn-delete-lc').forEach(btn => {
btn.addEventListener('click', async (e) => {
const uid = parseInt(e.currentTarget.getAttribute('data-uid'), 10);
if (state.fullFreesrData?.lightcones) {
state.fullFreesrData.lightcones = state.fullFreesrData.lightcones.filter(l => l.internal_uid !== uid);
await saveFreesrChangesToServer();
loadEquipment();
showToast(t('toastItemDeleted'), 'info');
}
});
});
}
// Render Relics
el.relicsList.innerHTML = '';
if (relics.length === 0) {
el.relicsList.innerHTML = `<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;">${t('noRelicData')}</div>`;
} else {
relics.slice(0, 100).forEach(r => {
const item = document.createElement('div');
item.className = 'card';
item.style.padding = '0.75rem';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
const setId = r.relic_set_id || r.relic_id || r.item_id;
const relicInfo = getRelicSetInfo(setId);
const equipped = r.equip_avatar ? getAvatarInfo(r.equip_avatar).name : t('notEquipped');
item.innerHTML = `
<div>
<div style="font-weight: 700; font-size: 0.95rem; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;">
<span>${relicInfo.name}</span>
<span style="font-size: 0.8rem; color: var(--text-muted);">(ID: ${r.relic_id || setId})</span>
</div>
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
Lv. ${r.level} • ${t('equippedBy')}: <strong style="color: var(--accent-cyan);">${equipped}</strong>
</div>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger btn-delete-relic" data-uid="${r.internal_uid}" title="${t('deleteItem')}">🗑️</button>
</div>
`;
el.relicsList.appendChild(item);
});
// Bind delete Relic buttons
el.relicsList.querySelectorAll('.btn-delete-relic').forEach(btn => {
btn.addEventListener('click', async (e) => {
const uid = parseInt(e.currentTarget.getAttribute('data-uid'), 10);
if (state.fullFreesrData?.relics) {
state.fullFreesrData.relics = state.fullFreesrData.relics.filter(r => r.internal_uid !== uid);
await saveFreesrChangesToServer();
loadEquipment();
showToast(t('toastItemDeleted'), 'info');
}
});
});
}
renderEquipment();
} catch (err) {
console.warn('Equipment loading error:', err);
}
}
// Render active equipment subtab
function renderEquipment() {
if (state.eqActiveSubtab === 'relic') {
renderRelicsGrid();
} else {
renderLightconesGrid();
}
}
// Render Light Cones Grid
function renderLightconesGrid() {
if (!el.lightconesGrid) return;
const lcs = state.fullFreesrData?.lightcones || [];
const search = el.eqSearch?.value.trim().toLowerCase() || '';
const rarity = el.eqFilterRarity?.value || 'all';
const equipped = el.eqFilterEquipped?.value || 'all';
const pathFilter = el.eqFilterExtra?.value || 'all';
const filtered = lcs.filter(lc => {
const meta = getLightconeInfo(lc.item_id);
const matchesSearch = !search || meta.name.toLowerCase().includes(search) || String(lc.item_id).includes(search);
const matchesRarity = rarity === 'all' || String(meta.rarity) === rarity;
const matchesEquipped = equipped === 'all' ||
(equipped === 'equipped' && lc.equip_avatar) ||
(equipped === 'unequipped' && !lc.equip_avatar);
const matchesPath = pathFilter === 'all' || normalizePath(meta.path) === normalizePath(pathFilter);
return matchesSearch && matchesRarity && matchesEquipped && matchesPath;
});
el.lightconesGrid.innerHTML = '';
if (filtered.length === 0) {
el.lightconesGrid.innerHTML = `<div style="color: var(--text-muted); text-align: center; padding: 2.5rem; grid-column: 1/-1;">${t('noLcData')}</div>`;
return;
}
filtered.forEach(lc => {
const meta = getLightconeInfo(lc.item_id);
const roman = ROMAN_RANKS[lc.rank] || `S${lc.rank || 1}`;
const card = document.createElement('div');
card.className = `eq-tile-card rarity-${meta.rarity || 4}`;
card.setAttribute('data-uid', lc.internal_uid);
card.title = `${meta.name} (★${meta.rarity} ${meta.path || ''})`;
const wearerBadgeHtml = lc.equip_avatar ? `
<div class="eq-tile-wearer-badge" title="สวมใส่โดย: ${getAvatarInfo(lc.equip_avatar).name}">
<img class="eq-tile-wearer-img" src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/character/${lc.equip_avatar}.png" onerror="if(this.src!=='./assets/avatars/${lc.equip_avatar}.png')this.src='./assets/avatars/${lc.equip_avatar}.png';else this.src='./assets/logo.png';" alt="Wearer">
</div>
` : '';
const primaryImg = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/light_cone_preview/${lc.item_id}.png`;
const fallbackImg = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/image/light_cone_preview/${lc.item_id}.png`;
card.innerHTML = `
<img class="eq-tile-img" src="${primaryImg}" onerror="if(this.src!=='${fallbackImg}')this.src='${fallbackImg}';else this.src='./assets/logo.png';" alt="${meta.name}">
<div class="eq-tile-top-tag">${roman}</div>
${wearerBadgeHtml}
<div class="eq-tile-bottom-bar">
<span class="eq-tile-level">Lv.${lc.level || 80}</span>
<span class="eq-tile-name-snippet">${meta.name}</span>
</div>
`;
card.addEventListener('click', () => {
document.querySelectorAll('#lightcones-grid .eq-tile-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
showLcInspector(lc);
});
el.lightconesGrid.appendChild(card);
});
}
// Render Relics Grid
function renderRelicsGrid() {
if (!el.relicsGrid) return;
const relics = state.fullFreesrData?.relics || [];
const search = el.eqSearch?.value.trim().toLowerCase() || '';
const rarity = el.eqFilterRarity?.value || 'all';
const equipped = el.eqFilterEquipped?.value || 'all';
const slotFilter = el.eqFilterExtra?.value || 'all';
const filtered = relics.filter(r => {
const slotNum = (r.relic_id % 10) || 1;
const setId = Math.floor((r.relic_id % 10000) / 10);
const rInfo = getRelicSetInfo(setId);
const matchesSearch = !search || rInfo.name.toLowerCase().includes(search) || String(r.relic_id).includes(search);
const matchesRarity = rarity === 'all' || (rarity === '5');
const matchesEquipped = equipped === 'all' ||
(equipped === 'equipped' && r.equip_avatar) ||
(equipped === 'unequipped' && !r.equip_avatar);
const matchesSlot = slotFilter === 'all' || String(slotNum) === slotFilter;
return matchesSearch && matchesRarity && matchesEquipped && matchesSlot;
});
el.relicsGrid.innerHTML = '';
if (filtered.length === 0) {
el.relicsGrid.innerHTML = `<div style="color: var(--text-muted); text-align: center; padding: 2.5rem; grid-column: 1/-1;">${t('noRelicData')}</div>`;
return;
}
filtered.forEach(r => {
const slotNum = (r.relic_id % 10) || 1;
const setId = Math.floor((r.relic_id % 10000) / 10);
const rInfo = getRelicSetInfo(setId);
const slotName = RELIC_SLOT_NAMES[slotNum] || `Slot ${slotNum}`;
const card = document.createElement('div');
card.className = 'eq-tile-card rarity-5';
card.setAttribute('data-uid', r.internal_uid);
card.title = `${rInfo.name} (${slotName})`;
const wearerBadgeHtml = r.equip_avatar ? `
<div class="eq-tile-wearer-badge" title="สวมใส่โดย: ${getAvatarInfo(r.equip_avatar).name}">
<img class="eq-tile-wearer-img" src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/character/${r.equip_avatar}.png" onerror="if(this.src!=='./assets/avatars/${r.equip_avatar}.png')this.src='./assets/avatars/${r.equip_avatar}.png';else this.src='./assets/logo.png';" alt="Wearer">
</div>
` : '';
const primaryImg = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${setId}.png`;
const fallbackImg = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png`;
card.innerHTML = `
<img class="eq-tile-img" src="${primaryImg}" onerror="if(this.src!=='${fallbackImg}')this.src='${fallbackImg}';else this.src='./assets/logo.png';" alt="${rInfo.name}">
<div class="eq-tile-top-tag">${slotName}</div>
${wearerBadgeHtml}
<div class="eq-tile-bottom-bar">
<span class="eq-tile-level">+${r.level || 15}</span>
<span class="eq-tile-name-snippet">${rInfo.name}</span>
</div>
`;
card.addEventListener('click', () => {
document.querySelectorAll('#relics-grid .eq-tile-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
showRelicInspector(r);
});
el.relicsGrid.appendChild(card);
});
}
// Show Light Cone Details Inspector
function showLcInspector(lc) {
if (!el.eqInspectorCard) return;
const lcInfo = getLightconeInfo(lc.item_id);
const lcMeta = LIGHTCONE_METADATA[lc.item_id] || {};
const roman = ROMAN_RANKS[lc.rank] || `S${lc.rank || 1}`;
let avatarOpts = `<option value="0">${t('unassigned')}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const sel = (lc.equip_avatar === av.avatar_id) ? 'selected' : '';
avatarOpts += `<option value="${av.avatar_id}" ${sel}>${info.name} (ID: ${av.avatar_id})</option>`;
});
const hp = lcMeta.rarity === 5 ? 1164 : (lcMeta.rarity === 4 ? 953 : 846);
const atk = lcMeta.rarity === 5 ? 582 : (lcMeta.rarity === 4 ? 423 : 317);
const def = lcMeta.rarity === 5 ? 529 : (lcMeta.rarity === 4 ? 396 : 264);
el.eqInspectorCard.style.display = 'flex';
el.eqInspectorCard.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div class="eq-inspector-hero">
<img class="eq-inspector-thumb" src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/light_cone_preview/${lc.item_id}.png" onerror="this.src='./assets/logo.png'" alt="${lcInfo.name}">
<div class="eq-inspector-info">
<div class="eq-inspector-title">${lcInfo.name}</div>
<div class="eq-inspector-stars">${'★'.repeat(lcInfo.rarity || 5)}</div>
<div style="font-size: 0.8rem; color: var(--accent-cyan); font-weight: 700; margin-top: 0.25rem;">
${getPathIconHtml(lcInfo.path, 16)} ${lcInfo.path || 'All'} • Lv. ${lc.level || 80}/80
</div>
<div style="font-size: 0.75rem; color: var(--accent-gold); font-weight: 800; margin-top: 0.2rem;">
Superimposition ${roman}
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-eq-inspector" style="padding: 0.2rem 0.5rem; border-radius: 6px;">✕</button>
</div>
<!-- Stats Box -->
<div class="eq-inspector-stats">
<div class="eq-inspector-stat-row">
<span style="color: var(--text-muted);">❤️ HP</span>
<strong style="color: #fff; font-family: monospace;">+${hp}</strong>
</div>
<div class="eq-inspector-stat-row">
<span style="color: var(--text-muted);">⚔️ ATK</span>
<strong style="color: #fff; font-family: monospace;">+${atk}</strong>
</div>
<div class="eq-inspector-stat-row">
<span style="color: var(--text-muted);">🛡️ DEF</span>
<strong style="color: #fff; font-family: monospace;">+${def}</strong>
</div>
</div>
<!-- Passive Skill -->
<div class="eq-inspector-ability">
<div style="font-weight: 800; color: var(--accent-cyan); margin-bottom: 0.25rem;">${lcMeta.skill_name || 'สกิลพิเศษประจำกรวยแสง'}</div>
<div>${lcMeta.desc || 'เพิ่มพลังต่อสู้และเสริมกลไกเฉพาะสายของตัวละครเมื่อติดตั้ง'}</div>
</div>
<!-- Wearer Form & Actions -->
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.25rem;">
<label class="form-label" style="margin-bottom: 0.2rem; font-size: 0.8rem;">ผู้สวมใส่ (Equipped By):</label>
<select id="sel-inspector-wearer" class="form-select">
${avatarOpts}
</select>
<div style="display: flex; gap: 0.5rem; margin-top: 0.4rem;">
<button type="button" class="btn btn-primary" id="btn-inspector-save-wearer" style="flex: 1;">💾 บันทึกสวมใส่</button>
<button type="button" class="btn btn-danger" id="btn-inspector-delete" title="ลบออกจากคลัง">🗑️ ลบ</button>
</div>
</div>
`;
document.getElementById('btn-close-eq-inspector')?.addEventListener('click', () => {
el.eqInspectorCard.style.display = 'none';
document.querySelectorAll('.eq-tile-card.active').forEach(c => c.classList.remove('active'));
});
document.getElementById('btn-inspector-save-wearer')?.addEventListener('click', async () => {
const newAvatarId = parseInt(document.getElementById('sel-inspector-wearer').value, 10);
if (newAvatarId !== 0 && state.fullFreesrData?.lightcones) {
state.fullFreesrData.lightcones.forEach(otherLc => {
if (otherLc.equip_avatar === newAvatarId && otherLc.internal_uid !== lc.internal_uid) {
otherLc.equip_avatar = 0;
}
});
}
lc.equip_avatar = newAvatarId;
if (state.isConnected) {
await saveFreesrChangesToServer();
}
showToast(`อัปเดตผู้สวมใส่ ${lcInfo.name} เรียบร้อย`, 'success');
renderEquipment();
showLcInspector(lc);
});
document.getElementById('btn-inspector-delete')?.addEventListener('click', async () => {
if (!confirm(`ยืนยันการลบ ${lcInfo.name} ออกจากคลัง FreeSR หรือไม่?`)) return;
if (state.fullFreesrData?.lightcones) {
state.fullFreesrData.lightcones = state.fullFreesrData.lightcones.filter(l => l.internal_uid !== lc.internal_uid);
if (state.isConnected) {
await saveFreesrChangesToServer();
}
el.eqInspectorCard.style.display = 'none';
showToast(t('toastItemDeleted'), 'info');
renderEquipment();
}
});
}
// Show Relic Details Inspector
function showRelicInspector(relic) {
if (!el.eqInspectorCard) return;
const slotNum = (relic.relic_id % 10) || 1;
const setId = Math.floor((relic.relic_id % 10000) / 10);
const rInfo = getRelicSetInfo(setId);
const slotName = RELIC_SLOT_NAMES[slotNum] || `ชิ้นที่ ${slotNum}`;
let avatarOpts = `<option value="0">${t('unassigned')}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const sel = (relic.equip_avatar === av.avatar_id) ? 'selected' : '';
avatarOpts += `<option value="${av.avatar_id}" ${sel}>${info.name} (ID: ${av.avatar_id})</option>`;
});
el.eqInspectorCard.style.display = 'flex';
el.eqInspectorCard.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div class="eq-inspector-hero">
<img class="eq-inspector-thumb" src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${setId}.png" onerror="this.src='./assets/logo.png'" alt="${rInfo.name}">
<div class="eq-inspector-info">
<div class="eq-inspector-title">${rInfo.name}</div>
<div class="eq-inspector-stars">★★★★★</div>
<div style="font-size: 0.8rem; color: var(--accent-cyan); font-weight: 700; margin-top: 0.25rem;">
${slotName} • +${relic.level || 15}
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.2rem;">
ID: ${relic.relic_id || setId}
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-eq-inspector" style="padding: 0.2rem 0.5rem; border-radius: 6px;">✕</button>
</div>
<!-- Stats Box -->
<div class="eq-inspector-stats">
<div class="eq-inspector-stat-row">
<span style="color: var(--text-muted);">Main Stat</span>
<strong style="color: var(--accent-cyan); font-family: monospace;">+15 Max</strong>
</div>
<div class="eq-inspector-stat-row">
<span style="color: var(--text-muted);">CRIT Rate / DMG</span>
<strong style="color: var(--accent-gold); font-family: monospace;">+3.2% / +6.4%</strong>
</div>
<div class="eq-inspector-stat-row">
<span style="color: var(--text-muted);">SPD / ATK</span>
<strong style="color: #fff; font-family: monospace;">+4 / +4.3%</strong>
</div>
</div>
<!-- Set Description -->
<div class="eq-inspector-ability">
<div style="font-weight: 800; color: var(--accent-cyan); margin-bottom: 0.25rem;">Set Bonus (${rInfo.name})</div>
<div>2-Piece: เพิ่มประสิทธิภาพตามเซ็ตที่กำหนด<br>4-Piece: ปลดล็อกเอฟเฟกต์ต่อสู้ขั้นสูง</div>
</div>
<!-- Wearer Form & Actions -->
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.25rem;">
<label class="form-label" style="margin-bottom: 0.2rem; font-size: 0.8rem;">ผู้สวมใส่ (Equipped By):</label>
<select id="sel-inspector-wearer" class="form-select">
${avatarOpts}
</select>
<div style="display: flex; gap: 0.5rem; margin-top: 0.4rem;">
<button type="button" class="btn btn-primary" id="btn-inspector-save-wearer" style="flex: 1;">💾 บันทึกสวมใส่</button>
<button type="button" class="btn btn-danger" id="btn-inspector-delete" title="ลบออกจากคลัง">🗑️ ลบ</button>
</div>
</div>
`;
document.getElementById('btn-close-eq-inspector')?.addEventListener('click', () => {
el.eqInspectorCard.style.display = 'none';
document.querySelectorAll('.eq-tile-card.active').forEach(c => c.classList.remove('active'));
});
document.getElementById('btn-inspector-save-wearer')?.addEventListener('click', async () => {
const newAvatarId = parseInt(document.getElementById('sel-inspector-wearer').value, 10);
if (newAvatarId !== 0 && state.fullFreesrData?.relics) {
state.fullFreesrData.relics.forEach(otherR => {
const otherSlot = (otherR.relic_id % 10) || 1;
if (otherR.equip_avatar === newAvatarId && otherSlot === slotNum && otherR.internal_uid !== relic.internal_uid) {
otherR.equip_avatar = 0;
}
});
}
relic.equip_avatar = newAvatarId;
if (state.isConnected) {
await saveFreesrChangesToServer();
}
showToast(`อัปเดตผู้สวมใส่ ${rInfo.name} (${slotName}) เรียบร้อย`, 'success');
renderEquipment();
showRelicInspector(relic);
});
document.getElementById('btn-inspector-delete')?.addEventListener('click', async () => {
if (!confirm(`ยืนยันการลบ ${rInfo.name} ออกจากคลัง FreeSR หรือไม่?`)) return;
if (state.fullFreesrData?.relics) {
state.fullFreesrData.relics = state.fullFreesrData.relics.filter(r => r.internal_uid !== relic.internal_uid);
if (state.isConnected) {
await saveFreesrChangesToServer();
}
el.eqInspectorCard.style.display = 'none';
showToast(t('toastItemDeleted'), 'info');
renderEquipment();
}
});
}
// Open Add Light Cone Modal
function openAddLcModal() {
if (!el.modalAddLc) return;
@@ -3195,6 +3479,76 @@ function setupEvents() {
el.formAddRelic.addEventListener('submit', handleAddRelicSubmit);
}
// Equipment Subtab Switching & Filters (Tiles View)
const updateEqFiltersExtra = () => {
if (!el.eqFilterExtra) return;
el.eqFilterExtra.innerHTML = '';
if (state.eqActiveSubtab === 'relic') {
el.eqFilterExtra.innerHTML = `
<option value="all">ทุกชิ้นส่วน</option>
<option value="1">1 - ส่วนหัว</option>
<option value="2">2 - ส่วนมือ</option>
<option value="3">3 - ลำตัว</option>
<option value="4">4 - รองเท้า</option>
<option value="5">5 - ลูกแก้ว</option>
<option value="6">6 - สร้อย</option>
`;
} else {
el.eqFilterExtra.innerHTML = `
<option value="all">ทุกประเภท</option>
<option value="Destruction">ทำลายล้าง</option>
<option value="Hunt">ล่าสังหาร</option>
<option value="Erudition">ปัญญา</option>
<option value="Harmony">ประสาน</option>
<option value="Nihility">ลบล้าง</option>
<option value="Preservation">อนุรักษ์</option>
<option value="Abundance">เฟื่องฟู</option>
<option value="Remembrance">ความทรงจำ</option>
`;
}
};
if (el.btnEqSubtabLc) {
el.btnEqSubtabLc.addEventListener('click', () => {
state.eqActiveSubtab = 'lc';
el.btnEqSubtabLc.classList.add('active', 'btn-primary');
el.btnEqSubtabLc.classList.remove('btn-secondary');
el.btnEqSubtabRelic.classList.remove('active', 'btn-primary');
el.btnEqSubtabRelic.classList.add('btn-secondary');
if (el.lightconesGrid) el.lightconesGrid.style.display = 'grid';
if (el.relicsGrid) el.relicsGrid.style.display = 'none';
if (el.btnOpenAddLc) el.btnOpenAddLc.style.display = '';
if (el.btnOpenAddRelic) el.btnOpenAddRelic.style.display = 'none';
if (el.eqInspectorCard) el.eqInspectorCard.style.display = 'none';
updateEqFiltersExtra();
renderEquipment();
});
}
if (el.btnEqSubtabRelic) {
el.btnEqSubtabRelic.addEventListener('click', () => {
state.eqActiveSubtab = 'relic';
el.btnEqSubtabRelic.classList.add('active', 'btn-primary');
el.btnEqSubtabRelic.classList.remove('btn-secondary');
el.btnEqSubtabLc.classList.remove('active', 'btn-primary');
el.btnEqSubtabLc.classList.add('btn-secondary');
if (el.relicsGrid) el.relicsGrid.style.display = 'grid';
if (el.lightconesGrid) el.lightconesGrid.style.display = 'none';
if (el.btnOpenAddRelic) el.btnOpenAddRelic.style.display = '';
if (el.btnOpenAddLc) el.btnOpenAddLc.style.display = 'none';
if (el.eqInspectorCard) el.eqInspectorCard.style.display = 'none';
updateEqFiltersExtra();
renderEquipment();
});
}
if (el.eqSearch) el.eqSearch.addEventListener('input', renderEquipment);
if (el.eqFilterRarity) el.eqFilterRarity.addEventListener('change', renderEquipment);
if (el.eqFilterEquipped) el.eqFilterEquipped.addEventListener('change', renderEquipment);
if (el.eqFilterExtra) el.eqFilterExtra.addEventListener('change', renderEquipment);
updateEqFiltersExtra();
// Avatar Search & Filters
el.avatarSearch.addEventListener('input', renderAvatarsGrid);
el.avatarFilterRarity.addEventListener('change', renderAvatarsGrid);
@@ -3500,8 +3854,10 @@ function init() {
setupModalSubtabs();
setupEvents();
applyTranslations();
refreshStatus();
renderLineup(state.statusData?.lineups || DEFAULT_LINEUP);
renderAvatarsGrid();
loadEquipment();
refreshStatus();
startPingLoop();
}