feat: fix multilingual element/path icons, sort avatars descending, add LC & Relic creators for freesr, and add Aha global toggle

This commit is contained in:
CAtlas0784 committed 2026-10-17 00:48:33 +07:00
1 parent 6438b86450
commit 29a78b4e10
4 files changed
+683 -20

No files matched your search

+329 -14
View File
@@ -1,5 +1,5 @@
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
import { getLightconeInfo, getRelicSetInfo } from './equipment-data.js';
import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetInfo } from './equipment-data.js';
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js';
// Application State
@@ -60,6 +60,26 @@ const el = {
relicsList: document.getElementById('relics-list'),
badgeTotalLc: document.getElementById('badge-total-lc'),
badgeTotalRelics: document.getElementById('badge-total-relics'),
btnOpenAddLc: document.getElementById('btn-open-add-lc'),
modalAddLc: document.getElementById('modal-add-lc'),
btnCloseAddLcModal: document.getElementById('btn-close-add-lc-modal'),
btnCancelAddLc: document.getElementById('btn-cancel-add-lc'),
formAddLc: document.getElementById('form-add-lc'),
addLcSelect: document.getElementById('add-lc-select'),
addLcLevel: document.getElementById('add-lc-level'),
addLcPromotion: document.getElementById('add-lc-promotion'),
addLcRank: document.getElementById('add-lc-rank'),
addLcAvatar: document.getElementById('add-lc-avatar'),
btnOpenAddRelic: document.getElementById('btn-open-add-relic'),
modalAddRelic: document.getElementById('modal-add-relic'),
btnCloseAddRelicModal: document.getElementById('btn-close-add-relic-modal'),
btnCancelAddRelic: document.getElementById('btn-cancel-add-relic'),
formAddRelic: document.getElementById('form-add-relic'),
addRelicSet: document.getElementById('add-relic-set'),
addRelicSlot: document.getElementById('add-relic-slot'),
addRelicLevel: document.getElementById('add-relic-level'),
addRelicMainAffix: document.getElementById('add-relic-main-affix'),
addRelicAvatar: document.getElementById('add-relic-avatar'),
// Persistent Tab
tabBtnPersistent: document.getElementById('tab-btn-persistent'),
@@ -68,6 +88,7 @@ const el = {
persistentMarchSelect: document.getElementById('persistent-march-select'),
toggleSwGlobal: document.getElementById('toggle-sw-global'),
toggleCastoriceGlobal: document.getElementById('toggle-castorice-global'),
toggleAhaGlobal: document.getElementById('toggle-aha-global'),
btnSavePersistentToggles: document.getElementById('btn-save-persistent-toggles'),
// Ex-Rank
@@ -236,7 +257,45 @@ function setConnectionStatus(connected, extraText = '') {
}
// Helper to get element CSS class
// Canonical element & path maps (handles English and all 13 supported languages)
const CANONICAL_ELEMENTS = {
physical: 'Physical', fire: 'Fire', ice: 'Ice', lightning: 'Lightning',
wind: 'Wind', quantum: 'Quantum', imaginary: 'Imaginary', thunder: 'Lightning'
};
const CANONICAL_PATHS = {
destruction: 'Destruction', hunt: 'Hunt', 'the hunt': 'Hunt', erudition: 'Erudition',
harmony: 'Harmony', nihility: 'Nihility', preservation: 'Preservation',
abundance: 'Abundance', remembrance: 'Remembrance', elation: 'Elation'
};
// Dynamically index all translated element and path strings across languages
for (const langKey in TRANSLATIONS) {
const tr = TRANSLATIONS[langKey];
if (tr.elements) {
for (const [enKey, locVal] of Object.entries(tr.elements)) {
if (locVal) CANONICAL_ELEMENTS[String(locVal).trim().toLowerCase()] = enKey;
}
}
if (tr.paths) {
for (const [enKey, locVal] of Object.entries(tr.paths)) {
if (locVal) CANONICAL_PATHS[String(locVal).trim().toLowerCase()] = (enKey === 'The Hunt' ? 'Hunt' : enKey);
}
}
}
function normalizeElement(elem) {
if (!elem) return 'Physical';
return CANONICAL_ELEMENTS[String(elem).trim().toLowerCase()] || elem;
}
function normalizePath(p) {
if (!p) return 'Destruction';
return CANONICAL_PATHS[String(p).trim().toLowerCase()] || (p === 'The Hunt' ? 'Hunt' : p);
}
function getElementClass(elem) {
const canonical = normalizeElement(elem);
const map = {
Fire: 'elem-fire',
Ice: 'elem-ice',
@@ -246,30 +305,31 @@ function getElementClass(elem) {
Quantum: 'elem-quantum',
Imaginary: 'elem-imaginary'
};
return map[elem] || 'elem-physical';
return map[canonical] || 'elem-physical';
}
// Render Element Icon HTML (Official Star Rail Game Icon)
function getElementIconHtml(element, size = 20) {
if (!element) return '';
const elemClass = getElementClass(element);
const canonical = normalizeElement(element);
const elemClass = getElementClass(canonical);
return `<span class="icon-badge element-icon-badge ${elemClass}" title="${element}">
<img src="assets/elements/${element}.png"
<img src="assets/elements/${canonical}.png"
alt="${element}"
style="width: ${size}px; height: ${size}px;"
onerror="if(this.src.indexOf('StarRailRes')===-1){this.src='https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/element/${element === 'Lightning' ? 'Thunder' : element}.png';}">
onerror="if(this.src.indexOf('StarRailRes')===-1){this.src='https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/element/${canonical === 'Lightning' ? 'Thunder' : canonical}.png';}">
</span>`;
}
// Render Path Icon HTML (Official Star Rail Game Icon)
function getPathIconHtml(path, size = 20) {
if (!path) return '';
const safePath = path === 'The Hunt' ? 'Hunt' : path;
const canonical = normalizePath(path);
return `<span class="icon-badge path-icon-badge" title="${path}">
<img src="assets/paths/${path}.png"
<img src="assets/paths/${canonical}.png"
alt="${path}"
style="width: ${size}px; height: ${size}px;"
onerror="if(this.src.indexOf('StarRailRes')===-1){this.src='https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/path/${safePath}.png';}">
onerror="if(this.src.indexOf('StarRailRes')===-1){this.src='https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/path/${canonical}.png';}">
</span>`;
}
@@ -442,6 +502,7 @@ function initAvatarsList() {
rank: 6
});
}
list.sort((a, b) => b.avatar_id - a.avatar_id);
state.avatarsList = list;
}
@@ -580,8 +641,9 @@ function renderPickerGrid() {
});
el.pickerAvatarsGrid.appendChild(removeCard);
// Available avatars
state.avatarsList.forEach(item => {
// Available avatars (sorted descending by avatar_id)
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(item => {
const meta = getAvatarInfo(item.avatar_id);
if (search && !meta.name.toLowerCase().includes(search) && !String(item.avatar_id).includes(search)) {
return;
@@ -665,6 +727,7 @@ async function loadAvatars() {
console.warn('Live avatar list fetch failed, using local list:', err);
}
}
state.avatarsList.sort((a, b) => b.avatar_id - a.avatar_id);
renderAvatarsGrid();
}
@@ -678,11 +741,15 @@ function renderAvatarsGrid() {
const meta = getAvatarInfo(item.avatar_id);
const matchesSearch = meta.name.toLowerCase().includes(searchTerm) || String(item.avatar_id).includes(searchTerm);
const matchesRarity = rarityFilter === 'all' || String(meta.rarity) === rarityFilter;
const matchesElement = elementFilter === 'all' || meta.element === elementFilter;
const matchesPath = pathFilter === 'all' || meta.path === pathFilter || (pathFilter === 'Hunt' && (meta.path === 'Hunt' || meta.path === 'The Hunt'));
const elemCanonical = normalizeElement(meta.rawElement || meta.element);
const matchesElement = elementFilter === 'all' || elemCanonical === normalizeElement(elementFilter);
const pathCanonical = normalizePath(meta.rawPath || meta.path);
const matchesPath = pathFilter === 'all' || pathCanonical === normalizePath(pathFilter);
return matchesSearch && matchesRarity && matchesElement && matchesPath;
});
filtered.sort((a, b) => b.avatar_id - a.avatar_id);
if (filtered.length === 0) {
el.avatarsGrid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">${t('noAvatarsFound')}</div>`;
return;
@@ -1294,9 +1361,25 @@ async function loadEquipment() {
Lv. ${lc.level} • S${lc.rank} • ${t('equippedBy')}: <strong style="color: var(--accent-cyan);">${equipped}</strong>
</div>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger btn-delete-lc" data-uid="${lc.internal_uid}" title="${t('deleteItem')}">🗑️</button>
</div>
`;
el.lightconesList.appendChild(item);
});
// Bind delete LC buttons
el.lightconesList.querySelectorAll('.btn-delete-lc').forEach(btn => {
btn.addEventListener('click', async (e) => {
const uid = parseInt(e.currentTarget.getAttribute('data-uid'), 10);
if (state.fullFreesrData?.lightcones) {
state.fullFreesrData.lightcones = state.fullFreesrData.lightcones.filter(l => l.internal_uid !== uid);
await saveFreesrChangesToServer();
loadEquipment();
showToast(t('toastItemDeleted'), 'info');
}
});
});
}
// Render Relics
@@ -1325,15 +1408,214 @@ async function loadEquipment() {
Lv. ${r.level} • ${t('equippedBy')}: <strong style="color: var(--accent-cyan);">${equipped}</strong>
</div>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger btn-delete-relic" data-uid="${r.internal_uid}" title="${t('deleteItem')}">🗑️</button>
</div>
`;
el.relicsList.appendChild(item);
});
// Bind delete Relic buttons
el.relicsList.querySelectorAll('.btn-delete-relic').forEach(btn => {
btn.addEventListener('click', async (e) => {
const uid = parseInt(e.currentTarget.getAttribute('data-uid'), 10);
if (state.fullFreesrData?.relics) {
state.fullFreesrData.relics = state.fullFreesrData.relics.filter(r => r.internal_uid !== uid);
await saveFreesrChangesToServer();
loadEquipment();
showToast(t('toastItemDeleted'), 'info');
}
});
});
}
} catch (err) {
console.warn('Equipment loading error:', err);
}
}
// Open Add Light Cone Modal
function openAddLcModal() {
if (!el.modalAddLc) return;
el.addLcSelect.innerHTML = '';
const lcIds = Object.keys(LIGHTCONE_METADATA).sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
lcIds.forEach(id => {
const meta = getLightconeInfo(id);
const opt = document.createElement('option');
opt.value = id;
opt.textContent = `★${meta.rarity} [${meta.path || 'LC'}] ${meta.name} (ID: ${id})`;
el.addLcSelect.appendChild(opt);
});
el.addLcAvatar.innerHTML = `<option value="0">${t('unassigned')}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const opt = document.createElement('option');
opt.value = av.avatar_id;
opt.textContent = `${info.name} (ID: ${av.avatar_id})`;
el.addLcAvatar.appendChild(opt);
});
el.addLcLevel.value = 80;
el.addLcPromotion.value = 6;
el.addLcRank.value = 5;
el.modalAddLc.classList.add('active');
}
// Open Add Relic Modal
function openAddRelicModal() {
if (!el.modalAddRelic) return;
el.addRelicSet.innerHTML = '';
const setIds = Object.keys(RELIC_SET_METADATA).sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
setIds.forEach(setId => {
const meta = getRelicSetInfo(setId);
const opt = document.createElement('option');
opt.value = setId;
opt.textContent = `${meta.name} (Set ID: ${setId})`;
el.addRelicSet.appendChild(opt);
});
el.addRelicAvatar.innerHTML = `<option value="0">${t('unassigned')}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const opt = document.createElement('option');
opt.value = av.avatar_id;
opt.textContent = `${info.name} (ID: ${av.avatar_id})`;
el.addRelicAvatar.appendChild(opt);
});
el.addRelicSlot.value = "1";
el.addRelicLevel.value = 15;
updateRelicMainAffixes("1");
el.modalAddRelic.classList.add('active');
}
function updateRelicMainAffixes(slot) {
if (!el.addRelicMainAffix) return;
el.addRelicMainAffix.innerHTML = '';
const affixesBySlot = {
"1": [{ id: 1, name: "HP (Flat)" }],
"2": [{ id: 1, name: "ATK (Flat)" }],
"3": [
{ id: 1, name: "HP%" }, { id: 2, name: "ATK%" }, { id: 3, name: "DEF%" },
{ id: 4, name: "CRIT Rate%" }, { id: 5, name: "CRIT DMG%" },
{ id: 6, name: "Outgoing Healing Boost%" }, { id: 7, name: "Effect Hit Rate%" }
],
"4": [
{ id: 1, name: "HP%" }, { id: 2, name: "ATK%" }, { id: 3, name: "DEF%" },
{ id: 4, name: "SPD" }
],
"5": [
{ id: 1, name: "HP%" }, { id: 2, name: "ATK%" }, { id: 3, name: "DEF%" },
{ id: 4, name: "Physical DMG Boost%" }, { id: 5, name: "Fire DMG Boost%" },
{ id: 6, name: "Ice DMG Boost%" }, { id: 7, name: "Lightning DMG Boost%" },
{ id: 8, name: "Wind DMG Boost%" }, { id: 9, name: "Quantum DMG Boost%" },
{ id: 10, name: "Imaginary DMG Boost%" }
],
"6": [
{ id: 1, name: "Break Effect%" }, { id: 2, name: "Energy Regeneration Rate%" },
{ id: 3, name: "HP%" }, { id: 4, name: "ATK%" }, { id: 5, name: "DEF%" }
]
};
const list = affixesBySlot[slot] || [{ id: 1, name: "Main Stat" }];
list.forEach(a => {
const opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.name;
el.addRelicMainAffix.appendChild(opt);
});
}
async function handleAddLcSubmit(e) {
e.preventDefault();
const itemId = parseInt(el.addLcSelect.value, 10);
const level = parseInt(el.addLcLevel.value, 10) || 80;
const promotion = parseInt(el.addLcPromotion.value, 10) || 6;
const rank = parseInt(el.addLcRank.value, 10) || 5;
const equipAvatar = parseInt(el.addLcAvatar.value, 10) || 0;
if (!state.fullFreesrData) {
state.fullFreesrData = { lightcones: [], relics: [], avatars: {} };
}
if (!state.fullFreesrData.lightcones) {
state.fullFreesrData.lightcones = [];
}
if (equipAvatar !== 0) {
state.fullFreesrData.lightcones.forEach(lc => {
if (lc.equip_avatar === equipAvatar) lc.equip_avatar = 0;
});
}
const maxUid = state.fullFreesrData.lightcones.reduce((m, lc) => Math.max(m, lc.internal_uid || 0), 0);
const newLc = {
level,
item_id: itemId,
equip_avatar: equipAvatar,
rank,
promotion,
internal_uid: maxUid + 1
};
state.fullFreesrData.lightcones.unshift(newLc);
await saveFreesrChangesToServer();
loadEquipment();
el.modalAddLc.classList.remove('active');
showToast(t('toastLcAdded'), 'success');
}
async function handleAddRelicSubmit(e) {
e.preventDefault();
const setId = parseInt(el.addRelicSet.value, 10);
const slot = parseInt(el.addRelicSlot.value, 10) || 1;
const level = parseInt(el.addRelicLevel.value, 10) || 15;
const mainAffixId = parseInt(el.addRelicMainAffix.value, 10) || 1;
const equipAvatar = parseInt(el.addRelicAvatar.value, 10) || 0;
const relicId = 60000 + (setId * 10) + slot;
if (!state.fullFreesrData) {
state.fullFreesrData = { lightcones: [], relics: [], avatars: {} };
}
if (!state.fullFreesrData.relics) {
state.fullFreesrData.relics = [];
}
if (equipAvatar !== 0) {
state.fullFreesrData.relics.forEach(r => {
if (r.equip_avatar === equipAvatar && (r.relic_id % 10 === slot)) {
r.equip_avatar = 0;
}
});
}
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, step: 1 },
{ sub_affix_id: 2, cnt: 2, step: 1 },
{ sub_affix_id: 3, cnt: 3, step: 2 },
{ sub_affix_id: 4, cnt: 4, step: 2 }
],
internal_uid: maxUid + 1,
equip_avatar: equipAvatar
};
state.fullFreesrData.relics.unshift(newRelic);
await saveFreesrChangesToServer();
loadEquipment();
el.modalAddRelic.classList.remove('active');
showToast(t('toastRelicAdded'), 'success');
}
// Load Persistent Save Settings (Exclusive to GaC-hkrpg-go)
async function loadPersistentSettings() {
if (state.serverMode !== 'gac') return;
@@ -1351,6 +1633,7 @@ async function loadPersistentSettings() {
el.toggleSwGlobal.checked = res.data.enable_sw_global !== false;
el.toggleCastoriceGlobal.checked = res.data.enable_castorice_global !== false;
if (el.toggleAhaGlobal) el.toggleAhaGlobal.checked = res.data.enable_aha_global !== false;
} catch (err) {
console.warn('Persistent settings loading error:', err);
}
@@ -1704,18 +1987,50 @@ function setupEvents() {
el.btnSavePersistentToggles.addEventListener('click', async () => {
const sw = el.toggleSwGlobal.checked;
const castorice = el.toggleCastoriceGlobal.checked;
const aha = el.toggleAhaGlobal ? el.toggleAhaGlobal.checked : true;
try {
await apiCall('/api/persistent', 'POST', {
enable_sw_global: sw,
enable_castorice_global: castorice
enable_castorice_global: castorice,
enable_aha_global: aha
});
showToast('บันทึกสวิตช์ Global เรียบร้อย!', 'success');
showToast(t('btnSaveGlobalSwitch') || 'บันทึกสวิตช์ Global เรียบร้อย!', 'success');
refreshStatus();
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
});
// Equipment Add Modal Controls
if (el.btnOpenAddLc) {
el.btnOpenAddLc.addEventListener('click', openAddLcModal);
}
if (el.btnCloseAddLcModal) {
el.btnCloseAddLcModal.addEventListener('click', () => el.modalAddLc.classList.remove('active'));
}
if (el.btnCancelAddLc) {
el.btnCancelAddLc.addEventListener('click', () => el.modalAddLc.classList.remove('active'));
}
if (el.formAddLc) {
el.formAddLc.addEventListener('submit', handleAddLcSubmit);
}
if (el.btnOpenAddRelic) {
el.btnOpenAddRelic.addEventListener('click', openAddRelicModal);
}
if (el.btnCloseAddRelicModal) {
el.btnCloseAddRelicModal.addEventListener('click', () => el.modalAddRelic.classList.remove('active'));
}
if (el.btnCancelAddRelic) {
el.btnCancelAddRelic.addEventListener('click', () => el.modalAddRelic.classList.remove('active'));
}
if (el.addRelicSlot) {
el.addRelicSlot.addEventListener('change', (e) => updateRelicMainAffixes(e.target.value));
}
if (el.formAddRelic) {
el.formAddRelic.addEventListener('submit', handleAddRelicSubmit);
}
// Avatar Search & Filters
el.avatarSearch.addEventListener('input', renderAvatarsGrid);
el.avatarFilterRarity.addEventListener('change', renderAvatarsGrid);