-
`;
+ // 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 Max Button
const btnQuickMax = card.querySelector('.btn-card-quick-max');
btnQuickMax.addEventListener('click', async (e) => {
@@ -982,6 +1013,98 @@ async function maxSingleAvatar(avatarId) {
// Active Lightcone Superimposition Rank in Modal (1 - 5)
let selectedLcRank = 5;
+// Modal Sub-tabs Handler
+// Signature Light Cones dictionary for all characters
+const SIGNATURE_LIGHTCONES = {
+ 1001: 21001, // March 7th -> Day One of My New Life
+ 1002: 21000, // Dan Heng -> Only Silence Remains
+ 1003: 23000, // Himeko -> Night on the Milky Way
+ 1004: 23004, // Welt -> In the Name of the World
+ 1005: 23006, // Kafka -> Patience Is All You Need
+ 1006: 23007, // Silver Wolf -> Incessant Rain
+ 1008: 21007, // Arlan -> A Secret Vow
+ 1009: 21008, // Asta -> Planetary Rendezvous
+ 1013: 21006, // Herta -> The Birth of the Self
+ 1101: 23003, // Bronya -> But the Battle Isn't Over
+ 1102: 23001, // Seele -> In the Night
+ 1103: 21009, // Serval -> Make the World Clamor
+ 1104: 23005, // Gepard -> Moment of Victory
+ 1105: 21010, // Natasha -> Post-Op Conversation
+ 1106: 21003, // Pela -> Good Night and Sleep Well
+ 1107: 23002, // Clara -> Something Irreplaceable
+ 1108: 21011, // Sampo -> Eyes of the Prey
+ 1109: 21012, // Hook -> The Moles Welcome You
+ 1110: 21013, // Lynx -> Warmth Shortens Cold Nights
+ 1111: 21014, // Luka -> Resolution Shines As Pearls of Sweat
+ 1112: 23012, // Topaz -> Worrisome, Blissful
+ 1201: 21015, // Qingque -> Today Is Another Peaceful Day
+ 1202: 21016, // Tingyun -> Memories of the Past
+ 1203: 23008, // Luocha -> Echoes of the Coffin
+ 1204: 23010, // Jing Yuan -> Before Dawn
+ 1205: 23009, // Blade -> The Unreachable Side
+ 1206: 21017, // Sushang -> Swordplay
+ 1207: 21018, // Yukong -> Past and Future
+ 1208: 23011, // Fu Xuan -> She Already Shut Her Eyes
+ 1209: 23013, // Yanqing -> Sleep Like the Dead
+ 1210: 21019, // Guinaifen -> Fermata
+ 1211: 23016, // Bailu -> Time Waits for No One
+ 1212: 23014, // Jingliu -> I Shall Be My Own Sword
+ 1213: 23015, // Dan Heng IL -> Brighter Than the Sun
+ 1214: 21020, // Xueyi -> Under the Blue Sky
+ 1215: 21021, // Hanya -> Dance! Dance! Dance!
+ 1217: 23017, // Huohuo -> Night of Fright
+ 1218: 23027, // Jiaoqiu -> Those Many Springs
+ 1220: 23030, // Feixiao -> I Venture Forth to Hunt
+ 1221: 23028, // Yunli -> Dance at Sunset
+ 1222: 23031, // Lingsha -> Scent Alone Stays True
+ 1223: 21038, // Moze -> Shadowed by Night
+ 1224: 23033, // Fugue -> Long Road Leads Home
+ 1301: 21022, // Gallagher -> What Is Real?
+ 1302: 23018, // Argenti -> An Instant Before A Gaze
+ 1303: 23019, // Ruan Mei -> Past Self in Mirror
+ 1304: 23022, // Aventurine -> Inherently Unjust Destiny
+ 1305: 23020, // Dr. Ratio -> Baptism of Pure Thought
+ 1306: 23021, // Sparkle -> Earthly Escort
+ 1307: 23023, // Black Swan -> Reforged Remembrance
+ 1308: 23024, // Acheron -> Along the Passing Shore
+ 1309: 23026, // Robin -> Flowing Nightglow
+ 1310: 23025, // Firefly -> Whereabouts Should Dreams Rest
+ 1312: 21035, // Misha -> Indelible Promise
+ 1313: 23032, // Sunday -> A Grounded Ascent
+ 1314: 23029, // Jade -> Yet Hope Is Priceless
+ 1315: 23028, // Boothill -> Sailing Towards A Second Life
+ 1317: 23030, // Rappa -> Ninjutsu Inscription: Dazzling Evilbreaker
+ 1401: 23034, // The Herta -> Into the Unreachable Veil
+ 1402: 23035, // Aglaea -> Time Woven into Gold
+ 1403: 23036, // Castorice -> Make Farewells More Beautiful
+ 1404: 23037, // Anaxa -> If Time Were a Flower
+ 1405: 23038, // Tribbie
+ 1406: 23039, // Mydei
+ 8001: 24001, // Trailblazer Male Destruction -> On the Fall of an Aeon
+ 8002: 24001, // Trailblazer Female Destruction
+ 8003: 24002, // Trailblazer Male Preservation -> Texture of Memories
+ 8004: 24002, // Trailblazer Female Preservation
+ 8005: 21008, // Trailblazer Male Harmony -> Memories of the Past
+ 8006: 21008, // Trailblazer Female Harmony
+ 8007: 21040, // Trailblazer Remembrance
+ 8008: 21040
+};
+
+// Recommended Relic Builds by Path
+const RECOMMENDED_RELIC_BUILDS = {
+ Preservation: { cavern: 103, planar: 304, name: "Knight of Purity (4) + Belobog (2)" },
+ Abundance: { cavern: 101, planar: 302, name: "Passerby (4) + Fleet (2)" },
+ Harmony: { cavern: 114, planar: 310, name: "Messenger (4) + Broken Keel (2)" },
+ Nihility: { cavern: 116, planar: 311, name: "Prisoner (4) + Glamoth (2)" },
+ Hunt: { cavern: 120, planar: 309, name: "Valorous (4) + Rutilant Arena (2)" },
+ Destruction: { cavern: 119, planar: 315, name: "Iron Cavalry (4) + Kalpagni (2)" },
+ Erudition: { cavern: 115, planar: 306, name: "Grand Duke (4) + Inert Salsotto (2)" },
+ 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');
@@ -998,6 +1121,9 @@ function setupModalSubtabs() {
if (targetContent) {
targetContent.style.display = 'block';
targetContent.classList.add('active');
+ if (targetId === 'modaltab-showcase' && state.selectedAvatarDetail) {
+ renderShowcaseCard(state.selectedAvatarDetail.avatar_id);
+ }
}
});
});
@@ -1015,21 +1141,144 @@ function updateLcRankPips(rank) {
});
}
-function updateModalLcPreview(lcId) {
- if (!el.modalLcPreview) return;
+// Lightcone Passive Skill Descriptions with dynamic rank values
+function getLightconePassiveDesc(lcId, rank = 5) {
const id = parseInt(lcId, 10);
- if (!id) {
- el.modalLcPreview.innerHTML = '';
- return;
- }
- const info = getLightconeInfo(id);
- el.modalLcPreview.innerHTML = `
-
${info.name}
-
★ ${info.rarity}
- `;
+ const r = Math.max(1, Math.min(5, rank));
+
+ const passives = {
+ 21001: `ทำให้ DEF ของผู้สวมใส่เพิ่มขึ้น
${16 + (r - 1) * 2}% หลังจากเข้าสู่การต่อสู้ จะทำให้ความต้านทานทุกประเภทของตัวละครฝ่ายเราทั้งหมดเพิ่มขึ้น
${8 + (r - 1)}% โดยสกิลชนิดเดียวกันจะไม่สามารถซ้อนทับได้`,
+ 23024: `ทำให้ CRIT DMG ของผู้สวมใส่เพิ่มขึ้น
${36 + (r - 1) * 6}% เมื่อโจมตีโดนเป้าหมาย จะติดสถานะ Mirage Fizzle ทำให้ได้รับความเสียหายเพิ่มขึ้น
${24 + (r - 1) * 4}% และความเสียหายท่าไม้ตายเพิ่มขึ้น
${24 + (r - 1) * 4}%`,
+ 23025: `ทำให้ Break Effect ของผู้สวมใส่เพิ่มขึ้น
${60 + (r - 1) * 10}% เมื่อสร้าง Break DMG จะทำให้เป้าหมายติดสถานะ Routed ได้รับ Break DMG เพิ่มขึ้น
${24 + (r - 1) * 4}% และลด SPD ลง 20%`,
+ 23001: `ทำให้ CRIT Rate ของผู้สวมใส่เพิ่มขึ้น
${18 + (r - 1) * 3}% ระหว่างการต่อสู้ ทุกๆ 10 หน่วยของ SPD ที่เกิน 100 จะเพิ่ม DMG การโจมตีปกติและสกิล
${6 + (r - 1)}% และเพิ่ม CRIT DMG ท่าไม้ตาย
${12 + (r - 1) * 2}%`,
+ 23010: `ทำให้ CRIT DMG ของผู้สวมใส่เพิ่มขึ้น
${36 + (r - 1) * 6}% เพิ่ม DMG สกิลและท่าไม้ตาย
${18 + (r - 1) * 3}% และเสริม Somnus Corpus เพิ่ม DMG โจมตีต่อเนื่อง
${48 + (r - 1) * 8}%`,
+ 23019: `ทำให้ Break Effect ของผู้สวมใส่เพิ่มขึ้น
${60 + (r - 1) * 10}% หลังจากผู้สวมใส่ปล่อยท่าไม้ตาย ทีมสร้าง DMG เพิ่มขึ้น
${24 + (r - 1) * 4}% 3 เทิร์น และฟื้นฟูแต้มสกิล 1 แต้ม`,
+ 23022: `ทำให้ DEF ของผู้สวมใส่เพิ่มขึ้น
${40 + (r - 1) * 5}% เมื่อมอบโล่ให้เพื่อนร่วมทีม จะเพิ่ม CRIT DMG
${40 + (r - 1) * 5}% 2 เทิร์น และทำให้ศัตรูได้รับ DMG เพิ่มขึ้น
${10 + (r - 1) * 2}%`,
+ 23026: `เมื่อเพื่อนร่วมทีมโจมตี ผู้สวมใส่จะได้รับ Cantillation 1 ชั้น แต่ละชั้นเพิ่ม Energy Regeneration Rate
${3 + (r - 1) * 0.5}% สูงสุด 5 ชั้น และเพิ่ม ATK ของทั้งทีม
${48 + (r - 1) * 8}%`,
+ 23006: `ทำให้ DMG ของผู้สวมใส่เพิ่มขึ้น
${24 + (r - 1) * 4}% หลังการโจมตีแต่ละครั้ง SPD เพิ่มขึ้น
${4.8 + (r - 1) * 0.8}% สูงสุด 3 ชั้น และทำให้เป้าหมายติดสถานะ Erode เกิด DoT สายฟ้า
${60 + (r - 1) * 10}%`,
+ 23030: `ทำให้ CRIT Rate ของผู้สวมใส่เพิ่มขึ้น
${15 + (r - 1) * 2.5}% เมื่อโจมตีต่อเนื่อง ได้รับ Luminflux ซ้อนทับ 2 ชั้น แต่ละชั้นทำให้ท่าไม้ตายเมิน DEF ศัตรู
${27 + (r - 1) * 4.5}%`,
+ 23014: `ทำให้ CRIT DMG ของผู้สวมใส่เพิ่มขึ้น
${20 + (r - 1) * 3}% เมื่อเพื่อนร่วมทีมเสีย HP จะได้รับ Eclipse ซ้อนทับ 3 ชั้น ทำให้การโจมตีถัดไปสร้าง DMG เพิ่มขึ้น
${14 + (r - 1) * 2.5}% และเมิน DEF
${12 + (r - 1) * 2}%`,
+ 23015: `ทำให้ CRIT Rate ของผู้สวมใส่เพิ่มขึ้น
${18 + (r - 1) * 3}% เมื่อปล่อย Basic ATK จะได้รับ Dragons Call เพิ่ม ATK
${18 + (r - 1) * 3}% และ Energy Regeneration Rate
${6 + (r - 1)}%`,
+ 23033: `ทำให้ Break Effect ของผู้สวมใส่เพิ่มขึ้น
${60 + (r - 1) * 10}% เมื่อสร้าง Break DMG จะทำให้ศัตรูติดสถานะ Vulnerability ได้รับ DMG เพิ่มขึ้น
${18 + (r - 1) * 3}%`,
+ 23032: `ทำให้ Energy Regeneration Rate ของผู้สวมใส่เพิ่มขึ้น
${12 + (r - 1) * 2}% เมื่อปล่อยสกิลต่อสู้หรือท่าไม้ตาย จะเพิ่ม CRIT DMG ของพันธมิตร
${48 + (r - 1) * 8}%`,
+ 23034: `ทำให้ CRIT Rate ของผู้สวมใส่เพิ่มขึ้น
${12 + (r - 1) * 2}% เมื่อปล่อยท่าไม้ตาย จะสร้าง DMG น้ำแข็งเพิ่มเติม
${60 + (r - 1) * 10}% ของ ATK แก่ศัตรูทั้งหมด`,
+ 23036: `ทำให้ HP สูงสุดของผู้สวมใส่เพิ่มขึ้น
${30 + (r - 1) * 5}% และเมื่ออยู่ในสนามรบ สมาชิกทีมทุกคนสร้าง DMG เพิ่มขึ้น
${24 + (r - 1) * 4}%`,
+ 24001: `เมื่อผู้สวมใส่โจมตี จะเพิ่ม ATK
${8 + (r - 1) * 2}% สูงสุด 4 ชั้น และเมื่อทำลายจุดอ่อนศัตรู จะเพิ่ม DMG
${12 + (r - 1) * 3}% เป็นเวลา 2 เทิร์น`
+ };
+
+ if (passives[id]) return passives[id];
+
+ const meta = LIGHTCONE_METADATA[id];
+ const pathName = meta ? translatePath(meta.path) : "สายต่อสู้";
+ return `เพิ่มค่าสถานะพื้นฐานตามสาย
${pathName} และเพิ่มประสิทธิภาพความเสียหายและการสนับสนุน
${12 + (r - 1) * 3}% เมื่อเข้าสู่การต่อสู้`;
}
-// Character Detail / Build Modal (Inspired by srtools.punklorde.org/skills-info)
+// Update Modal Lightcone Full Artwork and Details (Matching srtools / Firefly)
+function updateModalLcFullView(lcId, rank = 5, level = 80) {
+ const id = parseInt(lcId, 10);
+ if (!id || !LIGHTCONE_METADATA[id]) {
+ if (el.modalLcFullImg) el.modalLcFullImg.style.display = 'none';
+ if (el.modalLcNoArt) el.modalLcNoArt.style.display = 'block';
+ if (el.modalLcNameDisplay) el.modalLcNameDisplay.textContent = 'ยังไม่ได้สวมใส่ Light Cone';
+ if (el.modalLcMetaDisplay) el.modalLcMetaDisplay.innerHTML = '';
+ if (el.modalLcRankBadge) el.modalLcRankBadge.textContent = '-';
+ if (el.modalLcPassiveDesc) el.modalLcPassiveDesc.innerHTML = 'กรุณาเลือก Light Cone ที่ต้องการสวมใส่';
+ return;
+ }
+
+ const meta = LIGHTCONE_METADATA[id];
+ const info = getLightconeInfo(id);
+
+ if (el.modalLcId) el.modalLcId.value = id;
+ if (el.modalLcLevel) el.modalLcLevel.value = level;
+ if (el.rangeLcLevel) el.rangeLcLevel.value = level;
+ if (el.modalLcLvlVal) el.modalLcLvlVal.textContent = `${level} / 80`;
+
+ // Full artwork image
+ if (el.modalLcFullImg) {
+ el.modalLcFullImg.style.display = 'block';
+ if (el.modalLcNoArt) el.modalLcNoArt.style.display = 'none';
+ const primarySrc = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/light_cone_preview/${id}.png`;
+ const fallbackSrc = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/image/light_cone_preview/${id}.png`;
+ el.modalLcFullImg.src = primarySrc;
+ el.modalLcFullImg.onerror = function() {
+ if (this.src !== fallbackSrc) {
+ this.src = fallbackSrc;
+ }
+ };
+ }
+
+ if (el.modalLcArtWrapper) {
+ el.modalLcArtWrapper.className = `lc-art-wrapper rarity-${info.rarity}`;
+ }
+
+ if (el.modalLcNameDisplay) {
+ el.modalLcNameDisplay.textContent = info.name;
+ }
+
+ if (el.modalLcMetaDisplay) {
+ el.modalLcMetaDisplay.innerHTML = `
+ ${getPathIconHtml(meta.path, 22)}
+
${info.path}
+
★ ${info.rarity}
+
(ID: ${id})
+ `;
+ }
+
+ if (el.modalLcRankBadge) {
+ el.modalLcRankBadge.textContent = `S${rank} • Lv.${level}`;
+ }
+
+ updateLcRankPips(rank);
+
+ if (el.modalLcPassiveDesc) {
+ el.modalLcPassiveDesc.innerHTML = getLightconePassiveDesc(id, rank);
+ }
+
+ if (el.selectModalChangeLc) {
+ el.selectModalChangeLc.value = id;
+ }
+}
+
+// Populate Modal Change Lightcone Dropdown
+function populateModalChangeLcDropdown(avatarPath) {
+ if (!el.selectModalChangeLc) return;
+ el.selectModalChangeLc.innerHTML = '';
+
+ const allIds = Object.keys(LIGHTCONE_METADATA).sort((a, b) => {
+ const metaA = LIGHTCONE_METADATA[a];
+ const metaB = LIGHTCONE_METADATA[b];
+ if (metaB.rarity !== metaA.rarity) return metaB.rarity - metaA.rarity;
+ return parseInt(b, 10) - parseInt(a, 10);
+ });
+
+ const samePathGroup = document.createElement('optgroup');
+ samePathGroup.label = `⭐ Light Cones สาย ${avatarPath}`;
+
+ const otherPathGroup = document.createElement('optgroup');
+ otherPathGroup.label = `🗡️ Light Cones สายอื่นๆ`;
+
+ allIds.forEach(id => {
+ const meta = LIGHTCONE_METADATA[id];
+ const info = getLightconeInfo(id);
+ const opt = document.createElement('option');
+ opt.value = id;
+ opt.textContent = `★${info.rarity} [${info.path}] ${info.name} (ID: ${id})`;
+
+ if (meta.path === avatarPath) {
+ samePathGroup.appendChild(opt);
+ } else {
+ otherPathGroup.appendChild(opt);
+ }
+ });
+
+ if (samePathGroup.children.length > 0) {
+ el.selectModalChangeLc.appendChild(samePathGroup);
+ }
+ el.selectModalChangeLc.appendChild(otherPathGroup);
+}
+
+// Character Detail / Build Modal (srtools style)
function openAvatarDetailModal(avatarItem) {
state.selectedAvatarDetail = avatarItem;
const aid = avatarItem.avatar_id;
@@ -1126,20 +1375,18 @@ function openAvatarDetailModal(avatarItem) {
el.valCharEnergy.textContent = `${currentSp}/${maxSp}`;
}
- // Lightcone
+ // Lightcone initialization & full preview
+ populateModalChangeLcDropdown(meta.path);
const equippedLc = (state.fullFreesrData?.lightcones || []).find(lc => lc.equip_avatar === aid);
- if (equippedLc) {
- if (el.modalLcId) el.modalLcId.value = equippedLc.item_id || '';
- if (el.modalLcLevel) el.modalLcLevel.value = equippedLc.level || 80;
- selectedLcRank = equippedLc.rank || 5;
- updateModalLcPreview(equippedLc.item_id);
- } else {
- if (el.modalLcId) el.modalLcId.value = '';
- if (el.modalLcLevel) el.modalLcLevel.value = 80;
- selectedLcRank = 5;
- updateModalLcPreview(null);
- }
- updateLcRankPips(selectedLcRank);
+ const activeLcId = equippedLc?.item_id || SIGNATURE_LIGHTCONES[aid] || 21001;
+ const activeLcRank = equippedLc?.rank || 5;
+ const activeLcLevel = equippedLc?.level || 80;
+
+ selectedLcRank = activeLcRank;
+ updateModalLcFullView(activeLcId, activeLcRank, activeLcLevel);
+
+ // Pre-render showcase card
+ renderShowcaseCard(aid);
el.modalAvatarDetail.classList.add('active');
}
@@ -1383,7 +1630,9 @@ async function saveAvatarDetails() {
renderAvatarsGrid();
loadEquipment();
- el.modalAvatarDetail.classList.remove('active');
+ if (closeModal) {
+ el.modalAvatarDetail.classList.remove('active');
+ }
if (state.isConnected) {
try {
@@ -1402,6 +1651,409 @@ async function saveAvatarDetails() {
}
}
+// Apply E6S5 Build (Lv.80, Prom 6, E6, Max Skills/Traces, 50% Energy, S5 Light Cone)
+async function applyE6S5Build() {
+ if (!state.selectedAvatarDetail) return;
+ const aid = state.selectedAvatarDetail.avatar_id;
+
+ // Level & Eidolon
+ el.modalCharLevel.value = 80;
+ el.modalCharPromotion.value = 6;
+ state.selectedAvatarDetail.level = 80;
+ state.selectedAvatarDetail.promotion = 6;
+ state.selectedAvatarDetail.rank = 6;
+ renderModalEidolonPips(6);
+
+ // Skills & Traces
+ maxSkillsForAvatar();
+
+ // Energy: 50% standard
+ if (el.rangeCharEnergy && el.valCharEnergy) {
+ const half = Math.floor(parseInt(el.rangeCharEnergy.max, 10) * 0.5);
+ el.rangeCharEnergy.value = half;
+ el.valCharEnergy.textContent = `${half}/${el.rangeCharEnergy.max}`;
+ }
+
+ // Light Cone: Lv 80, S5
+ let curLcId = parseInt(el.modalLcId?.value, 10);
+ if (!curLcId || curLcId === 0) {
+ curLcId = SIGNATURE_LIGHTCONES[aid] || 21001;
+ if (el.modalLcId) el.modalLcId.value = curLcId;
+ }
+ selectedLcRank = 5;
+ updateModalLcFullView(curLcId, 5, 80);
+
+ // Auto equip recommended relics if none equipped
+ const hasRelics = (state.fullFreesrData?.relics || []).some(r => r.equip_avatar === aid);
+ if (!hasRelics) {
+ await autoEquipRecommendedRelics(aid, false);
+ }
+
+ await saveAvatarDetails(false);
+ renderShowcaseCard(aid);
+ showToast(t('toastE6S5Applied'), 'success');
+}
+
+// Apply E0S1 Build (Lv.80, Prom 6, E0, Max Skills/Traces, 50% Energy, S1 Light Cone)
+async function applyE0S1Build() {
+ if (!state.selectedAvatarDetail) return;
+ const aid = state.selectedAvatarDetail.avatar_id;
+
+ // Level & Eidolon
+ el.modalCharLevel.value = 80;
+ el.modalCharPromotion.value = 6;
+ state.selectedAvatarDetail.level = 80;
+ state.selectedAvatarDetail.promotion = 6;
+ state.selectedAvatarDetail.rank = 0;
+ renderModalEidolonPips(0);
+
+ // Skills & Traces
+ maxSkillsForAvatar();
+
+ // Energy: 50% standard
+ if (el.rangeCharEnergy && el.valCharEnergy) {
+ const half = Math.floor(parseInt(el.rangeCharEnergy.max, 10) * 0.5);
+ el.rangeCharEnergy.value = half;
+ el.valCharEnergy.textContent = `${half}/${el.rangeCharEnergy.max}`;
+ }
+
+ // Light Cone: Lv 80, S1
+ let curLcId = parseInt(el.modalLcId?.value, 10);
+ if (!curLcId || curLcId === 0) {
+ curLcId = SIGNATURE_LIGHTCONES[aid] || 21001;
+ if (el.modalLcId) el.modalLcId.value = curLcId;
+ }
+ selectedLcRank = 1;
+ updateModalLcFullView(curLcId, 1, 80);
+
+ // Auto equip recommended relics if none equipped
+ const hasRelics = (state.fullFreesrData?.relics || []).some(r => r.equip_avatar === aid);
+ if (!hasRelics) {
+ await autoEquipRecommendedRelics(aid, false);
+ }
+
+ await saveAvatarDetails(false);
+ renderShowcaseCard(aid);
+ showToast(t('toastE0S1Applied'), 'info');
+}
+
+// Render Showcase Card (Matching srtools.punklorde.org/showcase-card)
+function renderShowcaseCard(avatarId) {
+ if (!el.showcaseCardContainer) return;
+ const aid = parseInt(avatarId, 10);
+ const meta = getAvatarInfo(aid);
+ const av = state.fullFreesrData?.avatars?.[aid];
+ const level = av?.level || state.selectedAvatarDetail?.level || 80;
+ const rank = av?.data?.rank ?? state.selectedAvatarDetail?.rank ?? 6;
+
+ // 1. Equipped Light Cone
+ const equippedLc = (state.fullFreesrData?.lightcones || []).find(lc => lc.equip_avatar === aid);
+ const lcId = equippedLc?.item_id || SIGNATURE_LIGHTCONES[aid] || 21001;
+ const lcMeta = LIGHTCONE_METADATA[lcId] || {};
+ const lcInfo = getLightconeInfo(lcId);
+ const lcLevel = equippedLc?.level || 80;
+ const lcRank = equippedLc?.rank || selectedLcRank || 5;
+
+ const baseHp = meta.rarity === 5 ? 1200 : 1058;
+ const baseAtk = meta.rarity === 5 ? 640 : 511;
+ const baseDef = meta.rarity === 5 ? 600 : 573;
+ const baseSpd = 100;
+
+ const lcHp = lcMeta.rarity === 5 ? 1164 : (lcMeta.rarity === 4 ? 953 : 846);
+ const lcAtk = lcMeta.rarity === 5 ? 582 : (lcMeta.rarity === 4 ? 423 : 317);
+ const lcDef = lcMeta.rarity === 5 ? 529 : (lcMeta.rarity === 4 ? 396 : 264);
+
+ // 2. Relics & Set Bonuses
+ const avatarRelics = (state.fullFreesrData?.relics || []).filter(r => r.equip_avatar === aid);
+ const setCounts = {};
+ avatarRelics.forEach(r => {
+ const setId = Math.floor((r.relic_id % 10000) / 10);
+ setCounts[setId] = (setCounts[setId] || 0) + 1;
+ });
+
+ const activeSetBadges = [];
+ for (const sId in setCounts) {
+ const count = setCounts[sId];
+ const sInfo = getRelicSetInfo(sId);
+ if (count >= 4) {
+ activeSetBadges.push({ name: sInfo.name, count: 4 });
+ } else if (count >= 2) {
+ activeSetBadges.push({ name: sInfo.name, count: 2 });
+ }
+ }
+
+ if (activeSetBadges.length === 0) {
+ const rec = RECOMMENDED_RELIC_BUILDS[meta.path] || RECOMMENDED_RELIC_BUILDS.Preservation;
+ activeSetBadges.push({ name: getRelicSetInfo(rec.cavern).name, count: 4 });
+ activeSetBadges.push({ name: getRelicSetInfo(rec.planar).name, count: 2 });
+ }
+
+ // Combat Stats
+ const totalHp = Math.round((baseHp + lcHp) * 1.45 + 706);
+ const totalAtk = Math.round((baseAtk + lcAtk) * 1.35 + 353);
+ const totalDef = Math.round((baseDef + lcDef) * 1.85 + (meta.path === 'Preservation' ? 1200 : 0));
+ const totalSpd = (baseSpd + 25 + 10.5).toFixed(1);
+ const critRate = (5.0 + (meta.path === 'Hunt' || meta.path === 'Destruction' ? 65.4 : 18.8)).toFixed(1);
+ const critDmg = (50.0 + (meta.path === 'Hunt' || meta.path === 'Destruction' ? 132.5 : 26.5)).toFixed(1);
+ const breakEffect = (meta.path === 'Harmony' || meta.path === 'Nihility' ? 142.0 : 10.4).toFixed(1);
+ const effectRes = (10.0).toFixed(1);
+ const err = (19.4).toFixed(1);
+ const ehr = (meta.path === 'Nihility' ? 67.2 : 19.4).toFixed(1);
+ const healing = (meta.path === 'Abundance' ? 34.5 : 0.0).toFixed(1);
+ const elemDmg = (38.8).toFixed(1);
+
+ // Eidolon strip
+ let eidolonStripHtml = '';
+ for (let e = 1; e <= 6; e++) {
+ const isAct = rank >= e;
+ eidolonStripHtml += `
E${e}
`;
+ }
+
+ // Sets HTML
+ let setsHtml = '';
+ activeSetBadges.forEach(sb => {
+ setsHtml += `
+
+ ${sb.name}
+ ${sb.count}-PC
+
+ `;
+ });
+
+ // Relic cards (Slots 1 to 6)
+ const slotNames = {
+ 1: { name: "ส่วนหัว", stat: "HP 706" },
+ 2: { name: "ส่วนมือ", stat: "ATK 353" },
+ 3: { name: "ลำตัว", stat: meta.path === 'Preservation' ? "DEF 54.0%" : (meta.path === 'Hunt' ? "CRIT Rate 32.4%" : "HP 43.2%") },
+ 4: { name: "รองเท้า", stat: "SPD 25.0" },
+ 5: { name: "ลูกแก้ว", stat: `${meta.element} 38.8%` },
+ 6: { name: "สร้อย", stat: meta.path === 'Preservation' || meta.path === 'Harmony' ? "ERR 19.4%" : "ATK 43.2%" }
+ };
+
+ let relicsHtml = '';
+ for (let slot = 1; slot <= 6; slot++) {
+ const equipped = avatarRelics.find(r => (r.relic_id % 10 === slot));
+ const rec = RECOMMENDED_RELIC_BUILDS[meta.path] || RECOMMENDED_RELIC_BUILDS.Preservation;
+ const setId = equipped ? Math.floor((equipped.relic_id % 10000) / 10) : (slot <= 4 ? rec.cavern : rec.planar);
+ const rInfo = getRelicSetInfo(setId);
+ const slotInfo = slotNames[slot];
+
+ relicsHtml += `
+
+
+

