Merge branch 'test-gay'

This commit is contained in:
CAtlas0784 committed 2026-10-17 13:25:51 +07:00
commit 5a2ac581c9
3 files changed
+663 -81

No files matched your search

+149
View File
@@ -3006,6 +3006,155 @@ input:checked + .slider:before {
border-color: var(--accent-gold);
}
/* Substats Customizer */
.slot-picker-substats-section {
background: rgba(15, 23, 42, 0.45);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.85rem;
margin-top: 0.85rem;
}
.substat-presets-strip {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.btn-substat-preset {
background: rgba(18, 24, 38, 0.8);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 0.725rem;
font-weight: 600;
padding: 0.25rem 0.55rem;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-substat-preset:hover {
border-color: var(--accent-cyan);
background: rgba(56, 189, 248, 0.15);
color: #fff;
}
.slot-picker-substats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.65rem;
margin-top: 0.5rem;
}
.substat-row-card {
background: rgba(18, 24, 38, 0.85);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.65rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.45rem;
transition: all 0.2s ease;
}
.substat-row-card:hover {
border-color: rgba(56, 189, 248, 0.4);
}
.substat-row-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
}
.substat-slot-label {
font-weight: 700;
color: var(--text-muted);
}
.substat-preview-badge {
font-weight: 800;
color: var(--accent-cyan);
font-family: monospace;
font-size: 0.825rem;
background: rgba(56, 189, 248, 0.12);
padding: 0.15rem 0.45rem;
border-radius: 4px;
border: 1px solid rgba(56, 189, 248, 0.25);
}
.substat-row-controls {
display: flex;
gap: 0.45rem;
align-items: center;
}
.substat-select {
flex: 1;
min-width: 0;
padding: 0.4rem 0.5rem;
font-size: 0.8rem;
background: rgba(10, 14, 23, 0.9);
border: 1px solid var(--border-color);
border-radius: 6px;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.substat-select:focus {
border-color: var(--accent-cyan);
outline: none;
}
.substat-stepper {
display: flex;
align-items: center;
background: rgba(10, 14, 23, 0.9);
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
}
.btn-substat-step {
background: transparent;
border: none;
color: var(--text-color);
width: 26px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background 0.15s;
}
.btn-substat-step:hover {
background: rgba(56, 189, 248, 0.2);
color: var(--accent-cyan);
}
.substat-count-input {
width: 32px;
border: none;
background: transparent;
text-align: center;
color: var(--accent-gold);
font-weight: 800;
font-family: monospace;
font-size: 0.875rem;
-moz-appearance: textfield;
}
.substat-count-input::-webkit-outer-spin-button,
.substat-count-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Git Branch Indicators */
.badge-branch {
+30 -2
View File
@@ -1533,9 +1533,37 @@
<!-- Injected main stat pill buttons -->
</div>
<div style="margin-top: 1rem; display: flex; justify-content: flex-end;">
<!-- Substats Customizer -->
<div class="slot-picker-substats-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
<div style="font-weight: 700; font-size: 0.85rem; color: var(--text-highlight);">
🔧 ปรับแต่งออฟรอง (Substats Customizer):
</div>
<div class="substat-presets-strip">
<button type="button" class="btn-substat-preset" data-preset="god" title="ออฟแนะนำตามตัวละคร">⭐ ออฟเทพแนะนำ</button>
<button type="button" class="btn-substat-preset" data-preset="crit" title="เน้นคริล้วน">🎯 คริ Rate/DMG</button>
<button type="button" class="btn-substat-preset" data-preset="spd" title="เน้นความเร็ว">⚡ สปีด SPD</button>
<button type="button" class="btn-substat-preset" data-preset="break" title="เน้นทำลายล้าง">🔨 Break Effect</button>
<button type="button" class="btn-substat-preset" data-preset="tank" title="เน้นถึก ป้องกัน เลือด">🛡️ ถึก HP/DEF</button>
</div>
</div>
<div class="slot-picker-substats-grid" id="slot-picker-substats-grid">
<!-- Injected by JS: 4 substat rows with selects, steppers & previews -->
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.65rem; padding: 0.4rem 0.75rem; background: rgba(10, 14, 23, 0.75); border-radius: var(--radius-sm); border: 1px solid var(--border-color); font-size: 0.775rem;">
<span style="color: var(--text-muted);">
💡 รวมจำนวนโรล (Rolls): <strong id="slot-picker-total-rolls" style="color: var(--accent-gold); font-family: monospace; font-size: 0.9rem;">9</strong> / 9 โรล (ระดับ 5★ เลเวล 15)
</span>
<button type="button" class="btn btn-sm btn-secondary" id="btn-reset-substats" style="font-size: 0.725rem; padding: 0.2rem 0.6rem;">↺ คืนค่าเดิม</button>
</div>
</div>
<div style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div id="slot-picker-edit-mode-indicator" style="font-size: 0.8rem; color: var(--accent-cyan); font-weight: 700;"></div>
<button type="button" class="btn btn-primary" id="btn-slot-picker-confirm-custom" style="padding: 0.65rem 1.4rem; font-weight: 800; font-size: 0.9rem;" data-i18n="btnEquipInstant">
⚡ สวมใส่ชิ้นนี้ทันที (+15 5★ ออฟเทพ)
⚡ สวมใส่ชิ้นนี้ทันที (+15 5★ ออฟที่เลือก)
</button>
</div>
</div>
+484 -79
View File
@@ -2609,6 +2609,52 @@ function getRelicSubAffixName(affixId) {
return map[affixId] || `Sub Affix ${affixId}`;
}
export const RELIC_SUBSTATS_CONFIG = [
{ id: 8, name: "CRIT Rate%", short: "CRIT Rate", icon: "🎯" },
{ id: 9, name: "CRIT DMG%", short: "CRIT DMG", icon: "💥" },
{ id: 7, name: "SPD", short: "SPD", icon: "⚡" },
{ id: 5, name: "ATK%", short: "ATK%", icon: "⚔️" },
{ id: 4, name: "HP%", short: "HP%", icon: "❤️" },
{ id: 6, name: "DEF%", short: "DEF%", icon: "🛡️" },
{ id: 12, name: "Break Effect%", short: "Break", icon: "🔨" },
{ id: 10, name: "Effect Hit Rate%", short: "EHR", icon: "🎯" },
{ id: 11, name: "Effect RES%", short: "RES", icon: "🔰" },
{ id: 2, name: "ATK (Flat)", short: "ATK", icon: "🗡️" },
{ id: 1, name: "HP (Flat)", short: "HP", icon: "🩸" },
{ id: 3, name: "DEF (Flat)", short: "DEF", icon: "🧱" }
];
export function formatRelicSubstatValue(affixId, rolls = 1) {
const r = parseInt(rolls, 10) || 1;
switch (parseInt(affixId, 10)) {
case 1: return `${Math.round(42 * r)}`;
case 2: return `${Math.round(21 * r)}`;
case 3: return `${Math.round(21 * r)}`;
case 4: return `${(4.3 * r).toFixed(1)}%`;
case 5: return `${(4.3 * r).toFixed(1)}%`;
case 6: return `${(5.4 * r).toFixed(1)}%`;
case 7: return `${(2.6 * r).toFixed(1)}`;
case 8: return `${(3.2 * r).toFixed(1)}%`;
case 9: return `${(6.4 * r).toFixed(1)}%`;
case 10: return `${(4.3 * r).toFixed(1)}%`;
case 11: return `${(4.3 * r).toFixed(1)}%`;
case 12: return `${(6.4 * r).toFixed(1)}%`;
default: return `${r}`;
}
}
export function getConflictingSubstat(slot, mainAffixId) {
const map = {
1: { 1: 1 },
2: { 1: 2 },
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 }
};
return map[slot]?.[mainAffixId] || null;
}
const STAR_RAIL_RES_LANGS = {
th: 'th', en: 'en', es: 'es', pt: 'pt', ru: 'ru', vi: 'vi',
de: 'de', fr: 'fr', id: 'id', ja: 'jp', ko: 'kr', zh: 'cn', zh_tw: 'cht'
@@ -3093,10 +3139,18 @@ function renderModalRelicsTab(avatarId) {
const rInfo = getRelicSetInfo(setId);
const mainAffixName = getRelicMainAffixName(slot, equipped.main_affix_id);
const subsSnippetHtml = (equipped.sub_affixes && equipped.sub_affixes.length > 0)
? equipped.sub_affixes.slice(0, 4).map(s => {
const sName = getRelicSubAffixName(s.sub_affix_id);
const rolls = s.count || s.cnt || 1;
return `${sName} (+${rolls})`;
}).join(' • ')
: '4 ออปชั่นรอง (คลิกเพื่อปรับแต่ง)';
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"
<img src="https://cdn.jsdelivr.net/gh/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">${slotInfo.icon} ${slotInfo.name.split(' ')[0]}</span>
</div>
@@ -3106,7 +3160,9 @@ function renderModalRelicsTab(avatarId) {
<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 class="relic-slot-subs" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.72rem; color: var(--accent-cyan);" title="${subsSnippetHtml}">
${subsSnippetHtml}
</div>
</div>
<button type="button" class="btn-slot-unequip-corner" title="ถอดชิ้นนี้">✕</button>
`;
@@ -3167,13 +3223,31 @@ function renderModalRelicsTab(avatarId) {
}
}
// Open Relic Slot Picker Modal (Spacious, Tabbed, Custom Set & Main Stat Builder + Inventory)
// Open Relic Slot Picker Modal (Spacious, Tabbed, Custom Set & Main Stat Builder + Substats Customizer + Inventory)
let slotPickerSelectedSetId = 101;
let slotPickerSelectedMainAffix = 1;
let slotPickerTargetRelic = null; // When non-null, editing an existing relic
let slotPickerSubstats = [
{ sub_affix_id: 8, count: 4, step: 2 },
{ sub_affix_id: 9, count: 3, step: 2 },
{ sub_affix_id: 7, count: 1, step: 2 },
{ sub_affix_id: 5, count: 1, step: 2 }
];
let slotPickerInitialSubstats = [];
function openSlotPicker(avatarId, slotNum) {
export function openSlotPickerForRelic(relic) {
if (!relic) return;
const slotNum = (relic.relic_id % 10) || 1;
const aid = relic.equip_avatar || state.selectedAvatarDetail?.avatar_id || 1001;
openSlotPicker(aid, slotNum, relic);
}
window.openSlotPickerForRelic = openSlotPickerForRelic;
function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) {
state.slotPickerAvatarId = parseInt(avatarId, 10);
state.slotPickerSlot = parseInt(slotNum, 10);
slotPickerTargetRelic = targetRelicToEdit || null;
const aid = state.slotPickerAvatarId;
const slot = state.slotPickerSlot;
const meta = getAvatarInfo(aid);
@@ -3186,17 +3260,27 @@ function openSlotPicker(avatarId, slotNum) {
// Title & Subtitle
const titleEl = document.getElementById('modal-slot-picker-title');
const subtitleEl = document.getElementById('modal-slot-picker-subtitle');
if (titleEl) {
titleEl.textContent = `${t('relicPickerTitle').replace('{slot}', slotInfo.name)} - ${meta.name}`;
}
if (subtitleEl) {
subtitleEl.textContent = t('quickGenRecDesc');
const editModeIndicator = document.getElementById('slot-picker-edit-mode-indicator');
const btnConfirmCustom = document.getElementById('btn-slot-picker-confirm-custom');
if (slotPickerTargetRelic) {
const curSetId = Math.floor((slotPickerTargetRelic.relic_id % 10000) / 10);
const curSetInfo = getRelicSetInfo(curSetId);
if (titleEl) titleEl.textContent = `🔧 แก้ไขรีลิกส์: ${curSetInfo.name} (${slotInfo.name}) - ${meta.name}`;
if (subtitleEl) subtitleEl.textContent = `ปรับแต่งเซ็ต สเตตัสหลัก และออฟรองของรีลิกส์ชิ้นนี้ (#${slotPickerTargetRelic.internal_uid})`;
if (editModeIndicator) editModeIndicator.innerHTML = `✏️ กำลังแก้ไขรีลิกส์ <strong>#${slotPickerTargetRelic.internal_uid}</strong>`;
if (btnConfirmCustom) btnConfirmCustom.textContent = `💾 บันทึกการแก้ไขออฟรองชิ้นนี้`;
} else {
if (titleEl) titleEl.textContent = `${t('relicPickerTitle').replace('{slot}', slotInfo.name)} - ${meta.name}`;
if (subtitleEl) subtitleEl.textContent = t('quickGenRecDesc');
if (editModeIndicator) editModeIndicator.textContent = '';
if (btnConfirmCustom) btnConfirmCustom.textContent = `⚡ สวมใส่ชิ้นนี้ทันที (+15 5★ ออฟที่เลือก)`;
}
// Current Equipped Relic Banner
const currentBox = document.getElementById('slot-picker-current-box');
const allRelics = state.fullFreesrData?.relics || [];
const currentRelic = allRelics.find(r => r.equip_avatar === aid && (r.relic_id % 10) === slot);
const currentRelic = slotPickerTargetRelic || allRelics.find(r => r.equip_avatar === aid && (r.relic_id % 10) === slot);
if (currentBox) {
if (currentRelic) {
@@ -3204,29 +3288,54 @@ function openSlotPicker(avatarId, slotNum) {
const curSetInfo = getRelicSetInfo(curSetId);
const curMainAffixName = getRelicMainAffixName(slot, currentRelic.main_affix_id);
const subsSnippet = (currentRelic.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 `<span style="display: inline-block; background: rgba(0,0,0,0.35); border: 1px solid var(--border-color); border-radius: 4px; padding: 2px 6px; font-size: 0.725rem; color: #fff;">${sName} +${val} <strong style="color:var(--accent-gold);">(+${rolls})</strong></span>`;
}).join(' ');
currentBox.className = 'slot-picker-current-banner';
currentBox.innerHTML = `
<div style="display: flex; gap: 0.85rem; align-items: center; min-width: 0;">
<img src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${curSetId}.png"
<div style="display: flex; gap: 0.85rem; align-items: center; min-width: 0; flex: 1;">
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${curSetId}.png"
onerror="this.src='./assets/logo.png'" alt="Relic"
style="width: 48px; height: 48px; object-fit: contain; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));">
<div>
<div style="font-size: 0.75rem; color: var(--accent-gold); font-weight: 700;">
${t('currentEquipped')} (+${currentRelic.level})
<div style="min-width: 0;">
<div style="display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;">
<span style="font-size: 0.75rem; color: var(--accent-gold); font-weight: 700;">
${currentRelic.equip_avatar ? t('currentEquipped') : 'ชิ้นในคลัง'} (+${currentRelic.level || 15})
</span>
<span style="font-size: 0.75rem; color: var(--accent-cyan); font-weight: 800;">
✦ ${curMainAffixName}
</span>
</div>
<div style="font-size: 1rem; font-weight: 800; color: #fff;">
<div style="font-size: 0.95rem; font-weight: 800; color: #fff; margin: 2px 0;">
${curSetInfo.name}
</div>
<div style="font-size: 0.825rem; color: var(--accent-cyan); font-weight: 700;">
✦ ${curMainAffixName}
<div style="display: flex; gap: 0.35rem; flex-wrap: wrap; margin-top: 3px;">
${subsSnippet}
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-danger" id="btn-slot-picker-unequip" style="padding: 0.45rem 1rem; font-weight: 700;">
${t('btnUnequip')}
</button>
<div style="display: flex; gap: 0.4rem; align-items: center;">
${!slotPickerTargetRelic ? `
<button type="button" class="btn btn-sm btn-warning" id="btn-slot-picker-edit-curr" style="padding: 0.45rem 0.85rem; font-weight: 700;" title="โหลดข้อมูลชิ้นนี้ลงมาแก้ไขออฟรอง">
✏️ แก้ไขออฟรองชิ้นนี้
</button>
` : ''}
${currentRelic.equip_avatar ? `
<button type="button" class="btn btn-sm btn-danger" id="btn-slot-picker-unequip" style="padding: 0.45rem 0.85rem; font-weight: 700;">
${t('btnUnequip')}
</button>
` : ''}
</div>
`;
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 = `
<div style="display: flex; gap: 0.85rem; align-items: center;">
<img src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${recSetId}.png"
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${recSetId}.png"
onerror="this.src='./assets/logo.png'" alt="Relic"
style="width: 52px; height: 52px; object-fit: contain;">
<div>
@@ -3274,7 +3401,7 @@ function openSlotPicker(avatarId, slotNum) {
✦ ${recMainName}
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 2px;">
ออฟรองแนะนำ 4 แถวเทพ ปรับสเต็ปการอัปเกรดระดับสูงครบถ้วน
ออฟรองแนะนำ 4 แถวเทพ ปรับแต่งได้ตามต้องการด้านล่าง
</div>
</div>
</div>
@@ -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 += `<option value="${stat.id}" ${isSelected ? 'selected' : ''} ${disabledAttr}>${stat.icon} ${stat.name}${conflictTag}</option>`;
});
card.innerHTML = `
<div class="substat-row-header">
<span class="substat-slot-label">ออฟรอง ${idx + 1}</span>
<span class="substat-preview-badge" id="substat-preview-${idx}">+${sVal} (${sub.count} โรล)</span>
</div>
<div class="substat-row-controls">
<select class="form-select substat-select" data-idx="${idx}">
${optsHtml}
</select>
<div class="substat-stepper">
<button type="button" class="btn-substat-step minus" data-idx="${idx}" title="ลดโรล">−</button>
<input type="number" class="substat-count-input" data-idx="${idx}" min="1" max="25" value="${sub.count}">
<button type="button" class="btn-substat-step plus" data-idx="${idx}" title="เพิ่มโรล">+</button>
</div>
</div>
`;
// 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 = `
<img src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${sId}.png"
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${sId}.png"
onerror="this.src='./assets/logo.png'" alt="${sInfo.name}">
<span title="${sInfo.name}">${sInfo.name}</span>
${sId === recSetId ? `<span style="font-size: 0.65rem; color: var(--accent-gold); font-weight: 800;">⭐ ${t('recommendedBadge')}</span>` : ''}
@@ -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 = `
<div style="display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;">
<div style="display: flex; gap: 0.65rem; align-items: center; min-width: 0;">
<img src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${setId}.png"
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png"
onerror="this.src='./assets/logo.png'" alt="${rInfo.name}"
style="width: 40px; height: 40px; object-fit: contain;">
<div style="min-width: 0;">
@@ -3506,7 +3839,9 @@ function renderSlotPickerInventoryGrid(aid, slot) {
<div style="font-size: 0.75rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.2rem; padding: 0.35rem 0.5rem; background: rgba(0,0,0,0.25); border-radius: 4px;">
${(r.sub_affixes || []).map(s => {
const sName = getRelicSubAffixName(s.sub_affix_id);
return `<div>• ${sName} (+${s.cnt})</div>`;
const rolls = s.count || s.cnt || 1;
const val = formatRelicSubstatValue(s.sub_affix_id, rolls);
return `<div>• ${sName} +${val} (+${rolls})</div>`;
}).join('')}
</div>
@@ -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 `<span>${sName} +${val}${rollBadge(rolls)}</span>`;
}).join('');
} else {
subsDisplayHtml = `
<span>CRIT Rate +${(3.2 + slot * 1.5).toFixed(1)}%${rollBadge(slot % 2 === 0 ? 3 : 2)}</span>
<span>CRIT DMG +${(5.8 + slot * 2.1).toFixed(1)}%${rollBadge(slot % 3 === 0 ? 4 : 2)}</span>
<span>ATK +${(3.8 + slot * 0.8).toFixed(1)}%${rollBadge(1)}</span>
<span>DEF +${(4.8 + slot * 1.2).toFixed(1)}%${rollBadge(1)}</span>
`;
}
relicsHtml += `
<div class="showcase-relic-card">
<div class="showcase-relic-icon-wrap">
<img class="showcase-relic-img" src="https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/relic/${setId}.png" onerror="this.src='./assets/logo.png'" alt="Relic">
<img class="showcase-relic-img" src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png" onerror="this.src='./assets/logo.png'" alt="Relic">
<span class="showcase-relic-lvl-tag">+15</span>
</div>
<div class="showcase-relic-info">
<div class="showcase-relic-main-stat">
<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>
<span style="color: var(--accent-cyan); font-weight: 800; flex-shrink: 0;">✦ ${mainStatDisplay}</span>
</div>
<div class="showcase-relic-subs">
<span>CRIT Rate +${(3.2 + slot * 1.5).toFixed(1)}%${rollBadge(slot % 2 === 0 ? 3 : 2)}</span>
<span>CRIT DMG +${(5.8 + slot * 2.1).toFixed(1)}%${rollBadge(slot % 3 === 0 ? 4 : 2)}</span>
<span>ATK +${(3.8 + slot * 0.8).toFixed(1)}%${rollBadge(1)}</span>
<span>DEF +${(4.8 + slot * 1.2).toFixed(1)}%${rollBadge(1)}</span>
${subsDisplayHtml}
</div>
</div>
</div>
@@ -5543,11 +5929,13 @@ function showRelicInspector(relic) {
avatarOpts += `<option value="${av.avatar_id}" ${sel}>${info.name} (ID: ${av.avatar_id})</option>`;
});
const mainAffixName = getRelicMainAffixName(slotNum, relic.main_affix_id);
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}">
<img class="eq-inspector-thumb" src="https://cdn.jsdelivr.net/gh/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>
@@ -5562,26 +5950,39 @@ function showRelicInspector(relic) {
<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 -->
<!-- Real 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 class="eq-inspector-stat-row" style="border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 0.35rem; margin-bottom: 0.35rem;">
<span style="color: var(--text-highlight); font-weight: 700;">สเตตัสหลัก (Main Stat)</span>
<strong style="color: var(--accent-cyan); font-family: monospace;">✦ ${mainAffixName}</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 style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; margin-bottom: 0.25rem;">
ออฟรอง (Substats):
</div>
${(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 `
<div class="eq-inspector-stat-row">
<span>• ${sName}</span>
<strong style="color: var(--accent-gold); font-family: monospace;">
+${val} <span style="font-size: 0.65rem; background: rgba(234,179,8,0.2); color: #fbbf24; border-radius: 3px; padding: 1px 4px; font-weight: 800;">+${rolls}</span>
</strong>
</div>
`;
}).join('') : '<div style="color: var(--text-muted); font-size: 0.75rem;">ไม่มีข้อมูลออฟรอง</div>'}
</div>
<!-- Edit Substats Action Button -->
<button type="button" class="btn btn-warning" id="btn-inspector-edit-subs" style="margin-top: 0.35rem; font-weight: 800; font-size: 0.825rem; display: flex; align-items: center; justify-content: center; gap: 0.4rem;">
🔧 ปรับแต่งออฟรอง / สเตตัส (Edit Substats)
</button>
<!-- Set Description -->
<div class="eq-inspector-ability">
<div class="eq-inspector-ability" style="margin-top: 0.4rem;">
<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>2-Piece: ${rInfo.desc2 || 'เพิ่มประสิทธิภาพตามเซ็ตที่กำหนด'}<br>4-Piece: ${rInfo.desc4 || 'ปลดล็อกเอฟเฟกต์ต่อสู้ขั้นสูง'}</div>
</div>
<!-- Wearer Form & Actions -->
@@ -5598,6 +5999,10 @@ function showRelicInspector(relic) {
</div>
`;
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'));