fix: redesign relic gear UI, click-to-pick LC modal, fix showcase export aspect ratio, clean card buttons and add top-bar All E0S1 button

This commit is contained in:
CAtlas0784 committed 2026-10-17 03:20:18 +07:00
1 parent 42efabfe9a
commit 030d02f7a1
4 files changed
+639 -90

No files matched your search

+92 -26
View File
@@ -1471,20 +1471,16 @@ input:checked + .slider:before {
background: linear-gradient(135deg, #0d111a 0%, #151a27 50%, #0f131f 100%);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
padding: 1.25rem;
padding: 1.5rem;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.7), inset 0 0 40px rgba(56, 189, 248, 0.03);
display: grid;
grid-template-columns: 240px 1.15fr 1.35fr;
grid-template-columns: 360px 320px 370px;
gap: 1.25rem;
position: relative;
overflow: hidden;
}
@media (max-width: 960px) {
.showcase-card-layout {
grid-template-columns: 1fr;
gap: 1.25rem;
}
min-width: 1050px;
width: 1050px;
box-sizing: border-box;
}
.showcase-splash-col {
@@ -1495,7 +1491,8 @@ input:checked + .slider:before {
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 380px;
min-height: 520px;
width: 360px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
@@ -2058,26 +2055,58 @@ input:checked + .slider:before {
/* Relics 6 Slots Layout */
.relics-slots-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
gap: 0.85rem;
}
.relic-slot-box {
background: rgba(18, 24, 38, 0.65);
background: rgba(18, 24, 38, 0.7);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.85rem;
padding: 0.85rem 1rem;
display: flex;
gap: 0.85rem;
align-items: center;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
}
.relic-slot-box.empty {
border: 1px dashed rgba(255, 255, 255, 0.18);
background: rgba(14, 19, 31, 0.5);
}
.relic-slot-box:hover {
border-color: rgba(56, 189, 248, 0.5);
background: rgba(18, 24, 38, 0.85);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
border-color: rgba(56, 189, 248, 0.6);
background: rgba(22, 30, 48, 0.9);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.relic-slot-box .btn-slot-unequip-corner {
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.35);
color: #f87171;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s;
z-index: 5;
}
.relic-slot-box .btn-slot-unequip-corner:hover {
background: #ef4444;
color: #fff;
transform: scale(1.1);
}
.relic-slot-icon-wrap {
@@ -2100,7 +2129,7 @@ input:checked + .slider:before {
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.85);
font-size: 0.6rem;
text-align: center;
font-weight: 800;
@@ -2114,33 +2143,70 @@ input:checked + .slider:before {
}
.relic-slot-title {
font-size: 0.85rem;
font-weight: 700;
font-size: 0.875rem;
font-weight: 800;
color: var(--text-highlight);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.relic-slot-lvl {
font-size: 0.7rem;
font-weight: 800;
color: #000;
background: var(--accent-gold);
padding: 0.05rem 0.4rem;
border-radius: 4px;
}
.relic-slot-main {
font-size: 0.75rem;
font-weight: 700;
color: var(--accent-gold);
font-size: 0.8rem;
font-weight: 800;
color: var(--accent-cyan);
margin-top: 0.2rem;
}
.relic-slot-subs {
font-size: 0.7rem;
font-size: 0.725rem;
color: var(--text-muted);
margin-top: 0.2rem;
line-height: 1.35;
}
.relic-slot-actions {
/* Lightcone Picker Card */
.lc-picker-card {
background: rgba(18, 24, 38, 0.7);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.65rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
gap: 0.65rem;
align-items: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.lc-picker-card:hover {
background: rgba(28, 38, 60, 0.95);
border-color: rgba(56, 189, 248, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
}
.lc-picker-card.active-equipped {
border-color: var(--accent-gold);
background: rgba(251, 191, 36, 0.1);
}
.lc-picker-thumb {
width: 44px;
height: 60px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
border: 1px solid var(--border-color);
}
/* Rich Skills & Traces */
+94 -9
View File
@@ -232,7 +232,8 @@
<option value="Elation">ปีติ</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="btn btn-primary" id="btn-all-avatars-e0s1" data-i18n="btnAllE0S1" style="background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); border-color: #38bdf8;">⚡ ปรับ E0S1 ทุกตัว</button>
<button class="btn btn-gold" id="btn-max-all-avatars" data-i18n="btnMaxAll">⭐ ปรับ Max E6 ทุกตัว</button>
<button class="btn btn-secondary" id="btn-reload-avatars" data-i18n="btnReload">🔄 รีโหลด</button>
</div>
@@ -633,10 +634,7 @@
📋 ข้อมูลภาพรวม
</button>
<button type="button" class="modal-subtab-btn" data-modaltab="modaltab-relics" data-i18n="modalTabRelics">
🛡️ รีลิกส์
</button>
<button type="button" class="modal-subtab-btn" data-modaltab="modaltab-lightcone" data-i18n="modalTabLightcone">
🗡️ กรวยแสง
🛡️ รีลิกส์ & อุปกรณ์
</button>
<button type="button" class="modal-subtab-btn" data-modaltab="modaltab-skills" data-i18n="modalTabSkills">
⚡ สกิลและร่องรอย
@@ -645,7 +643,7 @@
⚙️ เลเวล & Eidolon
</button>
<button type="button" class="modal-subtab-btn" data-modaltab="modaltab-showcase" data-i18n="modalTabShowcase">
🖼️ การ์ดแสดงผล
🖼️ การ์ดสรุปบิลด์
</button>
</div>
@@ -731,6 +729,31 @@
</div>
</div>
<!-- Equipped Lightcone Slot in Equipment Tab -->
<div class="card" id="relics-equipped-lc-box" style="background: rgba(18, 24, 38, 0.75); border: 1px solid var(--border-color); padding: 0.85rem 1rem; margin-bottom: 1rem; cursor: pointer; transition: all 0.2s;" title="คลิกเพื่อเลือกเปลี่ยน Light Cone">
<div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center;">
<span style="color: var(--accent-cyan); font-weight: 800;">🗡️ กรวยแสงที่สวมใส่ (คลิกเพื่อเลือกเปลี่ยน)</span>
<span style="color: var(--accent-gold); font-size: 0.75rem; font-weight: 700;">เปลี่ยนกรวยแสง ➔</span>
</div>
<div style="display: flex; gap: 0.85rem; align-items: center;">
<img id="relics-tab-lc-thumb" src="" alt="" style="width: 46px; height: 64px; border-radius: 6px; object-fit: cover; border: 1px solid var(--accent-gold);">
<div style="min-width: 0; flex: 1;">
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<span id="relics-tab-lc-name" style="font-weight: 800; font-size: 0.95rem; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">-</span>
<span id="relics-tab-lc-stars" style="color: var(--accent-gold); font-size: 0.8rem; letter-spacing: 1px;">★★★★★</span>
</div>
<div style="display: flex; gap: 0.6rem; align-items: center; margin-top: 0.25rem; font-size: 0.775rem; color: var(--text-muted); flex-wrap: wrap;">
<span id="relics-tab-lc-rank" style="color: var(--accent-gold); font-weight: 800;">S5</span>
<span>•</span>
<span id="relics-tab-lc-level" style="font-weight: 700; color: #fff;">Lv. 80 / 80</span>
<span>•</span>
<span id="relics-tab-lc-stats" style="color: var(--accent-cyan); font-family: monospace;">HP 952 / ATK 582 / DEF 529</span>
</div>
</div>
<button type="button" class="btn btn-sm btn-primary" style="pointer-events: none;">เลือกเปลี่ยน</button>
</div>
</div>
<!-- 6 Relic Slots Grid -->
<div class="relics-slots-grid" id="modal-relics-slots-grid">
<!-- Dynamically generated 6 slot boxes -->
@@ -1062,9 +1085,11 @@
</div>
</div>
<!-- The Showcase Card UI Container -->
<div id="showcase-card-container" class="showcase-card-layout">
<!-- Rendered dynamically by JS -->
<!-- The Showcase Card UI Container with horizontal scroll wrapper -->
<div class="showcase-scroll-wrapper" style="overflow-x: auto; padding-bottom: 0.5rem; max-width: 100%;">
<div id="showcase-card-container" class="showcase-card-layout">
<!-- Rendered dynamically by JS -->
</div>
</div>
</div>
@@ -1235,6 +1260,66 @@
</div>
</div>
<!-- Modal: Select Lightcone for Avatar -->
<div class="modal-overlay" id="modal-select-lightcone">
<div class="modal-content" style="max-width: 720px; max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header">
<h3 id="modal-lc-picker-title" style="font-weight: 800;">
🗡️ เลือกสวมใส่กรวยแสง (Light Cone)
</h3>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-lc-picker">✕</button>
</div>
<!-- Search & Filters -->
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap;">
<input type="text" id="lc-picker-search" class="form-input" placeholder="🔍 ค้นหาชื่อหรือ ID กรวยแสง..." style="flex: 1; min-width: 160px;">
<select id="lc-picker-filter-path" class="form-select" style="max-width: 140px;">
<option value="all">ทุกสาย</option>
<option value="same" selected>ตรงสายตัวละคร</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>
<option value="Elation">ปีติ</option>
</select>
<select id="lc-picker-rank" class="form-select" style="max-width: 110px;" title="ระดับการขัดเกลาเมื่อสวมใส่">
<option value="5" selected>ขัดเกลา S5</option>
<option value="4">ขัดเกลา S4</option>
<option value="3">ขัดเกลา S3</option>
<option value="2">ขัดเกลา S2</option>
<option value="1">ขัดเกลา S1</option>
</select>
</div>
<!-- Signature Recommendation Box -->
<div id="lc-picker-signature-box" style="display: none; background: rgba(251, 191, 36, 0.1); border: 1px dashed rgba(251, 191, 36, 0.5); border-radius: var(--radius-sm); padding: 0.65rem 0.85rem; margin-bottom: 0.75rem; align-items: center; justify-content: space-between; gap: 0.75rem;">
<div style="display: flex; gap: 0.6rem; align-items: center; min-width: 0;">
<img id="lc-picker-sig-thumb" src="" alt="" style="width: 38px; height: 52px; border-radius: 4px; object-fit: cover; border: 1px solid var(--accent-gold);">
<div style="min-width: 0;">
<div style="font-size: 0.7rem; color: var(--accent-gold); font-weight: 800;">⭐ กรวยแสงประจำตัวแนะนำ (Signature)</div>
<div id="lc-picker-sig-name" style="font-weight: 700; font-size: 0.875rem; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">-</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-gold" id="btn-lc-picker-quick-sig">⚡ สวมใส่ชิ้นนี้</button>
</div>
<!-- Lightcones List Grid -->
<div id="lc-picker-list" style="flex: 1; min-height: 0; max-height: 380px; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.5rem; padding-right: 0.35rem;">
<!-- Injected by JS -->
</div>
<!-- Footer Actions -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem; border-top: 1px solid var(--border-color); padding-top: 0.75rem;">
<button type="button" class="btn btn-sm btn-secondary" id="btn-lc-picker-unequip" style="color: var(--accent-red);">✕ ถอดกรวยแสงออก</button>
<button type="button" class="btn btn-sm btn-secondary" id="btn-lc-picker-close">ปิด</button>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div class="toast-container" id="toast-container"></div>
+403 -53
View File
@@ -546,7 +546,7 @@ function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(elem => {
const key = elem.getAttribute('data-i18n');
const val = t(key);
if (val) elem.textContent = val;
if (val && val !== key) elem.textContent = val;
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(elem => {
@@ -1129,37 +1129,9 @@ function renderAvatarsGrid() {
<span class="avatar-lvl">Lv. ${item.level}</span>
<span class="avatar-eidolon ${item.rank === 6 ? 'e6' : ''}">E${item.rank}</span>
</div>
<div style="display: flex; gap: 0.3rem; align-items: center;">
<button type="button" class="btn btn-sm btn-card-e0s1" title="ตั้งค่า E0S1 ทันที">⚡ E0S1</button>
<button type="button" class="btn btn-sm btn-card-e6s5" title="ตั้งค่า E6S5 เต็มพลัง">⭐ E6S5</button>
<button type="button" class="btn btn-sm btn-secondary btn-card-showcase" title="ดูการ์ดแสดงผล Build">🖼️</button>
</div>
</div>
`;
// Quick Showcase Card Button
const btnShowcase = card.querySelector('.btn-card-showcase');
btnShowcase?.addEventListener('click', (e) => {
e.stopPropagation();
openAvatarDetailModal(item);
const showcaseTabBtn = document.querySelector('.modal-subtab-btn[data-modaltab="modaltab-showcase"]');
if (showcaseTabBtn) showcaseTabBtn.click();
});
// Quick E0S1 Button on Card Outside
const btnE0S1 = card.querySelector('.btn-card-e0s1');
btnE0S1?.addEventListener('click', async (e) => {
e.stopPropagation();
await setAvatarE0S1(item.avatar_id);
});
// Quick E6S5 Button on Card Outside
const btnE6S5 = card.querySelector('.btn-card-e6s5');
btnE6S5?.addEventListener('click', async (e) => {
e.stopPropagation();
await setAvatarE6S5(item.avatar_id);
});
// Clicking card opens full character build modal
card.addEventListener('click', () => {
openAvatarDetailModal(item);
@@ -1899,13 +1871,52 @@ function renderModalOverviewTab(avatarId) {
}
}
// Render Relics 6 Slots Tab
// Render Relics & Gear Tab (6 Slots + Equipped Lightcone)
function renderModalRelicsTab(avatarId) {
const aid = parseInt(avatarId, 10);
const meta = getAvatarInfo(aid);
if (!el.modalRelicsSlotsGrid) return;
el.modalRelicsSlotsGrid.innerHTML = '';
// 1. Equipped Lightcone in Relics Tab
const lc = (state.fullFreesrData?.lightcones || []).find(l => l.equip_avatar === aid);
const lcId = lc?.item_id || SIGNATURE_LIGHTCONES[aid] || 21001;
const lcInfo = getLightconeInfo(lcId);
const lcRank = lc?.rank || 5;
const lcLevel = lc?.level || 80;
const lcMeta = LIGHTCONE_METADATA[lcId] || {};
const lcThumbEl = document.getElementById('relics-tab-lc-thumb');
const lcNameEl = document.getElementById('relics-tab-lc-name');
const lcStarsEl = document.getElementById('relics-tab-lc-stars');
const lcRankEl = document.getElementById('relics-tab-lc-rank');
const lcLvlEl = document.getElementById('relics-tab-lc-level');
const lcStatsEl = document.getElementById('relics-tab-lc-stats');
const lcBox = document.getElementById('relics-equipped-lc-box');
if (lcThumbEl) {
lcThumbEl.src = getLightconeArtSrc(lcId);
lcThumbEl.onerror = function() {
const fb = getLightconeArtFallback(lcId);
if (this.src !== fb) this.src = fb;
else this.src = './assets/logo.png';
};
}
if (lcNameEl) lcNameEl.textContent = lcInfo.name || `Light Cone ${lcId}`;
if (lcStarsEl) lcStarsEl.textContent = '★'.repeat(lcInfo.rarity || 5);
if (lcRankEl) lcRankEl.textContent = `S${lcRank}`;
if (lcLvlEl) lcLvlEl.textContent = `Lv. ${lcLevel} / 80`;
if (lcStatsEl) {
const hp = lcMeta.hp || (lcMeta.rarity === 5 ? 1164 : (lcMeta.rarity === 4 ? 953 : 846));
const atk = lcMeta.atk || (lcMeta.rarity === 5 ? 582 : (lcMeta.rarity === 4 ? 423 : 317));
const def = lcMeta.def || (lcMeta.rarity === 5 ? 529 : (lcMeta.rarity === 4 ? 396 : 264));
lcStatsEl.textContent = `HP ${hp} / ATK ${atk} / DEF ${def}`;
}
if (lcBox) {
lcBox.onclick = () => openLightconePicker(aid);
}
// 2. Relic Slots
const allRelics = state.fullFreesrData?.relics || [];
const equippedRelics = allRelics.filter(r => r.equip_avatar === aid);
@@ -1920,34 +1931,36 @@ function renderModalRelicsTab(avatarId) {
const equipped = equippedRelics.find(r => (r.relic_id % 10) === slot);
const box = document.createElement('div');
box.className = 'relic-slot-box';
if (equipped) {
const setId = Math.floor((equipped.relic_id % 10000) / 10);
const rInfo = getRelicSetInfo(setId);
const mainAffixName = getRelicMainAffixName(slot, equipped.main_affix_id);
box.className = 'relic-slot-box equipped';
box.innerHTML = `
<div class="relic-slot-icon-wrap">
<img src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${setId}.png"
onerror="this.src='./assets/logo.png'" alt="Relic" style="width: 100%; height: 100%; object-fit: contain;">
<span class="relic-slot-badge">Slot ${slot}</span>
<span class="relic-slot-badge">${slotInfo.icon} ${slotInfo.name.split(' ')[0]}</span>
</div>
<div class="relic-slot-content">
<div class="relic-slot-title" title="${rInfo.name}">${slotInfo.icon} ${rInfo.name}</div>
<div class="relic-slot-main">✦ ${mainAffixName} (+${equipped.level})</div>
<div class="relic-slot-subs">${equipped.sub_affixes?.length || 4} ออปชั่นรอง (Max Level 15)</div>
</div>
<div class="relic-slot-actions">
<button type="button" class="btn btn-sm btn-primary btn-slot-change" style="font-size: 0.725rem; padding: 0.25rem 0.5rem;">🔄 เปลี่ยน</button>
<button type="button" class="btn btn-sm btn-secondary btn-slot-unequip" style="font-size: 0.725rem; padding: 0.25rem 0.5rem; color: var(--accent-red);">✕ ถอด</button>
<div style="display: flex; justify-content: space-between; align-items: center; padding-right: 1.5rem;">
<span class="relic-slot-title" title="${rInfo.name}">${rInfo.name}</span>
<span class="relic-slot-lvl">+15</span>
</div>
<div class="relic-slot-main">✦ ${mainAffixName}</div>
<div class="relic-slot-subs">${equipped.sub_affixes?.length || 4} ออปชั่นรอง (คลิกเพื่อเลือกเปลี่ยน)</div>
</div>
<button type="button" class="btn-slot-unequip-corner" title="ถอดชิ้นนี้">✕</button>
`;
box.querySelector('.btn-slot-change').addEventListener('click', () => {
box.addEventListener('click', () => {
openSlotPicker(aid, slot);
});
box.querySelector('.btn-slot-unequip').addEventListener('click', async () => {
box.querySelector('.btn-slot-unequip-corner')?.addEventListener('click', async (e) => {
e.stopPropagation();
equipped.equip_avatar = 0;
if (state.isConnected) await saveFreesrChangesToServer();
renderModalRelicsTab(aid);
@@ -1956,20 +1969,20 @@ function renderModalRelicsTab(avatarId) {
showToast(`ถอดรีลิกส์ ${slotInfo.name} เรียบร้อย`, 'info');
});
} else {
box.className = 'relic-slot-box empty';
box.innerHTML = `
<div class="relic-slot-icon-wrap" style="opacity: 0.5;">
<div class="relic-slot-icon-wrap" style="opacity: 0.4;">
<span style="font-size: 1.5rem;">${slotInfo.icon}</span>
<span class="relic-slot-badge">Slot ${slot}</span>
<span class="relic-slot-badge">${slotInfo.name.split(' ')[0]}</span>
</div>
<div class="relic-slot-content">
<div class="relic-slot-title" style="color: var(--text-muted);">${slotInfo.name}</div>
<div class="relic-slot-subs" style="color: var(--text-muted);">ยังไม่ได้ติดตั้ง (แนะนำ: ${slotInfo.defaultStat})</div>
</div>
<div class="relic-slot-actions">
<button type="button" class="btn btn-sm btn-primary btn-slot-equip" style="font-size: 0.725rem; padding: 0.25rem 0.5rem;">➕ สวมใส่</button>
<div class="relic-slot-main" style="color: var(--text-muted); font-size: 0.775rem;">ยังไม่ได้สวมใส่</div>
<div class="relic-slot-subs" style="color: var(--accent-cyan); font-size: 0.725rem;">+ คลิกเพื่อเลือกหรือสร้าง (แนะนำ: ${slotInfo.defaultStat})</div>
</div>
`;
box.querySelector('.btn-slot-equip').addEventListener('click', () => {
box.addEventListener('click', () => {
openSlotPicker(aid, slot);
});
}
@@ -2137,6 +2150,305 @@ async function quickGenSlotRelic(avatarId, slotNum) {
showToast(`สร้างและติดตั้งรีลิกส์แนะนำช่อง ${slotNum} สำเร็จ!`, 'success');
}
// Open Lightcone Picker Modal
function openLightconePicker(avatarId) {
state.lcPickerAvatarId = parseInt(avatarId, 10);
const aid = state.lcPickerAvatarId;
const meta = getAvatarInfo(aid);
const modal = document.getElementById('modal-select-lightcone');
if (!modal) return;
const titleEl = document.getElementById('modal-lc-picker-title');
if (titleEl) {
titleEl.textContent = `🗡️ เลือกสวมใส่กรวยแสง: ${meta.name}`;
}
const sigLcId = SIGNATURE_LIGHTCONES[aid];
const sigBox = document.getElementById('lc-picker-signature-box');
const sigThumb = document.getElementById('lc-picker-sig-thumb');
const sigName = document.getElementById('lc-picker-sig-name');
const btnQuickSig = document.getElementById('btn-lc-picker-quick-sig');
if (sigLcId && LIGHTCONE_METADATA[sigLcId]) {
const sInfo = getLightconeInfo(sigLcId);
if (sigBox) sigBox.style.display = 'flex';
if (sigThumb) {
sigThumb.src = getLightconeArtSrc(sigLcId);
sigThumb.onerror = function() { this.src = getLightconeArtFallback(sigLcId); };
}
if (sigName) sigName.textContent = `${sInfo.name} (★${sInfo.rarity} ${sInfo.path})`;
if (btnQuickSig) {
btnQuickSig.onclick = async () => {
const rank = parseInt(document.getElementById('lc-picker-rank')?.value || '5', 10);
await equipLightconeToAvatar(aid, sigLcId, rank, 80);
modal.classList.remove('active');
};
}
} else if (sigBox) {
sigBox.style.display = 'none';
}
const pathSelect = document.getElementById('lc-picker-filter-path');
if (pathSelect) pathSelect.value = 'same';
const searchInput = document.getElementById('lc-picker-search');
if (searchInput) searchInput.value = '';
renderLightconePickerGrid();
modal.classList.add('active');
}
// Render Lightcone Picker Grid
function renderLightconePickerGrid() {
const listEl = document.getElementById('lc-picker-list');
if (!listEl) return;
listEl.innerHTML = '';
const aid = state.lcPickerAvatarId;
const avatarMeta = getAvatarInfo(aid);
const search = document.getElementById('lc-picker-search')?.value.trim().toLowerCase() || '';
const pathFilter = document.getElementById('lc-picker-filter-path')?.value || 'same';
const rank = parseInt(document.getElementById('lc-picker-rank')?.value || '5', 10);
const currentLc = (state.fullFreesrData?.lightcones || []).find(l => l.equip_avatar === aid);
const allIds = Object.keys(LIGHTCONE_METADATA).map(id => parseInt(id, 10)).sort((a, b) => {
const metaA = LIGHTCONE_METADATA[a];
const metaB = LIGHTCONE_METADATA[b];
if (metaB.rarity !== metaA.rarity) return metaB.rarity - metaA.rarity;
return b - a;
});
const filtered = allIds.filter(id => {
const meta = LIGHTCONE_METADATA[id];
const info = getLightconeInfo(id);
// Path match
if (pathFilter === 'same' && meta.path !== avatarMeta.path) return false;
if (pathFilter !== 'all' && pathFilter !== 'same' && meta.path !== pathFilter) return false;
// Search match
if (search) {
const matchName = info.name.toLowerCase().includes(search);
const matchId = String(id).includes(search);
if (!matchName && !matchId) return false;
}
return true;
});
if (filtered.length === 0) {
listEl.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">ไม่พบกรวยแสงตามเงื่อนไขที่ค้นหา</div>`;
return;
}
filtered.forEach(id => {
const meta = LIGHTCONE_METADATA[id];
const info = getLightconeInfo(id);
const isCurrent = currentLc?.item_id === id;
const isSig = SIGNATURE_LIGHTCONES[aid] === id;
const card = document.createElement('div');
card.className = `lc-picker-card rarity-${info.rarity} ${isCurrent ? 'active-equipped' : ''}`;
let badgeHtml = '';
if (isCurrent) {
badgeHtml = `<span style="font-size: 0.65rem; background: var(--accent-gold); color: #000; font-weight: 800; padding: 0.1rem 0.4rem; border-radius: 4px;">สวมใส่อยู่</span>`;
} else if (isSig) {
badgeHtml = `<span style="font-size: 0.65rem; background: rgba(251, 191, 36, 0.2); border: 1px solid var(--accent-gold); color: var(--accent-gold); font-weight: 800; padding: 0.1rem 0.4rem; border-radius: 4px;">ประจำตัว</span>`;
}
card.innerHTML = `
<img class="lc-picker-thumb" src="${getLightconeArtSrc(id)}" onerror="this.src='${getLightconeArtFallback(id)}'" alt="${info.name}">
<div style="flex: 1; min-width: 0;">
<div style="display: flex; justify-content: space-between; align-items: center; gap: 0.35rem;">
<span style="font-weight: 800; font-size: 0.85rem; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${info.name}">${info.name}</span>
${badgeHtml}
</div>
<div style="font-size: 0.725rem; color: var(--accent-gold); margin-top: 0.2rem;">${'★'.repeat(info.rarity || 5)}</div>
<div style="font-size: 0.7rem; color: var(--accent-cyan); margin-top: 0.15rem;">
${getPathIconHtml(meta.path, 14)} ${info.path}
</div>
</div>
`;
card.addEventListener('click', async () => {
await equipLightconeToAvatar(aid, id, rank, 80);
document.getElementById('modal-select-lightcone')?.classList.remove('active');
});
listEl.appendChild(card);
});
}
// Equip Lightcone directly to Avatar
async function equipLightconeToAvatar(avatarId, lcId, rank = 5, level = 80) {
const aid = parseInt(avatarId, 10);
const id = parseInt(lcId, 10);
const meta = getAvatarInfo(aid);
const lcInfo = getLightconeInfo(id);
if (!state.fullFreesrData) state.fullFreesrData = { avatars: {}, lightcones: [], relics: [] };
state.fullFreesrData.lightcones = state.fullFreesrData.lightcones || [];
// 1. Unassign other lightcones from this avatar
state.fullFreesrData.lightcones.forEach(lc => {
if (lc.equip_avatar === aid) lc.equip_avatar = 0;
});
// 2. Find or create lightcone entry
let targetLc = state.fullFreesrData.lightcones.find(lc => lc.item_id === id && (!lc.equip_avatar || lc.equip_avatar === 0));
if (!targetLc) {
targetLc = state.fullFreesrData.lightcones.find(lc => lc.item_id === id);
if (targetLc) {
targetLc.equip_avatar = aid;
targetLc.rank = rank;
targetLc.level = level;
targetLc.promotion = 6;
} else {
let maxUid = 1000;
state.fullFreesrData.lightcones.forEach(l => {
if (l.unique_id >= maxUid) maxUid = l.unique_id + 1;
});
targetLc = {
unique_id: maxUid,
internal_uid: state.fullFreesrData.lightcones.length,
item_id: id,
level: level,
rank: rank,
promotion: 6,
equip_avatar: aid
};
state.fullFreesrData.lightcones.push(targetLc);
}
} else {
targetLc.equip_avatar = aid;
targetLc.rank = rank;
targetLc.level = level;
targetLc.promotion = 6;
}
if (state.isConnected) {
await saveFreesrChangesToServer().catch(() => {});
}
renderModalOverviewTab(aid);
renderModalRelicsTab(aid);
renderShowcaseCard(aid);
renderLightconesGrid();
showToast(`🗡️ สวมใส่ ${lcInfo.name} (S${rank}) ให้กับ ${meta.name} เรียบร้อย!`, 'success');
}
// Unequip Lightcone from Avatar
async function unequipLightconeFromAvatar(avatarId) {
const aid = parseInt(avatarId, 10);
const meta = getAvatarInfo(aid);
if (state.fullFreesrData?.lightcones) {
state.fullFreesrData.lightcones.forEach(lc => {
if (lc.equip_avatar === aid) lc.equip_avatar = 0;
});
if (state.isConnected) await saveFreesrChangesToServer().catch(() => {});
renderModalOverviewTab(aid);
renderModalRelicsTab(aid);
renderShowcaseCard(aid);
renderLightconesGrid();
showToast(`ถอดกรวยแสงออกจาก ${meta.name} เรียบร้อย`, 'info');
}
}
// Quick Set All Avatars to E0S1 Standard (Lv.80, Prom 6, E0, S1 Signature LC)
async function quickSetAllAvatarsE0S1() {
try {
if (!state.fullFreesrData) {
state.fullFreesrData = { avatars: {}, lightcones: [], relics: [] };
}
state.fullFreesrData.avatars = state.fullFreesrData.avatars || {};
state.fullFreesrData.lightcones = state.fullFreesrData.lightcones || [];
const allAvatarIds = Object.keys(AVATARS_DATA).map(id => parseInt(id, 10));
let nextUniqueId = 1000;
state.fullFreesrData.lightcones.forEach(lc => {
if (lc.unique_id >= nextUniqueId) nextUniqueId = lc.unique_id + 1;
});
for (const aid of allAvatarIds) {
if (!state.fullFreesrData.avatars[aid]) {
state.fullFreesrData.avatars[aid] = {
owner_uid: 0,
avatar_id: aid,
level: 80,
promotion: 6,
techniques: [],
sp_max: 140,
sp_value: 70,
data: {
rank: 0,
skills: {
[`${aid}001`]: 6,
[`${aid}002`]: 10,
[`${aid}003`]: 10,
[`${aid}004`]: 10,
[`${aid}007`]: 1,
[`${aid}101`]: 1,
[`${aid}102`]: 1,
[`${aid}103`]: 1,
[`${aid}201`]: 1
},
skills_by_anchor_type: {
"1": 6, "2": 10, "3": 10, "4": 10, "5": 1, "6": 1, "7": 1, "8": 1, "9": 1
}
}
};
} else {
const av = state.fullFreesrData.avatars[aid];
av.level = 80;
av.promotion = 6;
av.data = av.data || {};
av.data.rank = 0;
if (!av.sp_value) av.sp_value = 70;
}
// Equip S1 Signature Lightcone
const sigLcId = SIGNATURE_LIGHTCONES[aid] || 21001;
let existingLc = state.fullFreesrData.lightcones.find(l => l.equip_avatar === aid);
if (existingLc) {
existingLc.item_id = sigLcId;
existingLc.rank = 1;
existingLc.level = 80;
existingLc.promotion = 6;
} else {
state.fullFreesrData.lightcones.push({
unique_id: nextUniqueId++,
internal_uid: state.fullFreesrData.lightcones.length,
item_id: sigLcId,
level: 80,
promotion: 6,
rank: 1,
equip_avatar: aid
});
}
}
initAvatarsList();
state.avatarsList.forEach(a => {
a.level = 80;
a.promotion = 6;
a.rank = 0;
});
if (state.isConnected) {
await saveFreesrChangesToServer().catch(() => {});
}
renderAvatarsGrid();
if (el.statAvatars) el.statAvatars.textContent = Object.keys(state.fullFreesrData.avatars).length;
showToast('⚡ ปรับตัวละครทุกตัวเป็น E0S1 พร้อมกรวยแสงประจำตัวเรียบร้อยแล้ว!', 'success');
} catch (err) {
showToast(err.message, 'error');
}
}
// Render Skills Tab with real StarRailRes descriptions & icons
async function renderModalSkillsTab(avatarId) {
const aid = String(avatarId);
@@ -2733,7 +3045,7 @@ function renderShowcaseCard(avatarId) {
activeSetBadges.forEach(sb => {
setsHtml += `
<div class="showcase-set-item">
<span style="color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 170px;">${sb.name}</span>
<span style="color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; margin-right: 0.5rem;" title="${sb.name}">${sb.name}</span>
<span class="showcase-set-badge">${sb.count}-PC</span>
</div>
`;
@@ -2769,8 +3081,8 @@ function renderShowcaseCard(avatarId) {
</div>
<div class="showcase-relic-info">
<div class="showcase-relic-main-stat">
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px;">${rInfo.name}</span>
<span style="color: var(--accent-cyan); font-weight: 800;">${slotInfo.stat}</span>
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; margin-right: 0.5rem;" title="${rInfo.name}">${rInfo.name}</span>
<span style="color: var(--accent-cyan); font-weight: 800; flex-shrink: 0;">${slotInfo.stat}</span>
</div>
<div class="showcase-relic-subs">
<span>CRIT Rate +${(3.2 + slot * 1.5).toFixed(1)}%${rollBadge(slot % 2 === 0 ? 3 : 2)}</span>
@@ -2989,12 +3301,22 @@ async function exportShowcaseImage(avatarId) {
if (typeof html2canvas !== 'undefined') {
try {
const origWidth = showcaseEl.style.width;
const origMinWidth = showcaseEl.style.minWidth;
showcaseEl.style.width = '1050px';
showcaseEl.style.minWidth = '1050px';
const canvas = await html2canvas(showcaseEl, {
useCORS: true,
allowTaint: true,
scale: 2,
backgroundColor: '#0c1222'
backgroundColor: '#0c1222',
windowWidth: 1280
});
showcaseEl.style.width = origWidth;
showcaseEl.style.minWidth = origMinWidth;
const dataUrl = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = dataUrl;
@@ -4344,9 +4666,37 @@ function setupEvents() {
setupSkillStepper(el.rangeSkillTalent, el.valSkillTalent, el.btnStepDecTalent, el.btnStepIncTalent, el.btnSkillMaxTalent, 10);
// Overview Tab Quick Navigation Cards
el.overviewLcCard?.addEventListener('click', () => switchModalSubtab('modaltab-lightcone'));
el.overviewLcCard?.addEventListener('click', () => {
if (state.selectedAvatarDetail) openLightconePicker(state.selectedAvatarDetail.avatar_id);
});
el.overviewRelicsCard?.addEventListener('click', () => switchModalSubtab('modaltab-relics'));
// Lightcone Picker Modal Controls
document.getElementById('btn-close-lc-picker')?.addEventListener('click', () => {
document.getElementById('modal-select-lightcone')?.classList.remove('active');
});
document.getElementById('btn-lc-picker-close')?.addEventListener('click', () => {
document.getElementById('modal-select-lightcone')?.classList.remove('active');
});
document.getElementById('lc-picker-search')?.addEventListener('input', () => {
renderLightconePickerGrid();
});
document.getElementById('lc-picker-filter-path')?.addEventListener('change', () => {
renderLightconePickerGrid();
});
document.getElementById('lc-picker-rank')?.addEventListener('change', () => {
renderLightconePickerGrid();
});
document.getElementById('btn-lc-picker-unequip')?.addEventListener('click', async () => {
if (state.lcPickerAvatarId) {
await unequipLightconeFromAvatar(state.lcPickerAvatarId);
}
document.getElementById('modal-select-lightcone')?.classList.remove('active');
});
// Top Bar Characters Tab Quick Actions
document.getElementById('btn-all-avatars-e0s1')?.addEventListener('click', quickSetAllAvatarsE0S1);
// Relic Tab Controls in Modal
el.btnModalRelicsAuto?.addEventListener('click', async () => {
if (!state.selectedAvatarDetail) return;
+50 -2
View File
@@ -268,7 +268,31 @@ export const TRANSLATIONS = {
"Elation": "ปีติ"
},
"settingsSaved": "บันทึกการตั้งค่าสำเร็จ!",
"settingsReset": "คืนค่าเริ่มต้นเรียบร้อย"
"settingsReset": "คืนค่าเริ่มต้นเรียบร้อย",
"qaLineupE0S1Title": "ปรับทีมเป็น E0S1",
"qaLineupE0S1Desc": "ตัวละครในทีมเป็น E0 พร้อม LC S1",
"qaLineupRelicsTitle": "ใส่อุปกรณ์ทีมครบชุด",
"qaLineupRelicsDesc": "ใส่รีลิกส์ +15 ครบ 4 คนในทีม",
"lblPersistenceInfo": "ระบบจัดการไฟล์เซฟและข้อมูลฝั่งเซิร์ฟเวอร์",
"modalTabOverview": "ภาพรวม",
"modalTabRelics": "รีลิกส์ & อุปกรณ์",
"overviewStatsTitle": "⚔️ ค่าพลังการต่อสู้ (Combat Stats)",
"overviewStatsNote": "รวมโบนัสกรวยแสงและรีลิกส์",
"overviewEquippedLc": "🗡️ กรวยแสงที่สวมใส่",
"overviewEquippedRelics": "🛡️ รีลิกส์และเซ็ต",
"modalRelicsTitle": "🛡️ สวมใส่รีลิกส์ (Relics 6 ชิ้น)",
"modalRelicsSubtitle": "คลิกแต่ละช่องเพื่อเลือกเปลี่ยน หรือสวมใส่เซ็ตแนะนำ",
"btnAutoEquipRelics": "⚡ สวมใส่เซ็ตแนะนำครบชุด",
"btnClearAllRelics": "🧹 ถอดทั้งหมด",
"activeSetEffects": "✨ เอฟเฟกต์เซ็ตที่เปิดใช้งาน",
"modalColLevel": "เลเวล",
"lblPromotion": "ขั้นเลื่อนขั้น",
"slotPickerTitle": "🛡️ เลือกสวมใส่รีลิกส์",
"quickGenRecTitle": "⚡ สร้างชิ้นแนะนำสำหรับช่องนี้",
"quickGenRecDesc": "สร้างรีลิกส์ 5★ +15 ตรงเซ็ตและสเตตัสหลักที่ดีที่สุด",
"btnGenAndEquip": "สร้าง & สวมใส่",
"selectFromInventory": "เลือกจากคลังเก็บของ (ตรงช่อง)",
"btnAllE0S1": "⚡ ปรับ E0S1 ทุกตัว"
},
"en": {
"ahaGlobalDesc": "Enable server-wide Aeon ★ Aha special trait",
@@ -520,7 +544,31 @@ export const TRANSLATIONS = {
"Elation": "Elation"
},
"settingsSaved": "Settings saved successfully!",
"settingsReset": "Reset to default settings"
"settingsReset": "Reset to default settings",
"qaLineupE0S1Title": "Set Team to E0S1",
"qaLineupE0S1Desc": "Team characters to E0 with S1 LC",
"qaLineupRelicsTitle": "Auto-Equip Team Relics",
"qaLineupRelicsDesc": "Equip +15 relics for 4 teammates",
"lblPersistenceInfo": "Server save file & data management",
"modalTabOverview": "Overview",
"modalTabRelics": "Relics & Gear",
"overviewStatsTitle": "⚔️ Combat Stats",
"overviewStatsNote": "Includes Lightcone & Relic bonuses",
"overviewEquippedLc": "🗡️ Equipped Lightcone",
"overviewEquippedRelics": "🛡️ Relics & Set Bonuses",
"modalRelicsTitle": "🛡️ Equip Relics (6 Slots)",
"modalRelicsSubtitle": "Click each slot to change or equip recommended sets",
"btnAutoEquipRelics": "⚡ Equip Recommended Sets",
"btnClearAllRelics": "🧹 Unequip All",
"activeSetEffects": "✨ Active Set Bonuses",
"modalColLevel": "Level",
"lblPromotion": "Ascension Rank",
"slotPickerTitle": "🛡️ Select Relic to Equip",
"quickGenRecTitle": "⚡ Generate Recommended Piece",
"quickGenRecDesc": "Generate 5★ +15 with best set & main stat",
"btnGenAndEquip": "Generate & Equip",
"selectFromInventory": "Select from Inventory",
"btnAllE0S1": "⚡ All E0S1"
},
"zh-CN": {
"ahaGlobalDesc": "全服启用星神·阿哈特殊被动",