fix: enable client-verified planar relic sets
This commit is contained in:
1 parent
22e07ea5f2
commit
303332469a
6 files changed
+838
-60
No files matched your search
@@ -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
@@ -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
@@ -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>
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user