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

+118 -6
View File
@@ -187,9 +187,12 @@
<div class="grid-2">
<!-- Lightcones Card -->
<div class="card">
<div class="card-title">
<span data-i18n="eqLcTitle">🗡️ กรวยแสงในคลัง</span>
<span class="btn-sm btn-secondary" id="badge-total-lc">0 ชิ้น</span>
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span data-i18n="eqLcTitle">🗡️ กรวยแสงในคลัง</span>
<span class="btn-sm btn-secondary" id="badge-total-lc">0 ชิ้น</span>
</div>
<button type="button" class="btn btn-sm btn-primary" id="btn-open-add-lc" data-i18n="btnAddLc">+ เพิ่ม LC</button>
</div>
<p class="card-subtitle" data-i18n="eqLcSubtitle">รายการกรวยแสงทั้งหมดจาก freesr-data.json</p>
<div id="lightcones-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
@@ -199,9 +202,12 @@
<!-- Relics Card -->
<div class="card">
<div class="card-title">
<span data-i18n="eqRelicTitle">🛡️ รีลิกส์ในคลัง</span>
<span class="btn-sm btn-secondary" id="badge-total-relics">0 ชิ้น</span>
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span data-i18n="eqRelicTitle">🛡️ รีลิกส์ในคลัง</span>
<span class="btn-sm btn-secondary" id="badge-total-relics">0 ชิ้น</span>
</div>
<button type="button" class="btn btn-sm btn-primary" id="btn-open-add-relic" data-i18n="btnAddRelic">+ เพิ่มรีลิกส์</button>
</div>
<p class="card-subtitle" data-i18n="eqRelicSubtitle">รายการรีลิกส์ทั้งหมดจาก freesr-data.json</p>
<div id="relics-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
@@ -282,6 +288,17 @@
</label>
</div>
<div class="ex-switch-card">
<div>
<div style="font-weight: 700;">Aeon ★ Aha Global Feature</div>
<div style="font-size: 0.8rem; color: var(--text-muted);" data-i18n="ahaGlobalDesc">เปิดใช้งานฟังก์ชันพิเศษ Aeon ★ Aha ทั่วทั้งเซิร์ฟเวอร์</div>
</div>
<label class="switch">
<input type="checkbox" id="toggle-aha-global">
<span class="slider"></span>
</label>
</div>
<button class="btn btn-success" id="btn-save-persistent-toggles" data-i18n="btnSaveGlobalSwitch">
💾 บันทึกสวิตช์ Global
</button>
@@ -653,6 +670,101 @@
</div>
</div>
<!-- Add Light Cone Modal -->
<div class="modal-overlay" id="modal-add-lc">
<div class="modal-content" style="max-width: 540px;">
<div class="modal-header">
<h3 style="font-weight: 800;" data-i18n="modalAddLcTitle">🗡️ เพิ่มกรวยแสงลง FreeSR</h3>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-add-lc-modal">✕</button>
</div>
<form id="form-add-lc">
<div class="form-group">
<label class="form-label" data-i18n="lblSelectLc">เลือกกรวยแสง</label>
<select id="add-lc-select" class="form-select" required style="max-height: 200px;">
<!-- Injected by JS -->
</select>
</div>
<div class="grid-3" style="gap: 0.75rem; margin-bottom: 1rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="modalColLevel">เลเวล (1-80)</label>
<input type="number" id="add-lc-level" class="form-input" min="1" max="80" value="80" required>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="lblPromotion">ขัดเกลา (0-6)</label>
<input type="number" id="add-lc-promotion" class="form-input" min="0" max="6" value="6" required>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">Superimpose (1-5)</label>
<input type="number" id="add-lc-rank" class="form-input" min="1" max="5" value="5" required>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="lblEquipAvatar">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label>
<select id="add-lc-avatar" class="form-select">
<option value="0" data-i18n="unassigned">-- ยังไม่สวมใส่ (เก็บในคลัง) --</option>
<!-- Injected by JS -->
</select>
</div>
<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-lc" data-i18n="btnCancel">ยกเลิก</button>
<button type="submit" class="btn btn-success" data-i18n="btnAddLcSubmit">💾 เพิ่มลง FreeSR</button>
</div>
</form>
</div>
</div>
<!-- Add Relic Modal -->
<div class="modal-overlay" id="modal-add-relic">
<div class="modal-content" style="max-width: 580px;">
<div class="modal-header">
<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>
</div>
<form id="form-add-relic">
<div class="form-group">
<label class="form-label" data-i18n="lblSelectRelicSet">เลือกเซ็ตรีลิกส์</label>
<select id="add-relic-set" class="form-select" required>
<!-- Injected by JS -->
</select>
</div>
<div class="grid-2" style="gap: 0.75rem; margin-bottom: 1rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="lblRelicSlot">ชิ้นส่วน (Slot)</label>
<select id="add-relic-slot" class="form-select" required>
<option value="1">1: ส่วนหัว (Head / HP)</option>
<option value="2">2: ส่วนมือ (Hands / ATK)</option>
<option value="3">3: ลำตัว (Body)</option>
<option value="4">4: รองเท้า (Feet)</option>
<option value="5">5: ลูกแก้ว (Planar Sphere)</option>
<option value="6">6: สร้อยเชื่อมต่อ (Link Rope)</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="modalColLevel">เลเวล (0-15)</label>
<input type="number" id="add-relic-level" class="form-input" min="0" max="15" value="15" required>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="lblMainAffix">สเตตัสหลัก (Main Affix)</label>
<select id="add-relic-main-affix" class="form-select" required>
<!-- Dynamic according to selected slot -->
</select>
</div>
<div class="form-group">
<label class="form-label" data-i18n="lblEquipAvatar">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label>
<select id="add-relic-avatar" class="form-select">
<option value="0" data-i18n="unassigned">-- ยังไม่สวมใส่ (เก็บในคลัง) --</option>
<!-- Injected by JS -->
</select>
</div>
<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="submit" class="btn btn-success" data-i18n="btnAddRelicSubmit">💾 เพิ่มลง FreeSR</button>
</div>
</form>
</div>
</div>
<!-- Toast Notification Container -->
<div class="toast-container" id="toast-container"></div>
+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);
+2
View File
@@ -2102,6 +2102,8 @@ export function getAvatarInfo(avatarId, langOverride = null) {
name: localizedName,
path: translatePath(meta.path),
element: translateElement(meta.element),
rawPath: meta.path,
rawElement: meta.element,
rarity: meta.rarity,
hasExRank: !!meta.hasExRank
};
+234
View File
@@ -19,6 +19,24 @@ export const SUPPORTED_LANGUAGES = [
export const TRANSLATIONS = {
"th": {
"ahaGlobalDesc": "เปิดใช้งานฟังก์ชันพิเศษ Aeon ★ Aha ทั่วทั้งเซิร์ฟเวอร์",
"btnAddLc": "+ เพิ่ม LC",
"btnAddRelic": "+ เพิ่มรีลิกส์",
"modalAddLcTitle": "🗡️ เพิ่มกรวยแสงลง FreeSR",
"modalAddRelicTitle": "🛡️ เพิ่มรีลิกส์ลง FreeSR",
"lblSelectLc": "เลือกกรวยแสง",
"lblSelectRelicSet": "เลือกเซ็ตรีลิกส์",
"lblRelicSlot": "ชิ้นส่วน (Slot)",
"lblMainAffix": "สเตตัสหลัก (Main Affix)",
"lblEquipAvatar": "สวมใส่ให้ตัวละคร",
"unassigned": "-- ยังไม่สวมใส่ (เก็บในคลัง) --",
"btnAddLcSubmit": "💾 เพิ่มลง FreeSR",
"btnAddRelicSubmit": "💾 เพิ่มลง FreeSR",
"toastLcAdded": "เพิ่มกรวยแสงลงใน FreeSR สำเร็จแล้ว",
"toastRelicAdded": "เพิ่มรีลิกส์ลงใน FreeSR สำเร็จแล้ว",
"deleteItem": "ลบ",
"toastItemDeleted": "ลบไอเทมสำเร็จแล้ว",
"appName": "GaC-Srtool",
"appSubtitle": "Star Rail FreeSR & Server Controller",
"online": "เชื่อมต่อแล้ว",
@@ -195,6 +213,24 @@ export const TRANSLATIONS = {
"settingsReset": "คืนค่าเริ่มต้นเรียบร้อย"
},
"en": {
"ahaGlobalDesc": "Enable server-wide Aeon ★ Aha special trait",
"btnAddLc": "+ Add LC",
"btnAddRelic": "+ Add Relic",
"modalAddLcTitle": "🗡️ Add Light Cone to FreeSR",
"modalAddRelicTitle": "🛡️ Add Relic to FreeSR",
"lblSelectLc": "Select Light Cone",
"lblSelectRelicSet": "Select Relic Set",
"lblRelicSlot": "Relic Slot",
"lblMainAffix": "Main Stat",
"lblEquipAvatar": "Equip to Avatar",
"unassigned": "-- Unassigned (Inventory) --",
"btnAddLcSubmit": "💾 Add to FreeSR",
"btnAddRelicSubmit": "💾 Add to FreeSR",
"toastLcAdded": "Light Cone added to FreeSR",
"toastRelicAdded": "Relic added to FreeSR",
"deleteItem": "Delete",
"toastItemDeleted": "Item deleted successfully",
"appName": "GaC-Srtool",
"appSubtitle": "Star Rail FreeSR & Server Controller",
"online": "Connected",
@@ -371,6 +407,24 @@ export const TRANSLATIONS = {
"settingsReset": "Reset to default settings"
},
"zh-CN": {
"ahaGlobalDesc": "全服启用星神·阿哈特殊被动",
"btnAddLc": "+ 添加光锥",
"btnAddRelic": "+ 添加遗器",
"modalAddLcTitle": "🗡️ 添加光锥至 FreeSR",
"modalAddRelicTitle": "🛡️ 添加遗器至 FreeSR",
"lblSelectLc": "选择光锥",
"lblSelectRelicSet": "选择遗器套装",
"lblRelicSlot": "遗器部位",
"lblMainAffix": "主词条属性",
"lblEquipAvatar": "装备给角色",
"unassigned": "-- 暂不装备(存入背包) --",
"btnAddLcSubmit": "💾 添加至 FreeSR",
"btnAddRelicSubmit": "💾 添加至 FreeSR",
"toastLcAdded": "已成功添加光锥至 FreeSR",
"toastRelicAdded": "已成功添加遗器至 FreeSR",
"deleteItem": "删除",
"toastItemDeleted": "道具已成功删除",
"appName": "GaC-Srtool",
"appSubtitle": "星穹铁道 FreeSR 与服务器控制器",
"online": "已连接",
@@ -547,6 +601,24 @@ export const TRANSLATIONS = {
"settingsReset": "已恢复默认设置"
},
"zh-TW": {
"ahaGlobalDesc": "全服啟用星神·阿哈特殊被動",
"btnAddLc": "+ 新增光錐",
"btnAddRelic": "+ 新增遺器",
"modalAddLcTitle": "🗡️ 新增光錐至 FreeSR",
"modalAddRelicTitle": "🛡️ 新增遺器至 FreeSR",
"lblSelectLc": "選擇光錐",
"lblSelectRelicSet": "選擇遺器套裝",
"lblRelicSlot": "遺器部位",
"lblMainAffix": "主詞條屬性",
"lblEquipAvatar": "裝備給角色",
"unassigned": "-- 暫不裝備(存入背包) --",
"btnAddLcSubmit": "💾 新增至 FreeSR",
"btnAddRelicSubmit": "💾 新增至 FreeSR",
"toastLcAdded": "已成功新增光錐至 FreeSR",
"toastRelicAdded": "已成功新增遺器至 FreeSR",
"deleteItem": "刪除",
"toastItemDeleted": "道具已成功刪除",
"appName": "GaC-Srtool",
"appSubtitle": "星穹鐵道 FreeSR 與伺服器控制器",
"online": "已連線",
@@ -723,6 +795,24 @@ export const TRANSLATIONS = {
"settingsReset": "已恢復預設設定"
},
"ja": {
"ahaGlobalDesc": "全サーバーで星神・アッハの特殊パッシブを有効化",
"btnAddLc": "+ 光円錐を追加",
"btnAddRelic": "+ 遺物を追加",
"modalAddLcTitle": "🗡️ FreeSRに光円錐を追加",
"modalAddRelicTitle": "🛡️ FreeSRに遺物を追加",
"lblSelectLc": "光円錐を選択",
"lblSelectRelicSet": "遺物セットを選択",
"lblRelicSlot": "部位",
"lblMainAffix": "メインステータス",
"lblEquipAvatar": "キャラクターに装備",
"unassigned": "-- 未装備(インベントリ) --",
"btnAddLcSubmit": "💾 FreeSRに追加",
"btnAddRelicSubmit": "💾 FreeSRに追加",
"toastLcAdded": "FreeSRに光円錐を追加しました",
"toastRelicAdded": "FreeSRに遺物を追加しました",
"deleteItem": "削除",
"toastItemDeleted": "アイテムを削除しました",
"appName": "GaC-Srtool",
"appSubtitle": "スターレイル FreeSR & サーバーコントローラー",
"online": "接続完了",
@@ -899,6 +989,24 @@ export const TRANSLATIONS = {
"settingsReset": "デフォルト設定に戻しました"
},
"ko": {
"ahaGlobalDesc": "서버 전체 에이언즈★아하 특수 패시브 활성화",
"btnAddLc": "+ 광추 추가",
"btnAddRelic": "+ 유물 추가",
"modalAddLcTitle": "🗡️ FreeSR에 광추 추가",
"modalAddRelicTitle": "🛡️ FreeSR에 유물 추가",
"lblSelectLc": "광추 선택",
"lblSelectRelicSet": "유물 세트 선택",
"lblRelicSlot": "부위",
"lblMainAffix": "메인 속성",
"lblEquipAvatar": "캐릭터에 장착",
"unassigned": "-- 미장착 (인벤토리) --",
"btnAddLcSubmit": "💾 FreeSR에 추가",
"btnAddRelicSubmit": "💾 FreeSR에 추가",
"toastLcAdded": "FreeSR에 광추가 추가되었습니다",
"toastRelicAdded": "FreeSR에 유물이 추가되었습니다",
"deleteItem": "삭제",
"toastItemDeleted": "아이템이 삭제되었습니다",
"appName": "GaC-Srtool",
"appSubtitle": "스타레일 FreeSR & 서버 컨트롤러",
"online": "연결됨",
@@ -1075,6 +1183,24 @@ export const TRANSLATIONS = {
"settingsReset": "기본 설정으로 복원되었습니다"
},
"vi": {
"ahaGlobalDesc": "Bật cơ chế đặc biệt Aeon ★ Aha toàn máy chủ",
"btnAddLc": "+ Thêm Nón Ánh Sáng",
"btnAddRelic": "+ Thêm Di Vật",
"modalAddLcTitle": "🗡️ Thêm Nón Ánh Sáng vào FreeSR",
"modalAddRelicTitle": "🛡️ Thêm Di Vật vào FreeSR",
"lblSelectLc": "Chọn Nón Ánh Sáng",
"lblSelectRelicSet": "Chọn Bộ Di Vật",
"lblRelicSlot": "Vị trí Di Vật",
"lblMainAffix": "Thuộc tính chính",
"lblEquipAvatar": "Trang bị cho nhân vật",
"unassigned": "-- Chưa trang bị (Kho đồ) --",
"btnAddLcSubmit": "💾 Thêm vào FreeSR",
"btnAddRelicSubmit": "💾 Thêm vào FreeSR",
"toastLcAdded": "Đã thêm Nón Ánh Sáng vào FreeSR",
"toastRelicAdded": "Đã thêm Di Vật vào FreeSR",
"deleteItem": "Xóa",
"toastItemDeleted": "Đã xóa vật phẩm thành công",
"appName": "GaC-Srtool",
"appSubtitle": "Bộ điều khiển máy chủ & FreeSR Star Rail",
"online": "Đã kết nối",
@@ -1251,6 +1377,24 @@ export const TRANSLATIONS = {
"settingsReset": "Đã khôi phục cài đặt mặc định"
},
"id": {
"ahaGlobalDesc": "Aktifkan kemampuan khusus Aeon ★ Aha di seluruh server",
"btnAddLc": "+ Tambah Light Cone",
"btnAddRelic": "+ Tambah Relic",
"modalAddLcTitle": "🗡️ Tambah Light Cone ke FreeSR",
"modalAddRelicTitle": "🛡️ Tambah Relic ke FreeSR",
"lblSelectLc": "Pilih Light Cone",
"lblSelectRelicSet": "Pilih Set Relic",
"lblRelicSlot": "Slot Relic",
"lblMainAffix": "Stat Utama",
"lblEquipAvatar": "Pasang ke Karakter",
"unassigned": "-- Belum Dipasang (Tas) --",
"btnAddLcSubmit": "💾 Tambah ke FreeSR",
"btnAddRelicSubmit": "💾 Tambah ke FreeSR",
"toastLcAdded": "Light Cone berhasil ditambahkan ke FreeSR",
"toastRelicAdded": "Relic berhasil ditambahkan ke FreeSR",
"deleteItem": "Hapus",
"toastItemDeleted": "Item berhasil dihapus",
"appName": "GaC-Srtool",
"appSubtitle": "Pengontrol Server & FreeSR Star Rail",
"online": "Terhubung",
@@ -1427,6 +1571,24 @@ export const TRANSLATIONS = {
"settingsReset": "Pengaturan dikembalikan ke default"
},
"es": {
"ahaGlobalDesc": "Activar rasgo especial de Eón ★ Aha en todo el servidor",
"btnAddLc": "+ Añadir cono de luz",
"btnAddRelic": "+ Añadir reliquia",
"modalAddLcTitle": "🗡️ Añadir cono de luz a FreeSR",
"modalAddRelicTitle": "🛡️ Añadir reliquia a FreeSR",
"lblSelectLc": "Seleccionar cono de luz",
"lblSelectRelicSet": "Seleccionar conjunto de reliquias",
"lblRelicSlot": "Ranura de reliquia",
"lblMainAffix": "Atributo principal",
"lblEquipAvatar": "Equipar a personaje",
"unassigned": "-- Sin equipar (Inventario) --",
"btnAddLcSubmit": "💾 Añadir a FreeSR",
"btnAddRelicSubmit": "💾 Añadir a FreeSR",
"toastLcAdded": "Cono de luz añadido a FreeSR",
"toastRelicAdded": "Reliquia añadida a FreeSR",
"deleteItem": "Eliminar",
"toastItemDeleted": "Objeto eliminado correctamente",
"appName": "GaC-Srtool",
"appSubtitle": "Controlador de servidor y FreeSR de Star Rail",
"online": "Conectado",
@@ -1603,6 +1765,24 @@ export const TRANSLATIONS = {
"settingsReset": "Ajustes restablecidos por defecto"
},
"fr": {
"ahaGlobalDesc": "Activer le trait spécial d'Éon ★ Aha sur tout le serveur",
"btnAddLc": "+ Ajouter cône de lumière",
"btnAddRelic": "+ Ajouter relique",
"modalAddLcTitle": "🗡️ Ajouter cône de lumière à FreeSR",
"modalAddRelicTitle": "🛡️ Ajouter relique à FreeSR",
"lblSelectLc": "Sélectionner cône de lumière",
"lblSelectRelicSet": "Sélectionner ensemble de reliques",
"lblRelicSlot": "Emplacement de relique",
"lblMainAffix": "Statistique principale",
"lblEquipAvatar": "Équiper au personnage",
"unassigned": "-- Non équipé (Inventaire) --",
"btnAddLcSubmit": "💾 Ajouter à FreeSR",
"btnAddRelicSubmit": "💾 Ajouter à FreeSR",
"toastLcAdded": "Cône de lumière ajouté à FreeSR",
"toastRelicAdded": "Relique ajoutée à FreeSR",
"deleteItem": "Supprimer",
"toastItemDeleted": "Objet supprimé avec succès",
"appName": "GaC-Srtool",
"appSubtitle": "Contrôleur de serveur et FreeSR Star Rail",
"online": "Connecté",
@@ -1779,6 +1959,24 @@ export const TRANSLATIONS = {
"settingsReset": "Paramètres réinitialisés par défaut"
},
"de": {
"ahaGlobalDesc": "Serverweite Äon ★ Aha-Spezialmechanik aktivieren",
"btnAddLc": "+ Lichtkegel hinzufügen",
"btnAddRelic": "+ Relikt hinzufügen",
"modalAddLcTitle": "🗡️ Lichtkegel zu FreeSR hinzufügen",
"modalAddRelicTitle": "🛡️ Relikt zu FreeSR hinzufügen",
"lblSelectLc": "Lichtkegel auswählen",
"lblSelectRelicSet": "Reliktset auswählen",
"lblRelicSlot": "Reliktslot",
"lblMainAffix": "Hauptwert",
"lblEquipAvatar": "Figur ausrüsten",
"unassigned": "-- Nicht ausgerüstet (Inventar) --",
"btnAddLcSubmit": "💾 Zu FreeSR hinzufügen",
"btnAddRelicSubmit": "💾 Zu FreeSR hinzufügen",
"toastLcAdded": "Lichtkegel zu FreeSR hinzugefügt",
"toastRelicAdded": "Relikt zu FreeSR hinzugefügt",
"deleteItem": "Löschen",
"toastItemDeleted": "Gegenstand erfolgreich gelöscht",
"appName": "GaC-Srtool",
"appSubtitle": "Star Rail FreeSR- & Server-Controller",
"online": "Verbunden",
@@ -1955,6 +2153,24 @@ export const TRANSLATIONS = {
"settingsReset": "Auf Standardeinstellungen zurückgesetzt"
},
"ru": {
"ahaGlobalDesc": "Включить особый пассивный навык Эона ★ Аха по всему серверу",
"btnAddLc": "+ Добавить световой конус",
"btnAddRelic": "+ Добавить реликвию",
"modalAddLcTitle": "🗡️ Добавить световой конус в FreeSR",
"modalAddRelicTitle": "🛡️ Добавить реликвию в FreeSR",
"lblSelectLc": "Выберите световой конус",
"lblSelectRelicSet": "Выберите набор реликвий",
"lblRelicSlot": "Слот реликвии",
"lblMainAffix": "Основная характеристика",
"lblEquipAvatar": "Экипировать персонажу",
"unassigned": "-- Не экипировано (инвентарь) --",
"btnAddLcSubmit": "💾 Добавить в FreeSR",
"btnAddRelicSubmit": "💾 Добавить в FreeSR",
"toastLcAdded": "Световой конус добавлен в FreeSR",
"toastRelicAdded": "Реликвия добавлена в FreeSR",
"deleteItem": "Удалить",
"toastItemDeleted": "Предмет успешно удален",
"appName": "GaC-Srtool",
"appSubtitle": "Контроллер сервера и FreeSR Star Rail",
"online": "Подключено",
@@ -2131,6 +2347,24 @@ export const TRANSLATIONS = {
"settingsReset": "Настройки сброшены по умолчанию"
},
"pt": {
"ahaGlobalDesc": "Ativar habilidade passiva especial de Aeon ★ Aha em todo o servidor",
"btnAddLc": "+ Adicionar Cone de Luz",
"btnAddRelic": "+ Adicionar Relíquia",
"modalAddLcTitle": "🗡️ Adicionar Cone de Luz ao FreeSR",
"modalAddRelicTitle": "🛡️ Adicionar Relíquia ao FreeSR",
"lblSelectLc": "Selecionar Cone de Luz",
"lblSelectRelicSet": "Selecionar Conjunto de Relíquias",
"lblRelicSlot": "Espaço de Relíquia",
"lblMainAffix": "Atributo Principal",
"lblEquipAvatar": "Equipar ao Personagem",
"unassigned": "-- Não Equipado (Inventário) --",
"btnAddLcSubmit": "💾 Adicionar ao FreeSR",
"btnAddRelicSubmit": "💾 Adicionar ao FreeSR",
"toastLcAdded": "Cone de Luz adicionado ao FreeSR",
"toastRelicAdded": "Relíquia adicionada ao FreeSR",
"deleteItem": "Excluir",
"toastItemDeleted": "Item excluído com sucesso",
"appName": "GaC-Srtool",
"appSubtitle": "Controlador de Servidor e FreeSR de Star Rail",
"online": "Conectado",