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
@@ -173,6 +173,7 @@ const el = {
|
||||
addLcPromotion: document.getElementById('add-lc-promotion'),
|
||||
addLcRank: document.getElementById('add-lc-rank'),
|
||||
addLcAvatar: document.getElementById('add-lc-avatar'),
|
||||
addLcAvatarBtn: document.getElementById('add-lc-avatar-btn'),
|
||||
btnOpenAddRelic: document.getElementById('btn-open-add-relic'),
|
||||
modalAddRelic: document.getElementById('modal-add-relic'),
|
||||
btnCloseAddRelicModal: document.getElementById('btn-close-add-relic-modal'),
|
||||
@@ -183,6 +184,7 @@ const el = {
|
||||
addRelicLevel: document.getElementById('add-relic-level'),
|
||||
addRelicMainAffix: document.getElementById('add-relic-main-affix'),
|
||||
addRelicAvatar: document.getElementById('add-relic-avatar'),
|
||||
addRelicAvatarBtn: document.getElementById('add-relic-avatar-btn'),
|
||||
|
||||
// Persistent Tab
|
||||
tabBtnPersistent: document.getElementById('tab-btn-persistent'),
|
||||
@@ -369,6 +371,17 @@ const el = {
|
||||
pickerSearch: document.getElementById('picker-search'),
|
||||
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')
|
||||
};
|
||||
|
||||
@@ -551,6 +564,44 @@ function getElementIconHtml(element, size = 20) {
|
||||
</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)
|
||||
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`;
|
||||
@@ -3768,7 +3819,8 @@ export function openSlotPickerForRelic(relic) {
|
||||
}
|
||||
window.openSlotPickerForRelic = openSlotPickerForRelic;
|
||||
|
||||
function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) {
|
||||
async function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) {
|
||||
await relicCaptureReady;
|
||||
state.slotPickerAvatarId = parseInt(avatarId, 10);
|
||||
state.slotPickerSlot = parseInt(slotNum, 10);
|
||||
slotPickerTargetRelic = targetRelicToEdit || null;
|
||||
@@ -4008,7 +4060,7 @@ function openSlotPicker(avatarId, slotNum, targetRelicToEdit = null) {
|
||||
// Confirm Custom Button
|
||||
if (btnConfirmCustom) {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@@ -4047,40 +4099,34 @@ function renderSlotPickerSubstats(slot, mainAffixId) {
|
||||
|
||||
const sVal = formatRelicSubstatValue(sub.sub_affix_id, sub.count);
|
||||
|
||||
let optsHtml = '';
|
||||
RELIC_SUBSTATS_CONFIG.forEach(stat => {
|
||||
const isConflict = stat.id === conflictSub;
|
||||
const isChosenElsewhere = slotPickerSubstats.some((s, i) => i !== idx && s.sub_affix_id === stat.id);
|
||||
const isSelected = stat.id === sub.sub_affix_id;
|
||||
const disabledAttr = (isConflict || (isChosenElsewhere && !isSelected)) ? 'disabled' : '';
|
||||
const conflictTag = isConflict ? t('substatMainConflict') : (isChosenElsewhere && !isSelected ? t('substatAlreadyChosen') : '');
|
||||
optsHtml += `<option value="${stat.id}" ${isSelected ? 'selected' : ''} ${disabledAttr}>${stat.name}${conflictTag}</option>`;
|
||||
});
|
||||
|
||||
const selectedStat = RELIC_SUBSTATS_CONFIG.find(stat => stat.id === sub.sub_affix_id);
|
||||
const options = RELIC_SUBSTATS_CONFIG.map(stat => {
|
||||
const conflict = stat.id === conflictSub;
|
||||
const used = slotPickerSubstats.some((other, i) => i !== idx && other.sub_affix_id === stat.id);
|
||||
const reason = conflict ? t('substatMainConflict') : (used ? t('substatAlreadyChosen') : '');
|
||||
return `<button type="button" class="substat-option" data-stat-id="${stat.id}" ${conflict || used ? 'disabled' : ''}>
|
||||
${getStatIconHtml(stat.icon, 18, '')}<span>${stat.name}${reason}</span></button>`;
|
||||
}).join('');
|
||||
card.innerHTML = `
|
||||
<div class="substat-row-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="substat-row-controls">
|
||||
${getStatIconHtml(RELIC_SUBSTATS_CONFIG.find(stat => stat.id === sub.sub_affix_id)?.icon, 20, "")}
|
||||
<select class="form-select substat-select" data-idx="${idx}">
|
||||
${optsHtml}
|
||||
</select>
|
||||
<details class="substat-menu">
|
||||
<summary class="substat-choice">${getStatIconHtml(selectedStat?.icon, 18, '')}<span>${selectedStat?.name || ''}</span></summary>
|
||||
<div class="substat-options">${options}</div>
|
||||
</details>
|
||||
<div class="substat-stepper">
|
||||
<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}">
|
||||
<button type="button" class="btn-substat-step plus" data-idx="${idx}" title="${t('substatIncreaseRoll')}">+</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Dropdown change
|
||||
card.querySelector('.substat-select')?.addEventListener('change', (e) => {
|
||||
const newId = parseInt(e.target.value, 10);
|
||||
slotPickerSubstats[idx].sub_affix_id = newId;
|
||||
</div>`;
|
||||
card.querySelectorAll('.substat-option').forEach(button => button.addEventListener('click', () => {
|
||||
slotPickerSubstats[idx].sub_affix_id = Number(button.dataset.statId);
|
||||
renderSlotPickerSubstats(slot, slotPickerSelectedMainAffix);
|
||||
});
|
||||
}));
|
||||
|
||||
// Stepper buttons
|
||||
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>` : ''}
|
||||
`;
|
||||
|
||||
if (RELIC_SET_METADATA[sId].creationAvailable === false) {
|
||||
if (!canCreateRelicSet(sId)) {
|
||||
card.style.opacity = '0.5';
|
||||
card.title = 'Requires matching server relic and main affix data';
|
||||
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
|
||||
function openAddRelicModal() {
|
||||
async function openAddRelicModal() {
|
||||
await relicCaptureReady;
|
||||
if (!el.modalAddRelic) return;
|
||||
el.addRelicSet.innerHTML = '';
|
||||
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 opt = document.createElement('option');
|
||||
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' : ''}`;
|
||||
el.addRelicSet.appendChild(opt);
|
||||
});
|
||||
@@ -6891,9 +6980,14 @@ function openAddRelicModal() {
|
||||
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.addRelicLevel.value = 15;
|
||||
updateRelicMainAffixes("1");
|
||||
const search = document.getElementById('add-relic-search');
|
||||
if (search) search.value = '';
|
||||
syncAddRelicSlot();
|
||||
renderAddRelicSetGrid();
|
||||
|
||||
el.modalAddRelic.classList.add('active');
|
||||
}
|
||||
@@ -6988,6 +7082,10 @@ async function handleAddRelicSubmit(e) {
|
||||
const mainAffixId = parseInt(el.addRelicMainAffix.value, 10) || 1;
|
||||
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;
|
||||
|
||||
if (!state.fullFreesrData) {
|
||||
@@ -6997,6 +7095,22 @@ async function handleAddRelicSubmit(e) {
|
||||
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) {
|
||||
state.fullFreesrData.relics.forEach(r => {
|
||||
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);
|
||||
await saveFreesrChangesToServer();
|
||||
loadEquipment();
|
||||
@@ -8763,6 +8862,7 @@ function setupEvents() {
|
||||
if (el.btnCancelAddRelic) {
|
||||
el.btnCancelAddRelic.addEventListener('click', () => el.modalAddRelic.classList.remove('active'));
|
||||
}
|
||||
document.getElementById('add-relic-search')?.addEventListener('input', renderAddRelicSetGrid);
|
||||
if (el.addRelicSlot) {
|
||||
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, {
|
||||
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' } },
|
||||
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' } }
|
||||
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: { 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) {
|
||||
|
||||
Reference in new issue
Block a user