+
+15
+
+
+
+ ${rInfo.name}
+ ${slotInfo.stat}
+
+
+ CRIT Rate +${(3.2 + slot * 1.5).toFixed(1)}%
+ CRIT DMG +${(5.8 + slot * 2.1).toFixed(1)}%
+ ATK +${(3.8 + slot * 0.8).toFixed(1)}%
+ DEF +${(4.8 + slot * 1.2).toFixed(1)}%
+
+
+
+ `;
+ }
+
+ const primaryPortrait = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/character_portrait/${aid}.png`;
+ const fallbackPortrait = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/image/character_portrait/${aid}.png`;
+ const lcArtThumb = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/image/light_cone_preview/${lcId}.png`;
+ const fallbackLcThumb = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/image/light_cone_preview/${lcId}.png`;
+
+ el.showcaseCardContainer.innerHTML = `
+
+
+

+
+
${meta.name}
+
+ ${getElementIconHtml(meta.element, 22)}
+ ${getPathIconHtml(meta.path, 22)}
+ Lv. ${level}/80
+
+
+ ${eidolonStripHtml}
+
+
+
+
+
+
+
+
+

+
+
${lcInfo.name}
+
+ S${lcRank} • Lv. ${lcLevel}/80
+
+
+ ❤️ ${lcHp}
+ ⚔️ ${lcAtk}
+ 🛡️ ${lcDef}
+
+
+
+
+
+
+
Active Set Bonuses
+ ${setsHtml}
+
+
+
+
+
❤️ HP${totalHp}
+
⚔️ ATK${totalAtk}
+
🛡️ DEF${totalDef}
+
👟 SPD${totalSpd}
+
🎯 CRIT Rate${critRate}%
+
💥 CRIT DMG${critDmg}%
+
⚡ Break Eff${breakEffect}%
+
🛡️ Eff RES${effectRes}%
+
🌀 ERR${err}%
+
🎯 Eff Hit${ehr}%
+
💚 Healing${healing}%
+
✨ ${meta.element}${elemDmg}%
+
+
+
+
+
+
Equipped Relics (+15)
+ ${relicsHtml}
+
+ `;
+}
+
+// Auto-equip recommended 6-piece relics for avatar
+async function autoEquipRecommendedRelics(avatarId, notify = true) {
+ if (!state.fullFreesrData) {
+ state.fullFreesrData = { avatars: {}, lightcones: [], relics: [] };
+ }
+ state.fullFreesrData.relics = state.fullFreesrData.relics || [];
+ const aid = parseInt(avatarId, 10);
+ const meta = getAvatarInfo(aid);
+ const rec = RECOMMENDED_RELIC_BUILDS[meta.path] || RECOMMENDED_RELIC_BUILDS.Preservation;
+
+ // Clear existing relics on this character
+ state.fullFreesrData.relics = state.fullFreesrData.relics.filter(r => r.equip_avatar !== aid);
+
+ let nextUid = (state.fullFreesrData.relics.reduce((m, r) => Math.max(m, r.internal_uid || 0), 0)) + 1;
+
+ for (let slot = 1; slot <= 6; slot++) {
+ const setId = slot <= 4 ? rec.cavern : rec.planar;
+ const relicId = 60000 + (setId * 10) + slot;
+ state.fullFreesrData.relics.push({
+ internal_uid: nextUid++,
+ relic_id: relicId,
+ level: 15,
+ equip_avatar: aid,
+ main_affix_id: 1,
+ sub_affixes: [
+ { sub_affix_id: 1, cnt: 2, step: 2 },
+ { sub_affix_id: 2, cnt: 3, step: 2 },
+ { sub_affix_id: 3, cnt: 2, step: 2 },
+ { sub_affix_id: 4, cnt: 2, step: 2 }
+ ]
+ });
+ }
+
+ if (state.isConnected) {
+ await saveFreesrChangesToServer();
+ }
+ loadEquipment();
+ renderShowcaseCard(aid);
+ if (notify) {
+ showToast(t('toastRelicsEquipped', { name: meta.name }), 'success');
+ }
+}
+
+// Copy Showcase Card build text to clipboard
+function copyShowcaseText(avatarId) {
+ const meta = getAvatarInfo(avatarId);
+ const av = state.fullFreesrData?.avatars?.[avatarId];
+ const rank = av?.data?.rank ?? state.selectedAvatarDetail?.rank ?? 6;
+ const lc = (state.fullFreesrData?.lightcones || []).find(l => l.equip_avatar === avatarId);
+ const lcInfo = lc ? getLightconeInfo(lc.item_id) : { name: "None", rarity: 0 };
+ const text = `✦ Honkai: Star Rail Character Build ✦
+Name: ${meta.name} (ID: ${avatarId})
+Path: ${meta.path} | Element: ${meta.element}
+Level: 80 | Eidolon: E${rank}
+Light Cone: ${lcInfo.name} (Lv. ${lc?.level || 80} / S${lc?.rank || 1})
+Generated via GaC-Srtool`;
+
+ navigator.clipboard.writeText(text).then(() => {
+ showToast(t('toastShowcaseCopied'), 'success');
+ }).catch(() => {
+ showToast('Failed to copy to clipboard', 'warning');
+ });
+}
+
+// Export Showcase Card as printable / image view
+function exportShowcaseImage(avatarId) {
+ const meta = getAvatarInfo(avatarId);
+ const showcaseEl = document.getElementById('showcase-card-container');
+ if (!showcaseEl) return;
+
+ const win = window.open('', '_blank');
+ if (!win) {
+ showToast('กรุณาอนุญาต Pop-up เพื่อพิมพ์หรือบันทึกรูปภาพการ์ด', 'warning');
+ return;
+ }
+
+ win.document.write(`
+
+
+
+
${meta.name} - Star Rail Build Showcase
+
+
+
+
+
+
+
+
+
+ ${showcaseEl.outerHTML}
+
+
+
+ `);
+ win.document.close();
+ showToast(t('showcaseExport') || 'เปิดหน้าต่างสำหรับบันทึกการ์ดเรียบร้อย', 'info');
+}
+
// ============================================================================
// Quick Action Functions
// ============================================================================
@@ -2252,9 +2904,12 @@ function setupEvents() {
// Character Detail Modal Controls
el.btnCloseAvatarModal.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
document.getElementById('btn-modal-cancel-avatar')?.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
- el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails);
+ el.btnModalSaveAvatar.addEventListener('click', () => saveAvatarDetails(true));
el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar);
- document.getElementById('btn-modal-max-all-char')?.addEventListener('click', maxAllBuildForCurrentAvatar);
+
+ // E6S5 and E0S1 Preset Buttons
+ el.btnModalE6S5?.addEventListener('click', applyE6S5Build);
+ el.btnModalE0S1?.addEventListener('click', applyE0S1Build);
// Steppers for 4 Skills (Basic, Skill, Ultra, Talent)
const setupSkillStepper = (rangeEl, valEl, decBtn, incBtn, maxBtn, maxVal) => {
@@ -2304,16 +2959,51 @@ function setupEvents() {
btn.addEventListener('click', () => {
const r = parseInt(btn.getAttribute('data-rank'), 10);
updateLcRankPips(r);
+ const curLcId = parseInt(el.modalLcId?.value, 10);
+ const curLvl = parseInt(el.modalLcLevel?.value, 10) || 80;
+ if (curLcId) updateModalLcFullView(curLcId, r, curLvl);
});
});
- el.btnModalMaxLc?.addEventListener('click', () => {
- if (el.modalLcLevel) el.modalLcLevel.value = 80;
- updateLcRankPips(5);
- showToast(t('modalBtnMaxLc'), 'info');
+
+ // Lightcone Level Slider & Input sync
+ el.rangeLcLevel?.addEventListener('input', () => {
+ const lvl = parseInt(el.rangeLcLevel.value, 10);
+ if (el.modalLcLevel) el.modalLcLevel.value = lvl;
+ if (el.modalLcLvlVal) el.modalLcLvlVal.textContent = `${lvl} / 80`;
+ if (el.modalLcRankBadge) el.modalLcRankBadge.textContent = `S${selectedLcRank} • Lv.${lvl}`;
});
- el.modalLcId?.addEventListener('input', (e) => {
- updateModalLcPreview(e.target.value);
+ el.modalLcLevel?.addEventListener('input', () => {
+ const lvl = parseInt(el.modalLcLevel.value, 10) || 1;
+ if (el.rangeLcLevel) el.rangeLcLevel.value = lvl;
+ if (el.modalLcLvlVal) el.modalLcLvlVal.textContent = `${lvl} / 80`;
+ if (el.modalLcRankBadge) el.modalLcRankBadge.textContent = `S${selectedLcRank} • Lv.${lvl}`;
+ });
+
+ // Apply change Light Cone button
+ el.btnModalApplyChangeLc?.addEventListener('click', () => {
+ if (!el.selectModalChangeLc || !state.selectedAvatarDetail) return;
+ const newLcId = parseInt(el.selectModalChangeLc.value, 10);
+ const aid = state.selectedAvatarDetail.avatar_id;
+ const curLvl = parseInt(el.modalLcLevel?.value, 10) || 80;
+
+ updateModalLcFullView(newLcId, selectedLcRank, curLvl);
+ const lcInfo = getLightconeInfo(newLcId);
+ showToast(`เปลี่ยน Light Cone เป็น: ${lcInfo.name} เรียบร้อยแล้ว`, 'info');
+ renderShowcaseCard(aid);
+ });
+
+ // Showcase Toolbar Controls
+ el.btnShowcaseCopyText?.addEventListener('click', () => {
+ if (state.selectedAvatarDetail) copyShowcaseText(state.selectedAvatarDetail.avatar_id);
+ });
+
+ el.btnShowcaseExportImg?.addEventListener('click', () => {
+ if (state.selectedAvatarDetail) exportShowcaseImage(state.selectedAvatarDetail.avatar_id);
+ });
+
+ el.btnShowcaseAutoRelics?.addEventListener('click', () => {
+ if (state.selectedAvatarDetail) autoEquipRecommendedRelics(state.selectedAvatarDetail.avatar_id, true);
});
// Quick Preset Buttons for Character Modal
diff --git a/js/i18n.js b/js/i18n.js
index dab6cdf..f11d601 100644
--- a/js/i18n.js
+++ b/js/i18n.js
@@ -20,6 +20,18 @@ export const SUPPORTED_LANGUAGES = [
export const TRANSLATIONS = {
"th": {
"ahaGlobalDesc": "เปิดใช้งานฟังก์ชันพิเศษ Aeon ★ Aha ทั่วทั้งเซิร์ฟเวอร์",
+ "btnE6S5": "⭐ E6S5",
+ "btnE0S1": "⚡ E0S1",
+ "modalTabShowcase": "🖼️ การ์ดแสดงผล",
+ "showcaseAutoRelics": "⚡ ติดตั้งรีลิกส์แนะนำ",
+ "showcaseCopy": "📋 คัดลอกข้อความ",
+ "showcaseExport": "🖼️ ส่งออกรูปภาพ",
+ "btnChangeLc": "เปลี่ยน Light Cone",
+ "lcPassiveTitle": "เอฟเฟกต์สกิลพิเศษ",
+ "toastE6S5Applied": "⭐ ปรับตัวละครเป็น E6S5 (Lv.80, สกิลเต็ม, S5) เรียบร้อย",
+ "toastE0S1Applied": "⚡ ปรับตัวละครเป็น E0S1 (Lv.80, สกิลเต็ม, S1) เรียบร้อย",
+ "toastShowcaseCopied": "📋 คัดลอกข้อมูล Build ไปยังคลิปบอร์ดแล้ว",
+ "toastRelicsEquipped": "⚡ ติดตั้งเซ็ตรีลิกส์แนะนำ (+15) ให้ {name} เรียบร้อยแล้ว",
"btnAddLc": "+ เพิ่ม LC",
"btnAddRelic": "+ เพิ่มรีลิกส์",
"btnAddSlot": "+ เพิ่ม Slot",
@@ -237,6 +249,18 @@ export const TRANSLATIONS = {
},
"en": {
"ahaGlobalDesc": "Enable server-wide Aeon ★ Aha special trait",
+ "btnE6S5": "⭐ E6S5",
+ "btnE0S1": "⚡ E0S1",
+ "modalTabShowcase": "🖼️ Showcase Card",
+ "showcaseAutoRelics": "⚡ Auto-equip Relics",
+ "showcaseCopy": "📋 Copy Text",
+ "showcaseExport": "🖼️ Export Image",
+ "btnChangeLc": "Change Light Cone",
+ "lcPassiveTitle": "Light Cone Ability",
+ "toastE6S5Applied": "⭐ Character build maxed to E6S5 (Lv.80, Max Skills, S5)!",
+ "toastE0S1Applied": "⚡ Character build set to E0S1 (Lv.80, Max Skills, S1)!",
+ "toastShowcaseCopied": "📋 Build information copied to clipboard!",
+ "toastRelicsEquipped": "⚡ Optimal +15 relic set equipped for {name}!",
"btnAddLc": "+ Add LC",
"btnAddRelic": "+ Add Relic",
"btnAddSlot": "+ Add Slot",