fix: enable client-verified planar relic sets

This commit is contained in:
CAtlas0784 committed 2026-11-02 03:53:48 +07:00
1 parent 22e07ea5f2
commit 303332469a
6 files changed
+838 -60

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
{
"source": {
"kind": "client table capture",
"client_version": "4.6.54",
"game_assembly_sha256": "4aa08a0706f27d2cf9951b61920c179140ece9ea94f7e34422c18e46922939b6",
"captured_at": "2026-11-02",
"tables": [
"RelicConfigExcelTable",
"RelicMainAffixConfigExcelTable",
"RelicSubAffixConfigExcelTable"
],
"scope": "Four relic IDs and main affix groups 55/56 and sub-affix group 5."
},
"relics": [
{
"Rarity": 5,
"Mode": 1,
"ID": 63295,
"MainAffixGroup": 55,
"MaxLevel": 15,
"SubAffixGroup": 5,
"SetID": 329,
"ExpProvide": 1500,
"ExpType": 4,
"Type": 5,
"CoinCost": 2250
},
{
"Rarity": 5,
"Mode": 1,
"ID": 63296,
"MainAffixGroup": 56,
"MaxLevel": 15,
"SubAffixGroup": 5,
"SetID": 329,
"ExpProvide": 1500,
"ExpType": 4,
"Type": 6,
"CoinCost": 2250
},
{
"Rarity": 5,
"Mode": 1,
"ID": 63305,
"MainAffixGroup": 55,
"MaxLevel": 15,
"SubAffixGroup": 5,
"SetID": 330,
"ExpProvide": 1500,
"ExpType": 4,
"Type": 5,
"CoinCost": 2250
},
{
"Rarity": 5,
"Mode": 1,
"ID": 63306,
"MainAffixGroup": 56,
"MaxLevel": 15,
"SubAffixGroup": 5,
"SetID": 330,
"ExpProvide": 1500,
"ExpType": 4,
"Type": 6,
"CoinCost": 2250
}
],
"main_affixes": [
{
"GroupID": 55,
"AffixID": 1,
"Property": 32,
"LevelAdd": "0.0241920005",
"BaseValue": "0.0691199996"
},
{
"GroupID": 55,
"AffixID": 2,
"Property": 33,
"LevelAdd": "0.0241920005",
"BaseValue": "0.0691199996"
},
{
"GroupID": 55,
"AffixID": 3,
"Property": 34,
"LevelAdd": "0.0302400004",
"BaseValue": "0.0863999999"
},
{
"GroupID": 55,
"AffixID": 4,
"Property": 12,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 55,
"AffixID": 5,
"Property": 14,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 55,
"AffixID": 6,
"Property": 16,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 55,
"AffixID": 7,
"Property": 18,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 55,
"AffixID": 8,
"Property": 20,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 55,
"AffixID": 9,
"Property": 22,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 55,
"AffixID": 10,
"Property": 24,
"LevelAdd": "0.0217730009",
"BaseValue": "0.0622080001"
},
{
"GroupID": 56,
"AffixID": 1,
"Property": 59,
"LevelAdd": "0.0362880004",
"BaseValue": "0.1036800008"
},
{
"GroupID": 56,
"AffixID": 2,
"Property": 54,
"LevelAdd": "0.0108860011",
"BaseValue": "0.0311040000"
},
{
"GroupID": 56,
"AffixID": 3,
"Property": 32,
"LevelAdd": "0.0241920005",
"BaseValue": "0.0691199996"
},
{
"GroupID": 56,
"AffixID": 4,
"Property": 33,
"LevelAdd": "0.0241920005",
"BaseValue": "0.0691199996"
},
{
"GroupID": 56,
"AffixID": 5,
"Property": 34,
"LevelAdd": "0.0302400004",
"BaseValue": "0.0863999999"
}
],
"sub_affixes": [
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "33.870039001",
"StepValue": "4.2337550008",
"Property": 27,
"AffixID": 1
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "16.935019000",
"StepValue": "2.1168770007",
"Property": 29,
"AffixID": 2
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "16.935019000",
"StepValue": "2.1168770007",
"Property": 31,
"AffixID": 3
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0345600005",
"StepValue": "0.0043200000",
"Property": 32,
"AffixID": 4
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0345600005",
"StepValue": "0.0043200000",
"Property": 33,
"AffixID": 5
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0431999999",
"StepValue": "0.0053999999",
"Property": 34,
"AffixID": 6
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "2",
"StepValue": "0.3000000002",
"Property": 51,
"AffixID": 7
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0259200003",
"StepValue": "0.0032400002",
"Property": 52,
"AffixID": 8
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0518400007",
"StepValue": "0.0064800004",
"Property": 53,
"AffixID": 9
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0345600005",
"StepValue": "0.0043200000",
"Property": 56,
"AffixID": 10
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0345600005",
"StepValue": "0.0043200000",
"Property": 57,
"AffixID": 11
},
{
"StepNum": 2,
"GroupID": 5,
"BaseValue": "0.0518400007",
"StepValue": "0.0064800004",
"Property": 59,
"AffixID": 12
}
]
}
+358
View File
@@ -1553,6 +1553,340 @@ input:checked + .slider:before {
transform: translateY(-1px); transform: translateY(-1px);
} }
/* ==========================================================================
Equip Target Chip + Avatar Assignment Picker
========================================================================== */
.equip-target {
width: 100%;
display: flex;
align-items: center;
gap: 0.65rem;
text-align: left;
padding: 0.5rem 0.65rem;
min-height: 52px;
background: rgba(10, 13, 20, 0.7);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-main);
cursor: pointer;
font: inherit;
transition: var(--transition);
}
.equip-target:hover {
border-color: var(--accent-cyan);
background: rgba(56, 189, 248, 0.07);
}
.equip-target:focus-visible {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
}
.equip-target.is-empty {
border-style: dashed;
}
.equip-target-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
border: 2px solid var(--border-color);
font-size: 1.05rem;
line-height: 1;
}
.equip-target.is-empty .equip-target-avatar {
border-style: dashed;
border-color: rgba(56, 189, 248, 0.45);
}
.equip-target-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.equip-target-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.equip-target-name {
font-size: 0.875rem;
font-weight: 700;
color: var(--text-highlight);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.25;
}
.equip-target-sub {
font-size: 0.7rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.3rem;
min-width: 0;
}
.equip-target-cta {
flex-shrink: 0;
font-size: 0.7rem;
font-weight: 800;
color: var(--accent-cyan);
border: 1px solid rgba(56, 189, 248, 0.4);
border-radius: 999px;
padding: 0.2rem 0.6rem;
background: rgba(56, 189, 248, 0.08);
white-space: nowrap;
}
.assign-picker-modal {
width: min(980px, 96vw) !important;
max-width: 980px !important;
max-height: 88vh;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.assign-picker-header {
align-items: flex-start;
margin-bottom: 0;
}
.assign-picker-heading {
min-width: 0;
}
.assign-picker-subtitle {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.2rem;
overflow-wrap: anywhere;
}
.assign-picker-subtitle strong {
color: var(--accent-gold);
font-weight: 700;
}
.assign-picker-search {
flex-shrink: 0;
}
.assign-picker-grid {
flex: 1;
min-height: 200px;
max-height: 52vh;
overflow-y: auto;
overscroll-behavior: contain;
padding-right: 0.45rem;
scrollbar-width: thin;
}
.assign-inventory-card {
grid-column: 1 / -1;
display: flex !important;
align-items: center !important;
gap: 0.75rem !important;
padding: 0.7rem 0.9rem !important;
min-height: 56px !important;
background: rgba(52, 211, 153, 0.06);
border: 1px dashed rgba(52, 211, 153, 0.4);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
position: relative;
}
.assign-inventory-card:hover {
background: rgba(52, 211, 153, 0.12);
border-color: var(--accent-green);
transform: translateY(-1px);
}
.assign-inventory-card.is-selected {
border-style: solid;
border-color: var(--accent-cyan);
background: linear-gradient(135deg, rgba(56, 189, 248, 0.16), rgba(18, 24, 38, 0.95));
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.35), 0 8px 22px rgba(56, 189, 248, 0.18);
}
.assign-inventory-icon {
width: 38px;
height: 38px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(52, 211, 153, 0.12);
border: 1px solid rgba(52, 211, 153, 0.35);
font-size: 1.05rem;
}
.assign-inventory-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.assign-inventory-text strong {
font-size: 0.9rem;
color: var(--text-highlight);
}
.assign-inventory-text small {
font-size: 0.7rem;
color: var(--text-muted);
}
.assign-check {
flex-shrink: 0;
width: 22px;
height: 22px;
border-radius: 50%;
border: 1.5px solid var(--border-color);
color: transparent;
font-size: 0.75rem;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.is-selected > .assign-check,
.assign-avatar-card.is-selected::after {
color: #04121d;
}
.is-selected > .assign-check {
background: var(--accent-cyan);
border-color: var(--accent-cyan);
}
.assign-avatar-card.is-selected {
border-color: var(--accent-cyan) !important;
background: linear-gradient(135deg, rgba(56, 189, 248, 0.16), rgba(18, 24, 38, 0.95)) !important;
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.35), 0 8px 22px rgba(56, 189, 248, 0.18) !important;
}
.assign-avatar-card.is-selected::after {
content: '✓';
position: absolute;
top: 0.35rem;
right: 0.45rem;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--accent-cyan);
font-size: 0.7rem;
font-weight: 900;
}
.assign-equipped-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-top: 0.3rem;
font-size: 0.68rem;
font-weight: 700;
color: var(--accent-gold);
line-height: 1.3;
overflow-wrap: anywhere;
}
.assign-picker-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
border-top: 1px solid var(--border-color);
padding-top: 0.7rem;
flex-shrink: 0;
}
.assign-picker-current {
display: flex;
align-items: center;
gap: 0.35rem;
min-width: 0;
font-size: 0.8rem;
}
.assign-current-label {
color: var(--text-muted);
}
.assign-current-value {
font-weight: 800;
color: var(--accent-cyan);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 42vw;
}
.assign-picker-actions {
display: flex;
gap: 0.5rem;
margin-left: auto;
}
@media (max-width: 640px) {
.assign-picker-modal {
padding: 1.1rem;
gap: 0.6rem;
}
.assign-picker-grid {
max-height: 48vh;
}
.assign-picker-footer {
align-items: stretch;
}
.assign-picker-actions {
width: 100%;
}
.assign-picker-actions .btn {
flex: 1;
}
.assign-current-value {
max-width: 55vw;
}
.equip-target-cta {
display: none;
}
}
/* ========================================================================== /* ==========================================================================
Modal Avatar Detail Widescreen Expansion Modal Avatar Detail Widescreen Expansion
========================================================================== */ ========================================================================== */
@@ -3847,3 +4181,27 @@ input:checked + .slider:before {
#add-lc-preview-thumb { width: 80px !important; height: 112px !important; } #add-lc-preview-thumb { width: 80px !important; height: 112px !important; }
.lc-picker-thumb, .add-lc-card-thumb { width: 76px; height: 106px; } .lc-picker-thumb, .add-lc-card-thumb { width: 76px; height: 106px; }
} }
/* Relic creation: searchable artwork cards and a compact settings panel. */
.add-relic-modal { width: min(1050px, 96vw); max-width: 1050px; max-height: 92vh; overflow-y: auto; }
.add-relic-search { margin: .35rem 0 .65rem; }
.add-relic-set-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: .55rem; max-height: 300px; overflow-y: auto; padding: .35rem; background: rgba(4, 10, 23, .45); border: 1px solid var(--border-color); border-radius: 12px; }
.add-relic-set-card { min-width: 0; min-height: 132px; border: 1px solid var(--border-color); border-radius: 10px; background: #111a2b; color: var(--text-color); padding: .55rem; display: flex; flex-direction: column; align-items: center; gap: .3rem; text-align: center; cursor: pointer; }
.add-relic-set-card:hover, .add-relic-set-card:focus-visible { border-color: var(--accent-cyan); background: #17263b; }
.add-relic-set-card.selected { border-color: var(--accent-cyan); box-shadow: inset 0 0 0 1px var(--accent-cyan); background: #142a3d; }
.add-relic-set-card:disabled { opacity: .45; cursor: not-allowed; }
.add-relic-set-card img { width: 62px; height: 62px; object-fit: contain; }
.add-relic-set-card span { font-size: .77rem; line-height: 1.2; font-weight: 700; overflow-wrap: anywhere; }
.add-relic-set-card small { color: var(--text-muted); font-size: .65rem; margin-top: auto; }
.sr-only-select { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.substat-menu { position: relative; min-width: 0; flex: 1; }
.substat-choice { display: flex; align-items: center; gap: .3rem; min-width: 0; min-height: 36px; padding: .35rem .5rem; border: 1px solid var(--border-color); border-radius: 6px; background: #0a0e17; color: #fff; font-size: .8rem; font-weight: 600; cursor: pointer; list-style: none; }
.substat-choice::-webkit-details-marker { display: none; }
.substat-choice::after { content: '▾'; margin-left: auto; color: var(--text-muted); }
.substat-choice span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.substat-menu[open] { z-index: 5; }
.substat-options { position: absolute; left: 0; bottom: calc(100% + 4px); width: max(100%, 215px); max-height: 250px; overflow-y: auto; padding: 4px; background: #101a2c; border: 1px solid #39506e; border-radius: 8px; box-shadow: 0 12px 28px #000a; }
.substat-option { width: 100%; display: flex; align-items: center; gap: .4rem; text-align: left; padding: .5rem; border: 0; border-radius: 5px; background: transparent; color: #eef4ff; cursor: pointer; font-size: .8rem; }
.substat-option:hover:not(:disabled), .substat-option:focus-visible { background: #263d59; }
.substat-option:disabled { opacity: .4; cursor: not-allowed; }
@media (max-width: 600px) { .add-relic-set-grid { grid-template-columns: repeat(auto-fill, minmax(105px, 1fr)); max-height: 230px; } }
+48 -13
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GaCy-Srtool</title> <title>GaCy-Srtool</title>
<link rel="stylesheet" href="css/styles.css?v=2.23"> <link rel="stylesheet" href="css/styles.css?v=2.24">
<link rel="stylesheet" href="css/item-catalog.css?v=2"> <link rel="stylesheet" href="css/item-catalog.css?v=2">
<link rel="icon" type="image/png" href="assets/logo.png"> <link rel="icon" type="image/png" href="assets/logo.png">
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
@@ -1552,10 +1552,12 @@
</div> </div>
<div class="form-group" style="margin-bottom: 0;"> <div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="lblEquipAvatar" style="font-size: 0.75rem;">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label> <label class="form-label" data-i18n="lblEquipAvatar" style="font-size: 0.75rem;">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label>
<select id="add-lc-avatar" class="form-select"> <select id="add-lc-avatar" class="sr-only-select" tabindex="-1" aria-hidden="true">
<option value="0" data-i18n="unassigned">-- ยังไม่สวมใส่ (เก็บในคลัง) --</option> <option value="0" selected>0</option>
<!-- Injected by JS -->
</select> </select>
<button type="button" class="equip-target is-empty" id="add-lc-avatar-btn" aria-haspopup="dialog">
<!-- Rendered by JS -->
</button>
</div> </div>
</div> </div>
@@ -1570,7 +1572,7 @@
<!-- Add Relic Modal --> <!-- Add Relic Modal -->
<div class="modal-overlay" id="modal-add-relic"> <div class="modal-overlay" id="modal-add-relic">
<div class="modal-content" style="max-width: 580px;"> <div class="modal-content add-relic-modal">
<div class="modal-header"> <div class="modal-header">
<h3 style="font-weight: 800;" data-i18n="modalAddRelicTitle">🛡️ เพิ่มรีลิกส์ลง FreeSR</h3> <h3 style="font-weight: 800;" data-i18n="modalAddRelicTitle">🛡️ เพิ่มรีลิกส์ลง FreeSR</h3>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-add-relic-modal">✕</button> <button type="button" class="btn btn-sm btn-secondary" id="btn-close-add-relic-modal">✕</button>
@@ -1578,9 +1580,9 @@
<form id="form-add-relic"> <form id="form-add-relic">
<div class="form-group"> <div class="form-group">
<label class="form-label" data-i18n="lblSelectRelicSet">เลือกเซ็ตรีลิกส์</label> <label class="form-label" data-i18n="lblSelectRelicSet">เลือกเซ็ตรีลิกส์</label>
<select id="add-relic-set" class="form-select" required> <input id="add-relic-search" class="form-input add-relic-search" type="search" placeholder="Search / ค้นหา" aria-label="Search relic sets">
<!-- Injected by JS --> <div class="add-relic-set-grid" id="add-relic-set-grid" role="group" aria-label="Relic sets"></div>
</select> <select id="add-relic-set" class="sr-only-select" tabindex="-1" aria-hidden="true" required></select>
</div> </div>
<div class="grid-2" style="gap: 0.75rem; margin-bottom: 1rem;"> <div class="grid-2" style="gap: 0.75rem; margin-bottom: 1rem;">
<div class="form-group" style="margin-bottom: 0;"> <div class="form-group" style="margin-bottom: 0;">
@@ -1607,10 +1609,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" data-i18n="lblEquipAvatar">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label> <label class="form-label" data-i18n="lblEquipAvatar">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label>
<select id="add-relic-avatar" class="form-select"> <select id="add-relic-avatar" class="sr-only-select" tabindex="-1" aria-hidden="true">
<option value="0" data-i18n="unassigned">-- ยังไม่สวมใส่ (เก็บในคลัง) --</option> <option value="0" selected>0</option>
<!-- Injected by JS -->
</select> </select>
<button type="button" class="equip-target is-empty" id="add-relic-avatar-btn" aria-haspopup="dialog">
<!-- Rendered by JS -->
</button>
</div> </div>
<div style="display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.25rem;"> <div style="display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.25rem;">
<button type="button" class="btn btn-secondary" id="btn-cancel-add-relic" data-i18n="btnCancel">ยกเลิก</button> <button type="button" class="btn btn-secondary" id="btn-cancel-add-relic" data-i18n="btnCancel">ยกเลิก</button>
@@ -1620,12 +1624,43 @@
</div> </div>
</div> </div>
<!-- Avatar Assignment Picker (Add Relic / Add Light Cone) -->
<div class="modal-overlay" id="modal-avatar-assign">
<div class="modal-content assign-picker-modal" role="dialog" aria-modal="true" aria-labelledby="modal-assign-title">
<div class="modal-header assign-picker-header">
<div class="assign-picker-heading">
<h3 id="modal-assign-title" style="font-weight: 800; margin: 0; font-size: 1.15rem;">สวมใส่ให้ตัวละคร</h3>
<div id="modal-assign-subtitle" class="assign-picker-subtitle"></div>
</div>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-assign-picker">✕</button>
</div>
<input type="search" id="assign-picker-search" class="form-input assign-picker-search"
data-i18n-placeholder="assignPickerSearch" placeholder="🔍 ค้นหาชื่อหรือ ID ตัวละคร..." autocomplete="off">
<div id="assign-picker-grid" class="picker-grid-container assign-picker-grid">
<!-- Injected by JS -->
</div>
<div class="assign-picker-footer">
<div class="assign-picker-current">
<span class="assign-current-label" data-i18n="assignCurrent">เลือกอยู่:</span>
<span id="assign-picker-current-value" class="assign-current-value">—</span>
</div>
<div class="assign-picker-actions">
<button type="button" class="btn btn-secondary" id="btn-cancel-assign-picker" data-i18n="btnCancel">ยกเลิก</button>
<button type="button" class="btn btn-primary" id="btn-confirm-assign-picker" data-i18n="assignConfirm">ตกลง</button>
</div>
</div>
</div>
</div>
<!-- Modal: Select Relic for Specific Slot (Large, Spacious & Easy to Use) --> <!-- Modal: Select Relic for Specific Slot (Large, Spacious & Easy to Use) -->
<div class="modal-overlay" id="modal-select-relic-slot"> <div class="modal-overlay" id="modal-select-relic-slot">
<div class="modal-content relic-picker-modal-content"> <div class="modal-content relic-picker-modal-content">
<div class="modal-header" style="margin-bottom: 0.75rem;"> <div class="modal-header" style="margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.65rem;"> <div style="display: flex; align-items: center; gap: 0.65rem;">
<img id="modal-slot-picker-icon" src="" alt="Slot" data-i18n-alt="slot" style="width: 28px; height: 28px; object-fit: contain;">
<div> <div>
<h3 id="modal-slot-picker-title" style="font-weight: 800; font-size: 1.15rem; margin: 0;" data-i18n="slotPickerTitle"> <h3 id="modal-slot-picker-title" style="font-weight: 800; font-size: 1.15rem; margin: 0;" data-i18n="slotPickerTitle">
🛡️ เลือกสวมใส่รีลิกส์ 🛡️ เลือกสวมใส่รีลิกส์
@@ -1823,6 +1858,6 @@
<div class="toast-container" id="toast-container"></div> <div class="toast-container" id="toast-container"></div>
<!-- Script Modules --> <!-- Script Modules -->
<script type="module" src="js/app.js?v=3.15"></script> <script type="module" src="js/app.js?v=3.16"></script>
</body> </body>
</html> </html>
+143 -43
View File
@@ -173,6 +173,7 @@ const el = {
addLcPromotion: document.getElementById('add-lc-promotion'), addLcPromotion: document.getElementById('add-lc-promotion'),
addLcRank: document.getElementById('add-lc-rank'), addLcRank: document.getElementById('add-lc-rank'),
addLcAvatar: document.getElementById('add-lc-avatar'), addLcAvatar: document.getElementById('add-lc-avatar'),
addLcAvatarBtn: document.getElementById('add-lc-avatar-btn'),
btnOpenAddRelic: document.getElementById('btn-open-add-relic'), btnOpenAddRelic: document.getElementById('btn-open-add-relic'),
modalAddRelic: document.getElementById('modal-add-relic'), modalAddRelic: document.getElementById('modal-add-relic'),
btnCloseAddRelicModal: document.getElementById('btn-close-add-relic-modal'), btnCloseAddRelicModal: document.getElementById('btn-close-add-relic-modal'),
@@ -183,6 +184,7 @@ const el = {
addRelicLevel: document.getElementById('add-relic-level'), addRelicLevel: document.getElementById('add-relic-level'),
addRelicMainAffix: document.getElementById('add-relic-main-affix'), addRelicMainAffix: document.getElementById('add-relic-main-affix'),
addRelicAvatar: document.getElementById('add-relic-avatar'), addRelicAvatar: document.getElementById('add-relic-avatar'),
addRelicAvatarBtn: document.getElementById('add-relic-avatar-btn'),
// Persistent Tab // Persistent Tab
tabBtnPersistent: document.getElementById('tab-btn-persistent'), tabBtnPersistent: document.getElementById('tab-btn-persistent'),
@@ -369,6 +371,17 @@ const el = {
pickerSearch: document.getElementById('picker-search'), pickerSearch: document.getElementById('picker-search'),
pickerAvatarsGrid: document.getElementById('picker-avatars-grid'), pickerAvatarsGrid: document.getElementById('picker-avatars-grid'),
// Avatar Assignment Picker (Add Relic / Add Light Cone)
modalAvatarAssign: document.getElementById('modal-avatar-assign'),
modalAssignTitle: document.getElementById('modal-assign-title'),
modalAssignSubtitle: document.getElementById('modal-assign-subtitle'),
assignPickerSearch: document.getElementById('assign-picker-search'),
assignPickerGrid: document.getElementById('assign-picker-grid'),
assignPickerCurrentValue: document.getElementById('assign-picker-current-value'),
btnCloseAssignPicker: document.getElementById('btn-close-assign-picker'),
btnCancelAssignPicker: document.getElementById('btn-cancel-assign-picker'),
btnConfirmAssignPicker: document.getElementById('btn-confirm-assign-picker'),
toastContainer: document.getElementById('toast-container') toastContainer: document.getElementById('toast-container')
}; };
@@ -551,6 +564,44 @@ function getElementIconHtml(element, size = 20) {
</span>`; </span>`;
} }
// A set without captured affix groups remains browse-only.
let relicClientCapture = null;
const relicCaptureReady = fetch('./assets/relic-client-capture.json')
.then(response => response.ok ? response.json() : null)
.then(data => { relicClientCapture = data; })
.catch(error => console.warn('Client relic capture unavailable', error));
function canCreateRelicSet(setId) {
const meta = RELIC_SET_METADATA[setId];
if (meta?.clientCaptureRequired) {
const rows = relicClientCapture?.relics?.filter(row => Number(row.SetID) === Number(setId)) || [];
return [5, 6].every(slot => rows.some(row => Number(row.Type) === slot &&
relicClientCapture.main_affixes?.some(affix => affix.GroupID === row.MainAffixGroup) &&
relicClientCapture.sub_affixes?.some(affix => affix.GroupID === row.SubAffixGroup)));
}
return meta?.creationAvailable !== false;
}
function isCapturedMainAffixValid(setId, slot, mainAffixId) {
if (!RELIC_SET_METADATA[setId]?.clientCaptureRequired) return true;
const relic = relicClientCapture?.relics?.find(row => Number(row.SetID) === Number(setId) && Number(row.Type) === Number(slot));
return !!relic && relicClientCapture.main_affixes.some(row => row.GroupID === relic.MainAffixGroup && row.AffixID === Number(mainAffixId));
}
function areCapturedSubAffixesValid(setId, slot, mainAffixId, subAffixes) {
if (!RELIC_SET_METADATA[setId]?.clientCaptureRequired) return true;
const relic = relicClientCapture?.relics?.find(row => Number(row.SetID) === Number(setId) && Number(row.Type) === Number(slot));
if (!relic) return false;
const main = relicClientCapture.main_affixes.find(row => row.GroupID === relic.MainAffixGroup && row.AffixID === Number(mainAffixId));
if (!main) return false;
const ids = new Set();
return subAffixes.every(sub => {
const id = Number(sub.sub_affix_id);
const row = relicClientCapture.sub_affixes.find(entry => entry.GroupID === relic.SubAffixGroup && entry.AffixID === id);
if (!row || row.Property === main.Property || ids.has(id)) return false;
ids.add(id);
return true;
});
}
// Official in-game property icons (same set used by the overview tab) // Official in-game property icons (same set used by the overview tab)
function getRelicImage(setId, slot = 5) { function getRelicImage(setId, slot = 5) {
return RELIC_SET_METADATA[setId]?.images?.[slot] || RELIC_SET_METADATA[setId]?.images?.[5] || `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png`; return RELIC_SET_METADATA[setId]?.images?.[slot] || RELIC_SET_METADATA[setId]?.images?.[5] || `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png`;
@@ -3768,7 +3819,8 @@ export function openSlotPickerForRelic(relic) {
} }
window.openSlotPickerForRelic = openSlotPickerForRelic; window.openSlotPickerForRelic = openSlotPickerForRelic;
function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) { async function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) {
await relicCaptureReady;
state.slotPickerAvatarId = parseInt(avatarId, 10); state.slotPickerAvatarId = parseInt(avatarId, 10);
state.slotPickerSlot = parseInt(slotNum, 10); state.slotPickerSlot = parseInt(slotNum, 10);
slotPickerTargetRelic = targetRelicToEdit || null; slotPickerTargetRelic = targetRelicToEdit || null;
@@ -4008,7 +4060,7 @@ function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) {
// Confirm Custom Button // Confirm Custom Button
if (btnConfirmCustom) { if (btnConfirmCustom) {
btnConfirmCustom.onclick = async () => { btnConfirmCustom.onclick = async () => {
if (RELIC_SET_METADATA[slotPickerSelectedSetId]?.creationAvailable === false) { if (!canCreateRelicSet(slotPickerSelectedSetId) || !isCapturedMainAffixValid(slotPickerSelectedSetId, slot, slotPickerSelectedMainAffix) || !areCapturedSubAffixesValid(slotPickerSelectedSetId, slot, slotPickerSelectedMainAffix, slotPickerSubstats)) {
showToast('Requires matching server relic and main affix data.', 'warning'); showToast('Requires matching server relic and main affix data.', 'warning');
return; return;
} }
@@ -4047,40 +4099,34 @@ function renderSlotPickerSubstats(slot, mainAffixId) {
const sVal = formatRelicSubstatValue(sub.sub_affix_id, sub.count); const sVal = formatRelicSubstatValue(sub.sub_affix_id, sub.count);
let optsHtml = ''; const selectedStat = RELIC_SUBSTATS_CONFIG.find(stat => stat.id === sub.sub_affix_id);
RELIC_SUBSTATS_CONFIG.forEach(stat => { const options = RELIC_SUBSTATS_CONFIG.map(stat => {
const isConflict = stat.id === conflictSub; const conflict = stat.id === conflictSub;
const isChosenElsewhere = slotPickerSubstats.some((s, i) => i !== idx && s.sub_affix_id === stat.id); const used = slotPickerSubstats.some((other, i) => i !== idx && other.sub_affix_id === stat.id);
const isSelected = stat.id === sub.sub_affix_id; const reason = conflict ? t('substatMainConflict') : (used ? t('substatAlreadyChosen') : '');
const disabledAttr = (isConflict || (isChosenElsewhere && !isSelected)) ? 'disabled' : ''; return `<button type="button" class="substat-option" data-stat-id="${stat.id}" ${conflict || used ? 'disabled' : ''}>
const conflictTag = isConflict ? t('substatMainConflict') : (isChosenElsewhere && !isSelected ? t('substatAlreadyChosen') : ''); ${getStatIconHtml(stat.icon, 18, '')}<span>${stat.name}${reason}</span></button>`;
optsHtml += `<option value="${stat.id}" ${isSelected ? 'selected' : ''} ${disabledAttr}>${stat.name}${conflictTag}</option>`; }).join('');
});
card.innerHTML = ` card.innerHTML = `
<div class="substat-row-header"> <div class="substat-row-header">
<span class="substat-slot-label">${t('substatSlotLabel', { n: idx + 1 })}</span> <span class="substat-slot-label">${t('substatSlotLabel', { n: idx + 1 })}</span>
<span class="substat-preview-badge" id="substat-preview-${idx}">${t('substatRolls', { value: sVal, count: sub.count })}</span> <span class="substat-preview-badge" id="substat-preview-${idx}">${t('substatRolls', { value: sVal, count: sub.count })}</span>
</div> </div>
<div class="substat-row-controls"> <div class="substat-row-controls">
${getStatIconHtml(RELIC_SUBSTATS_CONFIG.find(stat => stat.id === sub.sub_affix_id)?.icon, 20, "")} <details class="substat-menu">
<select class="form-select substat-select" data-idx="${idx}"> <summary class="substat-choice">${getStatIconHtml(selectedStat?.icon, 18, '')}<span>${selectedStat?.name || ''}</span></summary>
${optsHtml} <div class="substat-options">${options}</div>
</select> </details>
<div class="substat-stepper"> <div class="substat-stepper">
<button type="button" class="btn-substat-step minus" data-idx="${idx}" title="${t('substatDecreaseRoll')}">−</button> <button type="button" class="btn-substat-step minus" data-idx="${idx}" title="${t('substatDecreaseRoll')}">−</button>
<input type="number" class="substat-count-input" data-idx="${idx}" min="1" max="25" value="${sub.count}"> <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="${t('substatIncreaseRoll')}">+</button> <button type="button" class="btn-substat-step plus" data-idx="${idx}" title="${t('substatIncreaseRoll')}">+</button>
</div> </div>
</div> </div>`;
`; card.querySelectorAll('.substat-option').forEach(button => button.addEventListener('click', () => {
slotPickerSubstats[idx].sub_affix_id = Number(button.dataset.statId);
// 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); renderSlotPickerSubstats(slot, slotPickerSelectedMainAffix);
}); }));
// Stepper buttons // Stepper buttons
card.querySelector('.btn-substat-step.minus')?.addEventListener('click', () => { card.querySelector('.btn-substat-step.minus')?.addEventListener('click', () => {
@@ -4234,7 +4280,7 @@ function renderSlotPickerSetGrid(slot, recSetId) {
${sId === recSetId ? `<span style="font-size: 0.65rem; color: var(--accent-gold); font-weight: 800;">⭐ ${t('recommendedBadge')}</span>` : ''} ${sId === recSetId ? `<span style="font-size: 0.65rem; color: var(--accent-gold); font-weight: 800;">⭐ ${t('recommendedBadge')}</span>` : ''}
`; `;
if (RELIC_SET_METADATA[sId].creationAvailable === false) { if (!canCreateRelicSet(sId)) {
card.style.opacity = '0.5'; card.style.opacity = '0.5';
card.title = 'Requires matching server relic and main affix data'; card.title = 'Requires matching server relic and main affix data';
card.setAttribute('aria-disabled', 'true'); card.setAttribute('aria-disabled', 'true');
@@ -6867,8 +6913,51 @@ function renderAddLcVisualGrid() {
}); });
} }
function renderAddRelicSetGrid() {
const grid = document.getElementById('add-relic-set-grid');
const query = document.getElementById('add-relic-search')?.value.trim().toLowerCase() || '';
if (!grid) return;
grid.replaceChildren();
for (const option of el.addRelicSet.options) {
const id = Number(option.value);
const info = getRelicSetInfo(id);
if (query && !`${info.name} ${id}`.toLowerCase().includes(query)) continue;
const button = document.createElement('button');
button.type = 'button';
button.className = 'add-relic-set-card' + (el.addRelicSet.value === option.value ? ' selected' : '');
button.disabled = option.disabled;
button.setAttribute('aria-pressed', String(el.addRelicSet.value === option.value));
button.title = option.disabled ? 'Requires matching server relic and main affix data' : info.name;
const image = document.createElement('img');
image.src = getRelicImage(id, id >= 300 ? 5 : 1);
image.alt = '';
image.onerror = () => { image.onerror = null; image.src = './assets/logo.png'; };
const label = document.createElement('span');
label.textContent = info.name;
const tag = document.createElement('small');
tag.textContent = option.disabled ? 'Requires server data' : `#${id}`;
button.append(image, label, tag);
button.addEventListener('click', () => {
el.addRelicSet.value = option.value;
syncAddRelicSlot();
renderAddRelicSetGrid();
});
grid.appendChild(button);
}
}
function syncAddRelicSlot() {
const isPlanar = Number(el.addRelicSet.value) >= 300;
for (const option of el.addRelicSlot.options) {
option.disabled = isPlanar ? Number(option.value) < 5 : Number(option.value) >= 5;
}
if (el.addRelicSlot.selectedOptions[0]?.disabled) el.addRelicSlot.value = isPlanar ? '5' : '1';
updateRelicMainAffixes(el.addRelicSlot.value);
}
// Open Add Relic Modal // Open Add Relic Modal
function openAddRelicModal() { async function openAddRelicModal() {
await relicCaptureReady;
if (!el.modalAddRelic) return; if (!el.modalAddRelic) return;
el.addRelicSet.innerHTML = ''; el.addRelicSet.innerHTML = '';
const setIds = Object.keys(RELIC_SET_METADATA).sort((a, b) => parseInt(b, 10) - parseInt(a, 10)); const setIds = Object.keys(RELIC_SET_METADATA).sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
@@ -6876,7 +6965,7 @@ function openAddRelicModal() {
const meta = getRelicSetInfo(setId); const meta = getRelicSetInfo(setId);
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = setId; opt.value = setId;
opt.disabled = RELIC_SET_METADATA[setId].creationAvailable === false; opt.disabled = !canCreateRelicSet(setId);
opt.textContent = `${meta.name} (Set ID: ${setId})${opt.disabled ? ' — requires server data' : ''}`; opt.textContent = `${meta.name} (Set ID: ${setId})${opt.disabled ? ' — requires server data' : ''}`;
el.addRelicSet.appendChild(opt); el.addRelicSet.appendChild(opt);
}); });
@@ -6891,9 +6980,14 @@ function openAddRelicModal() {
el.addRelicAvatar.appendChild(opt); el.addRelicAvatar.appendChild(opt);
}); });
const firstAvailable = Array.from(el.addRelicSet.options).find(option => !option.disabled);
if (firstAvailable) el.addRelicSet.value = firstAvailable.value;
el.addRelicSlot.value = "1"; el.addRelicSlot.value = "1";
el.addRelicLevel.value = 15; el.addRelicLevel.value = 15;
updateRelicMainAffixes("1"); const search = document.getElementById('add-relic-search');
if (search) search.value = '';
syncAddRelicSlot();
renderAddRelicSetGrid();
el.modalAddRelic.classList.add('active'); el.modalAddRelic.classList.add('active');
} }
@@ -6988,6 +7082,10 @@ async function handleAddRelicSubmit(e) {
const mainAffixId = parseInt(el.addRelicMainAffix.value, 10) || 1; const mainAffixId = parseInt(el.addRelicMainAffix.value, 10) || 1;
const equipAvatar = parseInt(el.addRelicAvatar.value, 10) || 0; const equipAvatar = parseInt(el.addRelicAvatar.value, 10) || 0;
if (!canCreateRelicSet(setId) || !isCapturedMainAffixValid(setId, slot, mainAffixId)) {
showToast('Relic main affix data is unavailable for this set.', 'warning');
return;
}
const relicId = 60000 + (setId * 10) + slot; const relicId = 60000 + (setId * 10) + slot;
if (!state.fullFreesrData) { if (!state.fullFreesrData) {
@@ -6997,6 +7095,22 @@ async function handleAddRelicSubmit(e) {
state.fullFreesrData.relics = []; state.fullFreesrData.relics = [];
} }
const maxUid = state.fullFreesrData.relics.reduce((m, r) => Math.max(m, r.internal_uid || 0), 0);
const newRelic = {
level,
relic_id: relicId,
main_affix_id: mainAffixId,
// Exclude the main property's sub-affix from the same slot group.
sub_affixes: RELIC_SUBSTATS_CONFIG.filter(stat => stat.id !== getConflictingSubstat(slot, mainAffixId))
.slice(0, 4).map(stat => ({ sub_affix_id: stat.id, cnt: 1, count: 1, step: 0 })),
internal_uid: maxUid + 1,
equip_avatar: equipAvatar
};
if (!areCapturedSubAffixesValid(setId, slot, mainAffixId, newRelic.sub_affixes)) {
showToast('Relic sub-affix data is invalid for this set.', 'warning');
return;
}
if (equipAvatar !== 0) { if (equipAvatar !== 0) {
state.fullFreesrData.relics.forEach(r => { state.fullFreesrData.relics.forEach(r => {
if (r.equip_avatar === equipAvatar && (r.relic_id % 10 === slot)) { if (r.equip_avatar === equipAvatar && (r.relic_id % 10 === slot)) {
@@ -7005,21 +7119,6 @@ async function handleAddRelicSubmit(e) {
}); });
} }
const maxUid = state.fullFreesrData.relics.reduce((m, r) => Math.max(m, r.internal_uid || 0), 0);
const newRelic = {
level,
relic_id: relicId,
main_affix_id: mainAffixId,
sub_affixes: [
{ sub_affix_id: 1, cnt: 2, count: 2, step: 1 },
{ sub_affix_id: 2, cnt: 2, count: 2, step: 1 },
{ sub_affix_id: 3, cnt: 3, count: 3, step: 2 },
{ sub_affix_id: 4, cnt: 4, count: 4, step: 2 }
],
internal_uid: maxUid + 1,
equip_avatar: equipAvatar
};
state.fullFreesrData.relics.unshift(newRelic); state.fullFreesrData.relics.unshift(newRelic);
await saveFreesrChangesToServer(); await saveFreesrChangesToServer();
loadEquipment(); loadEquipment();
@@ -8763,6 +8862,7 @@ function setupEvents() {
if (el.btnCancelAddRelic) { if (el.btnCancelAddRelic) {
el.btnCancelAddRelic.addEventListener('click', () => el.modalAddRelic.classList.remove('active')); el.btnCancelAddRelic.addEventListener('click', () => el.modalAddRelic.classList.remove('active'));
} }
document.getElementById('add-relic-search')?.addEventListener('input', renderAddRelicSetGrid);
if (el.addRelicSlot) { if (el.addRelicSlot) {
el.addRelicSlot.addEventListener('change', (e) => updateRelicMainAffixes(e.target.value)); el.addRelicSlot.addEventListener('change', (e) => updateRelicMainAffixes(e.target.value));
} }
+3 -3
View File
@@ -4323,10 +4323,10 @@ export const RELIC_SET_METADATA = {
} }
}; };
// Set data and artwork are from the 4.6.54 catalog. Creation needs matching server affix tables. // Set data is backed by the captured Client relic and affix tables documented in the server resources.
Object.assign(RELIC_SET_METADATA, { Object.assign(RELIC_SET_METADATA, {
329: { creationAvailable: false, names: { en: "God's Moment of Joy" }, desc2: "Increases the wearer's SPD by 6%. When the wearer's Elation reaches 40% or higher, the wearer's action advances by 40% immediately when entering combat.", images: { 5: './assets/relics/329-5.webp', 6: './assets/relics/329-6.webp' } }, 329: { clientCaptureRequired: true, names: { en: "God's Moment of Joy" }, desc2: "Increases the wearer's SPD by 6%. When the wearer's Elation reaches 40% or higher, the wearer's action advances by 40% immediately when entering combat.", images: { 5: './assets/relics/329-5.webp', 6: './assets/relics/329-6.webp' } },
330: { creationAvailable: false, names: { en: 'Subspace ASC Center' }, desc2: "Increases the wearer's SPD by 6%. When the wearer's SPD reaches 145 or higher, increases all allies' Break Effect by 18% after entering combat. This effect cannot stack.", images: { 5: './assets/relics/330-5.webp', 6: './assets/relics/330-6.webp' } } 330: { clientCaptureRequired: true, names: { en: 'Subspace ASC Center' }, desc2: "Increases the wearer's SPD by 6%. When the wearer's SPD reaches 145 or higher, increases all allies' Break Effect by 18% after entering combat. This effect cannot stack.", images: { 5: './assets/relics/330-5.webp', 6: './assets/relics/330-6.webp' } }
}); });
export function getLightconeInfo(lcId, langOverride = null) { export function getLightconeInfo(lcId, langOverride = null) {
+13 -1
View File
@@ -14,7 +14,19 @@ test('4.6 equipment metadata and bundled artwork are available', async () => {
const relic = RELIC_SET_METADATA[id]; const relic = RELIC_SET_METADATA[id];
assert.equal(getRelicSetInfo(id, 'en').name, name); assert.equal(getRelicSetInfo(id, 'en').name, name);
assert.ok(getRelicSetInfo(id, 'en').desc2); assert.ok(getRelicSetInfo(id, 'en').desc2);
assert.equal(relic.creationAvailable, false); assert.notEqual(relic.creationAvailable, false);
for (const slot of [5, 6]) assert.ok(fs.existsSync(path.resolve(__dirname, '..', relic.images[slot]))); for (const slot of [5, 6]) assert.ok(fs.existsSync(path.resolve(__dirname, '..', relic.images[slot])));
} }
}); });
test('captured relic templates reference matching client affix properties', () => {
const capture = require('../assets/relic-client-capture.json');
assert.deepEqual(capture.relics.map(row => row.ID), [63295, 63296, 63305, 63306]);
for (const relic of capture.relics) {
const main = capture.main_affixes.filter(row => row.GroupID === relic.MainAffixGroup);
const sub = capture.sub_affixes.filter(row => row.GroupID === relic.SubAffixGroup);
assert.equal(main.length, relic.Type === 5 ? 10 : 5);
assert.equal(sub.length, 12);
assert.ok(main.every(row => sub.filter(item => item.Property !== row.Property).length >= 4));
}
});