- ${t('currentEquipped')} (+${currentRelic.level})
+
+
+
+ ${currentRelic.equip_avatar ? t('currentEquipped') : 'ชิ้นในคลัง'} (+${currentRelic.level || 15})
+
+
+ ✦ ${curMainAffixName}
+
-
+
${curSetInfo.name}
-
- ✦ ${curMainAffixName}
+
+ ${subsSnippet}
-
+
+ ${!slotPickerTargetRelic ? `
+
+ ` : ''}
+ ${currentRelic.equip_avatar ? `
+
+ ` : ''}
+
`;
+ document.getElementById('btn-slot-picker-edit-curr')?.addEventListener('click', () => {
+ openSlotPicker(aid, slot, currentRelic);
+ });
+
document.getElementById('btn-slot-picker-unequip')?.addEventListener('click', async () => {
currentRelic.equip_avatar = 0;
if (state.isConnected) await saveFreesrChangesToServer();
@@ -3243,7 +3352,7 @@ function openSlotPicker(avatarId, slotNum) {
}
}
- // Recommended Set & Main Stat for this slot
+ // Set & Main Stat defaults
const recSetId = slot <= 4 ? rec.cavern : rec.planar;
let recMainStat = 1;
if (slot === 1 || slot === 2) recMainStat = 1;
@@ -3252,8 +3361,26 @@ function openSlotPicker(avatarId, slotNum) {
else if (slot === 5) recMainStat = rec.sphere;
else if (slot === 6) recMainStat = rec.rope;
- slotPickerSelectedSetId = recSetId;
- slotPickerSelectedMainAffix = recMainStat;
+ if (slotPickerTargetRelic) {
+ slotPickerSelectedSetId = Math.floor((slotPickerTargetRelic.relic_id % 10000) / 10);
+ slotPickerSelectedMainAffix = slotPickerTargetRelic.main_affix_id;
+ if (slotPickerTargetRelic.sub_affixes && slotPickerTargetRelic.sub_affixes.length > 0) {
+ slotPickerSubstats = slotPickerTargetRelic.sub_affixes.map(s => ({
+ sub_affix_id: s.sub_affix_id,
+ count: s.count || s.cnt || 1,
+ step: s.step ?? 2
+ }));
+ } else {
+ applySubstatPreset('god', slot, slotPickerSelectedMainAffix, aid);
+ }
+ } else {
+ slotPickerSelectedSetId = recSetId;
+ slotPickerSelectedMainAffix = recMainStat;
+ applySubstatPreset('god', slot, slotPickerSelectedMainAffix, aid);
+ }
+
+ // Clone initial substats for reset button
+ slotPickerInitialSubstats = JSON.parse(JSON.stringify(slotPickerSubstats));
// Render Recommended Highlight Banner
const recHighlight = document.getElementById('slot-picker-rec-highlight');
@@ -3263,7 +3390,7 @@ function openSlotPicker(avatarId, slotNum) {
recHighlight.innerHTML = `
-
@@ -3274,7 +3401,7 @@ function openSlotPicker(avatarId, slotNum) {
✦ ${recMainName}
- ออฟรองแนะนำ 4 แถวเทพ ปรับสเต็ปการอัปเกรดระดับสูงครบถ้วน
+ ออฟรองแนะนำ 4 แถวเทพ ปรับแต่งได้ตามต้องการด้านล่าง
@@ -3284,7 +3411,7 @@ function openSlotPicker(avatarId, slotNum) {
`;
document.getElementById('btn-slot-picker-quick-rec')?.addEventListener('click', async () => {
- await equipCustomSlotRelic(aid, slot, recSetId, recMainStat);
+ await equipCustomSlotRelic(aid, slot, recSetId, recMainStat, slotPickerSubstats);
});
}
@@ -3294,6 +3421,26 @@ function openSlotPicker(avatarId, slotNum) {
// Populate Main Stat Pills
renderSlotPickerMainStatPills(slot, recMainStat);
+ // Populate Substats Customizer
+ renderSlotPickerSubstats(slot, slotPickerSelectedMainAffix);
+
+ // Preset buttons
+ document.querySelectorAll('.btn-substat-preset').forEach(btn => {
+ btn.onclick = () => {
+ const presetKey = btn.getAttribute('data-preset');
+ applySubstatPreset(presetKey, slot, slotPickerSelectedMainAffix, aid);
+ };
+ });
+
+ // Reset Substats button
+ const btnResetSubs = document.getElementById('btn-reset-substats');
+ if (btnResetSubs) {
+ btnResetSubs.onclick = () => {
+ slotPickerSubstats = JSON.parse(JSON.stringify(slotPickerInitialSubstats));
+ renderSlotPickerSubstats(slot, slotPickerSelectedMainAffix);
+ };
+ }
+
// Tab 2: Inventory Grid
renderSlotPickerInventoryGrid(aid, slot);
@@ -3327,20 +3474,205 @@ function openSlotPicker(avatarId, slotNum) {
// Close Button
const btnCloseSlot = document.getElementById('btn-close-slot-picker');
if (btnCloseSlot) {
- btnCloseSlot.onclick = () => modal.classList.remove('active');
+ btnCloseSlot.onclick = () => {
+ modal.classList.remove('active');
+ slotPickerTargetRelic = null;
+ };
}
// Confirm Custom Button
- const btnConfirmCustom = document.getElementById('btn-slot-picker-confirm-custom');
if (btnConfirmCustom) {
btnConfirmCustom.onclick = async () => {
- await equipCustomSlotRelic(aid, slot, slotPickerSelectedSetId, slotPickerSelectedMainAffix);
+ if (slotPickerTargetRelic) {
+ await saveEditedRelicSubstats(slotPickerTargetRelic, slotPickerSelectedSetId, slotPickerSelectedMainAffix, slotPickerSubstats);
+ } else {
+ await equipCustomSlotRelic(aid, slot, slotPickerSelectedSetId, slotPickerSelectedMainAffix, slotPickerSubstats);
+ }
};
}
modal.classList.add('active');
}
+// Render Substats Customizer Grid
+function renderSlotPickerSubstats(slot, mainAffixId) {
+ const grid = document.getElementById('slot-picker-substats-grid');
+ if (!grid) return;
+ grid.innerHTML = '';
+
+ const conflictSub = getConflictingSubstat(slot, mainAffixId);
+
+ // If any selected substat conflicts with main affix, swap it to a valid unused substat
+ slotPickerSubstats.forEach((sub, idx) => {
+ if (sub.sub_affix_id === conflictSub) {
+ const unused = RELIC_SUBSTATS_CONFIG.find(c =>
+ c.id !== conflictSub && !slotPickerSubstats.some((s, i) => i !== idx && s.sub_affix_id === c.id)
+ );
+ if (unused) sub.sub_affix_id = unused.id;
+ }
+ });
+
+ slotPickerSubstats.forEach((sub, idx) => {
+ const card = document.createElement('div');
+ card.className = 'substat-row-card';
+
+ const sVal = formatRelicSubstatValue(sub.sub_affix_id, sub.count);
+
+ let optsHtml = '';
+ RELIC_SUBSTATS_CONFIG.forEach(stat => {
+ const isConflict = stat.id === conflictSub;
+ const isChosenElsewhere = slotPickerSubstats.some((s, i) => i !== idx && s.sub_affix_id === stat.id);
+ const isSelected = stat.id === sub.sub_affix_id;
+ const disabledAttr = (isConflict || (isChosenElsewhere && !isSelected)) ? 'disabled' : '';
+ const conflictTag = isConflict ? ' (ตรงกับ Main Stat)' : (isChosenElsewhere && !isSelected ? ' (เลือกแล้ว)' : '');
+ optsHtml += `
`;
+ });
+
+ card.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Dropdown change
+ card.querySelector('.substat-select')?.addEventListener('change', (e) => {
+ const newId = parseInt(e.target.value, 10);
+ slotPickerSubstats[idx].sub_affix_id = newId;
+ renderSlotPickerSubstats(slot, slotPickerSelectedMainAffix);
+ });
+
+ // Stepper buttons
+ card.querySelector('.btn-substat-step.minus')?.addEventListener('click', () => {
+ if (slotPickerSubstats[idx].count > 1) {
+ slotPickerSubstats[idx].count--;
+ updateSubstatCardPreview(idx);
+ updateTotalRollsCount();
+ }
+ });
+
+ card.querySelector('.btn-substat-step.plus')?.addEventListener('click', () => {
+ if (slotPickerSubstats[idx].count < 25) {
+ slotPickerSubstats[idx].count++;
+ updateSubstatCardPreview(idx);
+ updateTotalRollsCount();
+ }
+ });
+
+ card.querySelector('.substat-count-input')?.addEventListener('input', (e) => {
+ let val = parseInt(e.target.value, 10);
+ if (isNaN(val) || val < 1) val = 1;
+ if (val > 30) val = 30;
+ slotPickerSubstats[idx].count = val;
+ updateSubstatCardPreview(idx);
+ updateTotalRollsCount();
+ });
+
+ grid.appendChild(card);
+ });
+
+ updateTotalRollsCount();
+}
+
+function updateSubstatCardPreview(idx) {
+ const sub = slotPickerSubstats[idx];
+ if (!sub) return;
+ const badge = document.getElementById(`substat-preview-${idx}`);
+ const input = document.querySelector(`.substat-count-input[data-idx="${idx}"]`);
+ if (badge) {
+ const sVal = formatRelicSubstatValue(sub.sub_affix_id, sub.count);
+ badge.textContent = `+${sVal} (${sub.count} โรล)`;
+ }
+ if (input) {
+ input.value = sub.count;
+ }
+}
+
+function updateTotalRollsCount() {
+ const lbl = document.getElementById('slot-picker-total-rolls');
+ if (!lbl) return;
+ const total = slotPickerSubstats.reduce((sum, s) => sum + (parseInt(s.count, 10) || 1), 0);
+ lbl.textContent = total;
+ if (total === 9) {
+ lbl.style.color = '#fbbf24';
+ } else if (total > 9) {
+ lbl.style.color = 'var(--accent-cyan)';
+ } else {
+ lbl.style.color = 'var(--text-muted)';
+ }
+}
+
+function applySubstatPreset(presetKey, slot, mainAffixId, avatarId) {
+ const conflict = getConflictingSubstat(slot, mainAffixId);
+ const aid = parseInt(avatarId, 10);
+ const rec = getOptimalBuildForAvatar(aid);
+
+ let targetIds = [];
+ let rolls = [4, 3, 1, 1];
+
+ if (presetKey === 'god') {
+ const pool = [...rec.substats, 7, 8, 9, 5, 12, 11, 4, 6];
+ for (const sid of pool) {
+ if (targetIds.length >= 4) break;
+ if (sid !== conflict && !targetIds.includes(sid)) targetIds.push(sid);
+ }
+ rolls = [4, 3, 1, 1];
+ } else if (presetKey === 'crit') {
+ const pool = [8, 9, 7, 5, 4, 12, 2];
+ for (const sid of pool) {
+ if (targetIds.length >= 4) break;
+ if (sid !== conflict && !targetIds.includes(sid)) targetIds.push(sid);
+ }
+ rolls = [4, 3, 1, 1];
+ } else if (presetKey === 'spd') {
+ const pool = [7, 8, 9, 5, 12, 4, 2];
+ for (const sid of pool) {
+ if (targetIds.length >= 4) break;
+ if (sid !== conflict && !targetIds.includes(sid)) targetIds.push(sid);
+ }
+ rolls = [5, 2, 1, 1];
+ } else if (presetKey === 'break') {
+ const pool = [12, 7, 5, 4, 11, 6, 2];
+ for (const sid of pool) {
+ if (targetIds.length >= 4) break;
+ if (sid !== conflict && !targetIds.includes(sid)) targetIds.push(sid);
+ }
+ rolls = [5, 2, 1, 1];
+ } else if (presetKey === 'tank') {
+ const pool = [4, 6, 11, 7, 1, 3, 5];
+ for (const sid of pool) {
+ if (targetIds.length >= 4) break;
+ if (sid !== conflict && !targetIds.includes(sid)) targetIds.push(sid);
+ }
+ rolls = [4, 3, 1, 1];
+ }
+
+ // Ensure 4 distinct items
+ while (targetIds.length < 4) {
+ const fallback = RELIC_SUBSTATS_CONFIG.find(c => c.id !== conflict && !targetIds.includes(c.id));
+ if (fallback) targetIds.push(fallback.id);
+ else break;
+ }
+
+ slotPickerSubstats = targetIds.slice(0, 4).map((sid, i) => ({
+ sub_affix_id: sid,
+ count: rolls[i] || 1,
+ step: 2
+ }));
+
+ renderSlotPickerSubstats(slot, mainAffixId);
+}
+
// Render Set Grid for Slot
function renderSlotPickerSetGrid(slot, recSetId) {
const grid = document.getElementById('slot-picker-set-grid');
@@ -3366,7 +3698,7 @@ function renderSlotPickerSetGrid(slot, recSetId) {
const isSelected = sId === slotPickerSelectedSetId;
card.className = `slot-picker-set-card ${isSelected ? 'selected' : ''}`;
card.innerHTML = `
-
${sInfo.name}
${sId === recSetId ? `
⭐ ${t('recommendedBadge')}` : ''}
@@ -3444,6 +3776,7 @@ function renderSlotPickerMainStatPills(slot, recMainStat) {
slotPickerSelectedMainAffix = item.id;
container.querySelectorAll('.slot-picker-pill').forEach(p => p.classList.remove('selected'));
pill.classList.add('selected');
+ renderSlotPickerSubstats(slot, slotPickerSelectedMainAffix);
});
container.appendChild(pill);
@@ -3486,7 +3819,7 @@ function renderSlotPickerInventoryGrid(aid, slot) {
card.innerHTML = `
-
@@ -3506,7 +3839,9 @@ function renderSlotPickerInventoryGrid(aid, slot) {
${(r.sub_affixes || []).map(s => {
const sName = getRelicSubAffixName(s.sub_affix_id);
- return `
• ${sName} (+${s.cnt})
`;
+ const rolls = s.count || s.cnt || 1;
+ const val = formatRelicSubstatValue(s.sub_affix_id, rolls);
+ return `
• ${sName} +${val} (+${rolls})
`;
}).join('')}
@@ -3545,7 +3880,7 @@ function renderSlotPickerInventoryGrid(aid, slot) {
}
// Equip custom created relic for avatar slot
-async function equipCustomSlotRelic(avatarId, slotNum, setId, mainAffixId) {
+async function equipCustomSlotRelic(avatarId, slotNum, setId, mainAffixId, customSubAffixes = null) {
const aid = parseInt(avatarId, 10);
const rec = getOptimalBuildForAvatar(aid);
const relicId = 60000 + (setId * 10) + slotNum;
@@ -3559,40 +3894,43 @@ async function equipCustomSlotRelic(avatarId, slotNum, setId, mainAffixId) {
}
});
- const mainToSubConflict = {
- 3: { 1: 4, 2: 5, 3: 6, 4: 8, 5: 9, 7: 10 },
- 4: { 1: 4, 2: 5, 3: 6, 4: 7 },
- 5: { 1: 4, 2: 5, 3: 6 },
- 6: { 1: 12, 3: 4, 4: 5, 5: 6 }
- };
- const conflictingSub = mainToSubConflict[slotNum]?.[mainAffixId];
-
- const pool = [...rec.substats, 7, 8, 9, 5, 12, 11, 4, 6];
- const chosenSubs = [];
- for (const sId of pool) {
- if (chosenSubs.length >= 4) break;
- if (sId !== conflictingSub && !chosenSubs.includes(sId)) {
- chosenSubs.push(sId);
+ let subAffixes = [];
+ if (customSubAffixes && customSubAffixes.length > 0) {
+ subAffixes = customSubAffixes.map(s => ({
+ sub_affix_id: parseInt(s.sub_affix_id, 10),
+ cnt: parseInt(s.count || s.cnt || 1, 10),
+ count: parseInt(s.count || s.cnt || 1, 10),
+ step: 2
+ }));
+ } else {
+ const conflictingSub = getConflictingSubstat(slotNum, mainAffixId);
+ const pool = [...rec.substats, 7, 8, 9, 5, 12, 11, 4, 6];
+ const chosenSubs = [];
+ for (const sId of pool) {
+ if (chosenSubs.length >= 4) break;
+ if (sId !== conflictingSub && !chosenSubs.includes(sId)) {
+ chosenSubs.push(sId);
+ }
}
+ const rolls = [4, 3, 1, 1];
+ subAffixes = chosenSubs.map((sid, idx) => ({
+ sub_affix_id: sid,
+ cnt: rolls[idx] || 1,
+ count: rolls[idx] || 1,
+ step: 2
+ }));
}
- const rolls = [4, 3, 1, 1];
- const subAffixes = chosenSubs.map((sid, idx) => ({
- sub_affix_id: sid,
- cnt: rolls[idx] || 1,
- count: rolls[idx] || 1,
- step: 2
- }));
-
const nextUid = (state.fullFreesrData.relics.reduce((m, r) => Math.max(m, r.internal_uid || 0), 0)) + 1;
- state.fullFreesrData.relics.push({
+ const newRelic = {
internal_uid: nextUid,
relic_id: relicId,
level: 15,
equip_avatar: aid,
main_affix_id: mainAffixId,
sub_affixes: subAffixes
- });
+ };
+ state.fullFreesrData.relics.push(newRelic);
if (state.isConnected) await saveFreesrChangesToServer();
renderModalRelicsTab(aid);
@@ -3601,7 +3939,36 @@ async function equipCustomSlotRelic(avatarId, slotNum, setId, mainAffixId) {
loadEquipment();
document.getElementById('modal-select-relic-slot')?.classList.remove('active');
const setInfo = getRelicSetInfo(setId);
- showToast(`⚡ สวมใส่ ${setInfo.name} (+15 5★ ออฟเทพ) เรียบร้อยแล้ว!`, 'success');
+ showToast(`⚡ สวมใส่ ${setInfo.name} (+15 5★ ออฟที่เลือก) เรียบร้อยแล้ว!`, 'success');
+}
+
+// Save edits to existing relic (main stat & substats)
+async function saveEditedRelicSubstats(relic, newSetId, newMainAffixId, newSubAffixes) {
+ if (!relic) return;
+ const slotNum = (relic.relic_id % 10) || 1;
+ if (newSetId) {
+ relic.relic_id = 60000 + (newSetId * 10) + slotNum;
+ }
+ relic.main_affix_id = newMainAffixId;
+ relic.sub_affixes = newSubAffixes.map(s => ({
+ sub_affix_id: parseInt(s.sub_affix_id, 10),
+ cnt: parseInt(s.count || s.cnt || 1, 10),
+ count: parseInt(s.count || s.cnt || 1, 10),
+ step: 2
+ }));
+
+ if (state.isConnected) await saveFreesrChangesToServer();
+ if (relic.equip_avatar) {
+ renderModalRelicsTab(relic.equip_avatar);
+ renderModalOverviewTab(relic.equip_avatar);
+ renderShowcaseCard(relic.equip_avatar);
+ }
+ loadEquipment();
+ showRelicInspector(relic);
+ document.getElementById('modal-select-relic-slot')?.classList.remove('active');
+ slotPickerTargetRelic = null;
+ const rInfo = getRelicSetInfo(newSetId || Math.floor((relic.relic_id % 10000) / 10));
+ showToast(`💾 บันทึกการแก้ไขออฟรอง ${rInfo.name} เรียบร้อย!`, 'success');
}
async function quickGenSlotRelic(avatarId, slotNum) {
@@ -4529,22 +4896,41 @@ function renderShowcaseCard(avatarId) {
const rInfo = getRelicSetInfo(setId);
const slotInfo = slotNames[slot];
+ let mainStatDisplay = slotInfo.stat;
+ if (equipped && equipped.main_affix_id) {
+ mainStatDisplay = getRelicMainAffixName(slot, equipped.main_affix_id);
+ }
+
+ let subsDisplayHtml = '';
+ if (equipped && equipped.sub_affixes && equipped.sub_affixes.length > 0) {
+ subsDisplayHtml = equipped.sub_affixes.map(sub => {
+ const sName = getRelicSubAffixName(sub.sub_affix_id);
+ const rolls = sub.count || sub.cnt || 1;
+ const val = formatRelicSubstatValue(sub.sub_affix_id, rolls);
+ return `
${sName} +${val}${rollBadge(rolls)}`;
+ }).join('');
+ } else {
+ subsDisplayHtml = `
+
CRIT Rate +${(3.2 + slot * 1.5).toFixed(1)}%${rollBadge(slot % 2 === 0 ? 3 : 2)}
+
CRIT DMG +${(5.8 + slot * 2.1).toFixed(1)}%${rollBadge(slot % 3 === 0 ? 4 : 2)}
+
ATK +${(3.8 + slot * 0.8).toFixed(1)}%${rollBadge(1)}
+
DEF +${(4.8 + slot * 1.2).toFixed(1)}%${rollBadge(1)}
+ `;
+ }
+
relicsHtml += `
-

+
+15
${rInfo.name}
- ${slotInfo.stat}
+ ✦ ${mainStatDisplay}
- CRIT Rate +${(3.2 + slot * 1.5).toFixed(1)}%${rollBadge(slot % 2 === 0 ? 3 : 2)}
- CRIT DMG +${(5.8 + slot * 2.1).toFixed(1)}%${rollBadge(slot % 3 === 0 ? 4 : 2)}
- ATK +${(3.8 + slot * 0.8).toFixed(1)}%${rollBadge(1)}
- DEF +${(4.8 + slot * 1.2).toFixed(1)}%${rollBadge(1)}
+ ${subsDisplayHtml}
@@ -5543,11 +5929,13 @@ function showRelicInspector(relic) {
avatarOpts += `
`;
});
+ const mainAffixName = getRelicMainAffixName(slotNum, relic.main_affix_id);
+
el.eqInspectorCard.style.display = 'flex';
el.eqInspectorCard.innerHTML = `
-

+
${rInfo.name}
★★★★★
@@ -5562,26 +5950,39 @@ function showRelicInspector(relic) {
-
+
-
-
Main Stat
-
+15 Max
+
+ สเตตัสหลัก (Main Stat)
+ ✦ ${mainAffixName}
-
- CRIT Rate / DMG
- +3.2% / +6.4%
-
-
-
SPD / ATK
-
+4 / +4.3%
+
+ ออฟรอง (Substats):
+ ${(relic.sub_affixes && relic.sub_affixes.length > 0) ? relic.sub_affixes.map(s => {
+ const sName = getRelicSubAffixName(s.sub_affix_id);
+ const rolls = s.count || s.cnt || 1;
+ const val = formatRelicSubstatValue(s.sub_affix_id, rolls);
+ return `
+
+ • ${sName}
+
+ +${val} +${rolls}
+
+
+ `;
+ }).join('') : '
ไม่มีข้อมูลออฟรอง
'}
+
+
+
-
+
Set Bonus (${rInfo.name})
-
2-Piece: เพิ่มประสิทธิภาพตามเซ็ตที่กำหนด
4-Piece: ปลดล็อกเอฟเฟกต์ต่อสู้ขั้นสูง
+
2-Piece: ${rInfo.desc2 || 'เพิ่มประสิทธิภาพตามเซ็ตที่กำหนด'}
4-Piece: ${rInfo.desc4 || 'ปลดล็อกเอฟเฟกต์ต่อสู้ขั้นสูง'}
@@ -5598,6 +5999,10 @@ function showRelicInspector(relic) {
`;
+ document.getElementById('btn-inspector-edit-subs')?.addEventListener('click', () => {
+ openSlotPickerForRelic(relic);
+ });
+
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'));