diff --git a/assets/avatars/1014.png b/assets/avatars/1014.png new file mode 100644 index 0000000..4c13a8b Binary files /dev/null and b/assets/avatars/1014.png differ diff --git a/assets/avatars/1015.png b/assets/avatars/1015.png new file mode 100644 index 0000000..73e9980 Binary files /dev/null and b/assets/avatars/1015.png differ diff --git a/assets/avatars/1321.png b/assets/avatars/1321.png new file mode 100644 index 0000000..347a548 Binary files /dev/null and b/assets/avatars/1321.png differ diff --git a/assets/avatars/1406.png b/assets/avatars/1406.png new file mode 100644 index 0000000..c640d1a Binary files /dev/null and b/assets/avatars/1406.png differ diff --git a/assets/avatars/1412.png b/assets/avatars/1412.png new file mode 100644 index 0000000..27f8d95 Binary files /dev/null and b/assets/avatars/1412.png differ diff --git a/assets/avatars/1413.png b/assets/avatars/1413.png new file mode 100644 index 0000000..9e86251 Binary files /dev/null and b/assets/avatars/1413.png differ diff --git a/assets/avatars/1414.png b/assets/avatars/1414.png new file mode 100644 index 0000000..4d53880 Binary files /dev/null and b/assets/avatars/1414.png differ diff --git a/assets/avatars/1415.png b/assets/avatars/1415.png new file mode 100644 index 0000000..233fbc4 Binary files /dev/null and b/assets/avatars/1415.png differ diff --git a/assets/avatars/1501.png b/assets/avatars/1501.png new file mode 100644 index 0000000..0d043c6 Binary files /dev/null and b/assets/avatars/1501.png differ diff --git a/assets/avatars/1502.png b/assets/avatars/1502.png new file mode 100644 index 0000000..838f05f Binary files /dev/null and b/assets/avatars/1502.png differ diff --git a/assets/avatars/1503.png b/assets/avatars/1503.png new file mode 100644 index 0000000..10a19a2 Binary files /dev/null and b/assets/avatars/1503.png differ diff --git a/assets/avatars/1504.png b/assets/avatars/1504.png new file mode 100644 index 0000000..b584ddb Binary files /dev/null and b/assets/avatars/1504.png differ diff --git a/assets/avatars/1505.png b/assets/avatars/1505.png new file mode 100644 index 0000000..85509e1 Binary files /dev/null and b/assets/avatars/1505.png differ diff --git a/assets/avatars/1506.png b/assets/avatars/1506.png new file mode 100644 index 0000000..7216f8a Binary files /dev/null and b/assets/avatars/1506.png differ diff --git a/assets/avatars/1507.png b/assets/avatars/1507.png new file mode 100644 index 0000000..b218eb4 Binary files /dev/null and b/assets/avatars/1507.png differ diff --git a/assets/avatars/1508.png b/assets/avatars/1508.png new file mode 100644 index 0000000..325e407 Binary files /dev/null and b/assets/avatars/1508.png differ diff --git a/assets/avatars/1509.png b/assets/avatars/1509.png new file mode 100644 index 0000000..03f5f4a Binary files /dev/null and b/assets/avatars/1509.png differ diff --git a/assets/avatars/1510.png b/assets/avatars/1510.png new file mode 100644 index 0000000..64f50a0 Binary files /dev/null and b/assets/avatars/1510.png differ diff --git a/assets/avatars/1511.png b/assets/avatars/1511.png new file mode 100644 index 0000000..2434c6e Binary files /dev/null and b/assets/avatars/1511.png differ diff --git a/assets/avatars/1512.png b/assets/avatars/1512.png new file mode 100644 index 0000000..a502280 Binary files /dev/null and b/assets/avatars/1512.png differ diff --git a/assets/avatars/1513.png b/assets/avatars/1513.png new file mode 100644 index 0000000..1e55cbf Binary files /dev/null and b/assets/avatars/1513.png differ diff --git a/assets/avatars/8009.png b/assets/avatars/8009.png new file mode 100644 index 0000000..6cf0743 Binary files /dev/null and b/assets/avatars/8009.png differ diff --git a/assets/avatars/8010.png b/assets/avatars/8010.png new file mode 100644 index 0000000..e814da9 Binary files /dev/null and b/assets/avatars/8010.png differ diff --git a/css/styles.css b/css/styles.css index b1344f0..c9278f4 100644 --- a/css/styles.css +++ b/css/styles.css @@ -541,31 +541,42 @@ body { } .eidolon-pip { - width: 25px; - height: 25px; - border-radius: 4px; - border: 1px solid var(--border-color); - background: transparent; - color: var(--text-muted); - font-size: 0.75rem; + width: 28px; + height: 28px; + border-radius: 6px; + border: 1.5px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.05); + color: #cbd5e1; + font-size: 0.8rem; font-weight: 800; cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: var(--transition); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + user-select: none; } .eidolon-pip:hover { border-color: var(--accent-cyan); - color: var(--text-main); + background: rgba(56, 189, 248, 0.15); + color: #fff; + transform: translateY(-2px); } .eidolon-pip.active { background: linear-gradient(135deg, #0284c7, #38bdf8); border-color: #38bdf8; color: #0b0d14; - box-shadow: 0 0 10px rgba(56, 189, 248, 0.6); + box-shadow: 0 0 12px rgba(56, 189, 248, 0.6); + font-weight: 900; +} + +.modal-eidolon-pip { + width: 38px; + height: 38px; + font-size: 0.95rem; + border-radius: 8px; } /* ========================================================================== diff --git a/index.html b/index.html index ac6d348..506af81 100644 --- a/index.html +++ b/index.html @@ -53,7 +53,6 @@ - @@ -130,9 +129,6 @@ - @@ -303,63 +299,7 @@ - -
-
- -
-
📍 จุดวาร์ปปลอดภัยยอดนิยม (Safe Presets)
-

คลิกจุดวาร์ปเพื่อย้ายฉากของผู้เล่นในไฟล์เซฟได้ทันที

-
- -
-
- - -
-
🎯 กำหนดพิกัดวาร์ปเอง (Custom Teleport)
-

ใส่ PlaneID, FloorID และ EntryID ตามต้องการ

- -
-
-
- - -
-
- - -
-
-
- - -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - +
@@ -467,27 +407,53 @@
- +
+ +
+ + + + + +
+
- +
+ +
+ + + +
+
- - -
- -
diff --git a/js/app.js b/js/app.js index 41b61b2..e026bee 100644 --- a/js/app.js +++ b/js/app.js @@ -1,5 +1,4 @@ import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js'; -import { PRESET_TELEPORTS, getTeleportName } from './teleports-data.js'; import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js'; // Application State @@ -18,7 +17,7 @@ const state = { selectedAvatarDetail: null }; -// UI Elements +// UI Elements Map const el = { serverModeSelect: document.getElementById('server-mode-select'), settingServerMode: document.getElementById('setting-server-mode'), @@ -44,7 +43,6 @@ const el = { lblCoordinates: document.getElementById('lbl-coordinates'), lineupContainer: document.getElementById('lineup-container'), btnQuickMaxAll: document.getElementById('btn-quick-max-all'), - btnQuickWarpExpress: document.getElementById('btn-quick-warp-express'), btnQuickSyncPersistent: document.getElementById('btn-quick-sync-persistent'), // Avatars @@ -75,17 +73,7 @@ const el = { btnExrankEnableAll: document.getElementById('btn-exrank-enable-all'), btnExrankDisableAll: document.getElementById('btn-exrank-disable-all'), - // Teleport - teleportPresetsList: document.getElementById('teleport-presets-list'), - formCustomTeleport: document.getElementById('form-custom-teleport'), - tpPlane: document.getElementById('tp-plane'), - tpFloor: document.getElementById('tp-floor'), - tpEntry: document.getElementById('tp-entry'), - tpX: document.getElementById('tp-x'), - tpY: document.getElementById('tp-y'), - tpZ: document.getElementById('tp-z'), - - // JSON + // JSON Tab btnSelectFreesr: document.getElementById('btn-select-freesr'), btnSelectPersistent: document.getElementById('btn-select-persistent'), btnJsonBeautify: document.getElementById('btn-json-beautify'), @@ -111,6 +99,8 @@ const el = { modalCharLevel: document.getElementById('modal-char-level'), modalCharPromotion: document.getElementById('modal-char-promotion'), modalCharEidolonBtns: document.getElementById('modal-char-eidolon-btns'), + btnModalQuickE0: document.getElementById('btn-modal-quick-e0'), + btnModalQuickE6: document.getElementById('btn-modal-quick-e6'), btnModalMaxSkills: document.getElementById('btn-modal-max-skills'), btnModalSaveAvatar: document.getElementById('btn-modal-save-avatar'), @@ -129,22 +119,28 @@ function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast ${type}`; - let icon = 'ℹ️'; - if (type === 'success') icon = '✅'; - if (type === 'error') icon = '❌'; + const icons = { + info: 'ℹ️', + success: '✅', + warning: '⚠️', + error: '❌' + }; + + toast.innerHTML = ` + ${icons[type] || 'ℹ️'} + ${message} + `; - toast.innerHTML = `${icon}${message}`; el.toastContainer.appendChild(toast); - setTimeout(() => { toast.style.opacity = '0'; - toast.style.transform = 'translateY(10px)'; + toast.style.transform = 'translateX(100%)'; toast.style.transition = 'all 0.3s ease'; setTimeout(() => toast.remove(), 300); - }, 3500); + }, 4000); } -// API Fetch Helper +// Low-level API caller async function apiCall(endpoint, method = 'GET', data = null) { const cleanBase = state.apiUrl.replace(/\/+$/, ''); const url = `${cleanBase}${endpoint}`; @@ -162,7 +158,7 @@ async function apiCall(endpoint, method = 'GET', data = null) { try { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 8000); + const timeoutId = setTimeout(() => controller.abort(), 6000); options.signal = controller.signal; const res = await fetch(url, options); @@ -220,11 +216,10 @@ function applyTranslations() { elem.placeholder = t(key); }); - if (state.statusData) { - renderLineup(state.statusData.lineups); - renderExRanks(state.statusData.ex_ranks || []); + renderLineup(state.statusData?.lineups); + if (state.statusData?.ex_ranks) { + renderExRanks(state.statusData.ex_ranks); } - renderTeleportPresets(); if (state.avatarsList.length > 0) { renderAvatarsGrid(); } @@ -251,26 +246,23 @@ function setupLanguageSelector() { }); } -// Setup Server Mode Selector +// Server Mode Setup function setupServerModes() { const updateModeDisplay = (mode) => { state.serverMode = mode; localStorage.setItem('gac_srtool_mode', mode); el.serverModeSelect.value = mode; - el.settingServerMode.value = mode; + if (el.settingServerMode) el.settingServerMode.value = mode; if (mode === 'gac') { el.lblActiveMode.textContent = 'GaC-hkrpg-go (เซิร์ฟเวอร์หลัก / Full Control)'; - el.lblActiveMode.style.color = 'var(--accent-cyan)'; - el.tabBtnPersistent.style.display = 'inline-flex'; + if (el.tabBtnPersistent) el.tabBtnPersistent.style.display = 'inline-flex'; } else if (mode === 'firefly') { el.lblActiveMode.textContent = 'FireflyGo (FreeSR Compatible)'; - el.lblActiveMode.style.color = '#f87171'; - el.tabBtnPersistent.style.display = 'none'; + if (el.tabBtnPersistent) el.tabBtnPersistent.style.display = 'none'; } else if (mode === 'robin') { el.lblActiveMode.textContent = 'RobinSR (FreeSR Compatible)'; - el.lblActiveMode.style.color = '#c084fc'; - el.tabBtnPersistent.style.display = 'none'; + if (el.tabBtnPersistent) el.tabBtnPersistent.style.display = 'none'; } }; @@ -280,15 +272,28 @@ function setupServerModes() { refreshStatus(); }); - el.settingServerMode.addEventListener('change', (e) => { - updateModeDisplay(e.target.value); - showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info'); - refreshStatus(); - }); - updateModeDisplay(state.serverMode); } +// Initialize Avatars Database: Populates all known avatars from metadata +function initAvatarsList() { + if (state.avatarsList.length > 0) return; + const list = []; + for (const aidStr in AVATAR_METADATA) { + const aid = parseInt(aidStr, 10); + list.push({ + avatar_id: aid, + level: 80, + promotion: 6, + rank: 6 + }); + } + state.avatarsList = list; +} + +// Default lineup matching our persistent server data +const DEFAULT_LINEUP = { 0: 1511, 1: 1506, 2: 1502, 3: 1503 }; + // Refresh status from server async function refreshStatus() { try { @@ -296,7 +301,6 @@ async function refreshStatus() { if (state.serverMode === 'gac') { data = await apiCall('/api/status'); } else { - // FireflyGo / RobinSR fallback check const freesr = await apiCall('/api/freesr').catch(() => null); if (freesr && freesr.data) { data = { @@ -304,7 +308,7 @@ async function refreshStatus() { avatar_count: Object.keys(freesr.data.avatars || {}).length, lightcone_count: (freesr.data.lightcones || []).length, relic_count: (freesr.data.relics || []).length, - lineups: freesr.data.lineups || { 0: 8001 }, + lineups: freesr.data.lineups || DEFAULT_LINEUP, scene: freesr.data.scene, position: freesr.data.position, ex_ranks: [] @@ -318,7 +322,7 @@ async function refreshStatus() { setConnectionStatus(true); el.lblApiUrl.textContent = state.apiUrl; - el.statAvatars.textContent = data.avatar_count ?? 0; + el.statAvatars.textContent = data.avatar_count ?? state.avatarsList.length; el.statLightcones.textContent = data.lightcone_count ?? 0; el.statRelics.textContent = data.relic_count ?? 0; @@ -332,18 +336,25 @@ async function refreshStatus() { el.lblCoordinates.textContent = `X: ${x}, Y: ${y}, Z: ${z}`; } - renderLineup(data.lineups); + renderLineup(data.lineups || DEFAULT_LINEUP); renderExRanks(data.ex_ranks || []); + loadAvatars(); } catch (err) { - setConnectionStatus(false, t('connectFailed')); + setConnectionStatus(false, t('offline')); + el.lblApiUrl.textContent = state.apiUrl; + el.statAvatars.textContent = state.avatarsList.length; + // Keep rendering with current or default lineup so UI is always functional + renderLineup(state.statusData?.lineups || DEFAULT_LINEUP); } } // Render party lineup cards function renderLineup(lineups) { el.lineupContainer.innerHTML = ''; + const currentLineup = lineups || state.statusData?.lineups || DEFAULT_LINEUP; + for (let slot = 0; slot < 4; slot++) { - const avatarId = lineups ? (lineups[slot] || 0) : 0; + const avatarId = currentLineup[slot] || currentLineup[String(slot)] || 0; const slotCard = document.createElement('div'); slotCard.className = `lineup-slot-card ${avatarId ? 'filled' : ''}`; slotCard.title = 'คลิกเพื่อเลือกตัวละครลงตำแหน่งนี้'; @@ -363,7 +374,7 @@ function renderLineup(lineups) { src="./assets/avatars/${avatarId}.png" onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';}" alt="${info.name}"> -
${info.name}
+
${info.name}
${info.element} ${info.path} @@ -392,7 +403,7 @@ function renderPickerGrid() { const search = el.pickerSearch.value.trim().toLowerCase(); el.pickerAvatarsGrid.innerHTML = ''; - // Add Remove button + // Option to remove character from slot const removeCard = document.createElement('div'); removeCard.className = 'card'; removeCard.style.padding = '0.75rem'; @@ -432,7 +443,10 @@ function renderPickerGrid() {
${meta.name}
-
Lv. ${item.level} • E${item.rank}
+
+ ${meta.element} + Lv. ${item.level} • E${item.rank} +
`; @@ -449,38 +463,48 @@ function renderPickerGrid() { // Assign avatar to lineup slot async function assignAvatarToLineup(slot, avatarId) { try { - if (!state.statusData || !state.statusData.lineups) return; - const lineups = { ...state.statusData.lineups }; + state.statusData = state.statusData || {}; + const lineups = { ...(state.statusData.lineups || DEFAULT_LINEUP) }; lineups[slot] = avatarId; state.statusData.lineups = lineups; - if (state.serverMode === 'gac') { - await apiCall('/api/persistent', 'POST', { lineups }); - } else { - // In FreeSR standard mode - const freesr = await apiCall('/api/freesr'); - if (freesr && freesr.data) { - freesr.data.lineups = lineups; - await apiCall('/api/freesr', 'POST', freesr.data); + if (state.isConnected) { + if (state.serverMode === 'gac') { + await apiCall('/api/persistent', 'POST', { lineups }); + } else { + await saveFreesrChangesToServer(); } } - showToast(`อัปเดตทีม Slot ${slot + 1} เรียบร้อย`, 'success'); + renderLineup(lineups); + const name = avatarId ? getAvatarInfo(avatarId).name : 'ว่าง'; + showToast(`อัปเดตทีม Slot ${slot + 1} เป็น: ${name}`, 'success'); } catch (err) { - showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error'); + showToast(`เกิดข้อผิดพลาดในการเปลี่ยนทีม: ${err.message}`, 'error'); } } // Load and Render Avatars async function loadAvatars() { - try { - el.avatarsGrid.innerHTML = `
${t('checking')}
`; - const res = await apiCall('/api/freesr/avatars'); - state.avatarsList = res.avatars || []; - renderAvatarsGrid(); - } catch (err) { - el.avatarsGrid.innerHTML = `
${err.message}
`; + initAvatarsList(); + if (state.isConnected) { + try { + const res = await apiCall('/api/freesr/avatars'); + if (res && res.avatars && res.avatars.length > 0) { + res.avatars.forEach(serverAv => { + const found = state.avatarsList.find(a => a.avatar_id === serverAv.avatar_id); + if (found) { + found.level = serverAv.level; + found.promotion = serverAv.promotion; + found.rank = serverAv.rank; + } + }); + } + } catch (err) { + console.warn('Live avatar list fetch failed, using local list:', err); + } } + renderAvatarsGrid(); } function renderAvatarsGrid() { @@ -509,7 +533,7 @@ function renderAvatarsGrid() { card.className = `card avatar-card card-rarity-${meta.rarity}`; const pipsHtml = [0, 1, 2, 3, 4, 5, 6].map(rank => ` - `).join(''); @@ -537,14 +561,14 @@ function renderAvatarsGrid() {
- ${t('eidolonLabel')} + Eidolon:
${pipsHtml}
`; - // Eidolon button clicks + // Eidolon quick button clicks card.querySelectorAll('.eidolon-pip').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); @@ -554,7 +578,7 @@ function renderAvatarsGrid() { }); }); - // Clicking card opens full character build modal! + // Clicking card opens full character build modal card.addEventListener('click', () => { openAvatarDetailModal(item); }); @@ -584,26 +608,59 @@ function openAvatarDetailModal(avatarItem) { el.modalCharLevel.value = avatarItem.level || 80; el.modalCharPromotion.value = avatarItem.promotion || 6; - // Render modal eidolon pips + renderModalEidolonPips(avatarItem.rank ?? 6); + el.modalAvatarDetail.classList.add('active'); +} + +function renderModalEidolonPips(currentRank) { el.modalCharEidolonBtns.innerHTML = ''; for (let r = 0; r <= 6; r++) { const btn = document.createElement('button'); - btn.className = `eidolon-pip ${avatarItem.rank >= r ? 'active' : ''}`; - btn.textContent = r; - btn.style.width = '32px'; - btn.style.height = '32px'; - btn.style.fontSize = '0.9rem'; + btn.type = 'button'; + btn.className = `eidolon-pip modal-eidolon-pip ${currentRank >= r ? 'active' : ''}`; + btn.textContent = `E${r}`; + btn.title = `Eidolon ${r}`; btn.addEventListener('click', () => { - avatarItem.rank = r; - el.modalCharEidolonBtns.querySelectorAll('.eidolon-pip').forEach((b, idx) => { - if (idx <= r) b.classList.add('active'); - else b.classList.remove('active'); - }); + if (state.selectedAvatarDetail) { + state.selectedAvatarDetail.rank = r; + } + renderModalEidolonPips(r); }); el.modalCharEidolonBtns.appendChild(btn); } +} - el.modalAvatarDetail.classList.add('active'); +// Set avatar rank (Used by both card pips and modal) +async function setAvatarRank(avatarId, rank) { + const target = state.avatarsList.find(a => a.avatar_id === avatarId); + if (target) { + target.rank = rank; + } + if (state.fullFreesrData?.avatars?.[avatarId]) { + state.fullFreesrData.avatars[avatarId].data.rank = rank; + } + + renderAvatarsGrid(); + + if (state.selectedAvatarDetail && state.selectedAvatarDetail.avatar_id === avatarId) { + state.selectedAvatarDetail.rank = rank; + renderModalEidolonPips(rank); + } + + if (state.isConnected) { + try { + if (state.serverMode === 'gac') { + await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank }); + } else { + await saveFreesrChangesToServer(); + } + showToast(t('avatarRankSuccess', { id: avatarId, rank }), 'success'); + } catch (err) { + console.warn('Rank sync to server failed:', err); + } + } else { + showToast(`อัปเดต ${getAvatarInfo(avatarId).name} เป็น E${rank} (บันทึกในเครื่อง)`, 'info'); + } } // Save avatar details from modal @@ -614,26 +671,39 @@ async function saveAvatarDetails() { const promotion = parseInt(el.modalCharPromotion.value, 10) || 6; const rank = state.selectedAvatarDetail.rank ?? 6; - try { - await apiCall('/api/freesr/avatars', 'POST', { - avatar_id: aid, - level, - promotion, - rank - }); - showToast(`บันทึกตัวละคร ${aid} สำเร็จ (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success'); - el.modalAvatarDetail.classList.remove('active'); + state.selectedAvatarDetail.level = level; + state.selectedAvatarDetail.promotion = promotion; + state.selectedAvatarDetail.rank = rank; - // Update local state and re-render - const target = state.avatarsList.find(a => a.avatar_id === aid); - if (target) { - target.level = level; - target.promotion = promotion; - target.rank = rank; - renderAvatarsGrid(); + const target = state.avatarsList.find(a => a.avatar_id === aid); + if (target) { + target.level = level; + target.promotion = promotion; + target.rank = rank; + } + + if (state.fullFreesrData?.avatars?.[aid]) { + state.fullFreesrData.avatars[aid].level = level; + state.fullFreesrData.avatars[aid].promotion = promotion; + state.fullFreesrData.avatars[aid].data.rank = rank; + } + + renderAvatarsGrid(); + el.modalAvatarDetail.classList.remove('active'); + + if (state.isConnected) { + try { + if (state.serverMode === 'gac') { + await apiCall('/api/freesr/avatars', 'POST', { avatar_id: aid, level, promotion, rank }); + } else { + await saveFreesrChangesToServer(); + } + showToast(`บันทึกตัวละคร ${getAvatarInfo(aid).name} เรียบร้อย (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success'); + } catch (err) { + showToast(`บันทึกในเครื่องแล้ว (ส่งเซิร์ฟเวอร์ไม่ได้: ${err.message})`, 'warning'); } - } catch (err) { - showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error'); + } else { + showToast(`บันทึกตัวละคร ${getAvatarInfo(aid).name} เรียบร้อย (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success'); } } @@ -641,293 +711,331 @@ async function saveAvatarDetails() { async function maxSkillsForAvatar() { if (!state.selectedAvatarDetail) return; const aid = state.selectedAvatarDetail.avatar_id; - try { - // Read freesr data, max skill levels for this avatar, and push back - const freesr = await apiCall('/api/freesr'); - if (freesr && freesr.data && freesr.data.avatars && freesr.data.avatars[aid]) { - const av = freesr.data.avatars[aid]; - if (av.data && av.data.skills) { - for (const k in av.data.skills) { - av.data.skills[k] = 10; - } - } + el.modalCharLevel.value = 80; + el.modalCharPromotion.value = 6; + state.selectedAvatarDetail.level = 80; + state.selectedAvatarDetail.promotion = 6; + state.selectedAvatarDetail.rank = 6; + renderModalEidolonPips(6); + + if (state.fullFreesrData?.avatars?.[aid]) { + const av = state.fullFreesrData.avatars[aid]; + av.level = 80; + av.promotion = 6; + av.data.rank = 6; + if (av.data.skills) { + for (const k in av.data.skills) av.data.skills[k] = 10; + } + } + + showToast(`อัพเลเวล 80, เลื่อนขั้น 6, E6 และ Traces 10/10 สำหรับ ${getAvatarInfo(aid).name} สำเร็จ!`, 'success'); +} + +// Max all avatars +async function maxAllAvatars() { + if (!confirm(t('confirmMaxAll'))) return; + state.avatarsList.forEach(a => { + a.level = 80; + a.promotion = 6; + a.rank = 6; + }); + + if (state.fullFreesrData?.avatars) { + for (const aid in state.fullFreesrData.avatars) { + const av = state.fullFreesrData.avatars[aid]; av.level = 80; av.promotion = 6; av.data.rank = 6; - await apiCall('/api/freesr', 'POST', freesr.data); - showToast(`อัพสกิลและ Traces ทั้งหมดของตัวละคร ${aid} เป็น 10/10 สำเร็จ!`, 'success'); - loadAvatars(); + if (av.data.skills) { + for (const k in av.data.skills) av.data.skills[k] = 10; + } } - } catch (err) { - showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error'); + } + + renderAvatarsGrid(); + + if (state.isConnected) { + try { + if (state.serverMode === 'gac') { + await apiCall('/api/avatar/max_all', 'POST', {}); + } else { + await saveFreesrChangesToServer(); + } + showToast(t('maxAllSuccess'), 'success'); + refreshStatus(); + } catch (err) { + showToast(`ปรับในเครื่องสำเร็จ แต่ส่งเซิร์ฟเวอร์ไม่ได้: ${err.message}`, 'warning'); + } + } else { + showToast('ปรับ Max Lv.80, เลื่อนขั้น 6, E6 ตัวละครทั้งหมดเรียบร้อย (โหมดออฟไลน์)', 'success'); } } -// Set avatar rank API -async function setAvatarRank(avatarId, rank) { - try { - await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank }); - showToast(t('avatarRankSuccess', { id: avatarId, rank }), 'success'); - - const target = state.avatarsList.find(a => a.avatar_id === avatarId); - if (target) { - target.rank = rank; - renderAvatarsGrid(); +// Push local freesr changes to server +async function saveFreesrChangesToServer() { + if (!state.fullFreesrData) return; + if (state.serverMode === 'firefly' || state.serverMode === 'robin') { + try { + await apiCall('/srtools', 'POST', { data: state.fullFreesrData }); + } catch (e) { + await apiCall('/api/freesr', 'POST', state.fullFreesrData); } - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); - } -} - -// Max all avatars API -async function maxAllAvatars() { - if (!confirm(t('confirmMaxAll'))) return; - try { - await apiCall('/api/avatar/max_all', 'POST', {}); - showToast(t('maxAllSuccess'), 'success'); - loadAvatars(); - refreshStatus(); - } catch (err) { - showToast(`Error: ${err.message}`, 'error'); + } else { + await apiCall('/api/freesr', 'POST', state.fullFreesrData); } } // Load Equipment (Lightcones & Relics) async function loadEquipment() { try { - const res = await apiCall('/api/freesr'); - if (!res || !res.data) return; - state.fullFreesrData = res.data; + let lcs = []; + let relics = []; - const lcs = res.data.lightcones || []; - const relics = res.data.relics || []; + if (state.fullFreesrData) { + lcs = state.fullFreesrData.lightcones || []; + relics = state.fullFreesrData.relics || []; + } else if (state.isConnected) { + const res = await apiCall('/api/freesr'); + if (res && res.data) { + state.fullFreesrData = res.data; + lcs = res.data.lightcones || []; + relics = res.data.relics || []; + } + } el.badgeTotalLc.textContent = `${lcs.length} ชิ้น`; el.badgeTotalRelics.textContent = `${relics.length} ชิ้น`; // Render Lightcones el.lightconesList.innerHTML = ''; - lcs.slice(0, 100).forEach(lc => { - const item = document.createElement('div'); - item.className = 'card'; - item.style.padding = '0.75rem'; - item.style.display = 'flex'; - item.style.justifyContent = 'space-between'; - item.style.alignItems = 'center'; + if (lcs.length === 0) { + el.lightconesList.innerHTML = '
ยังไม่มีข้อมูลกรวยแสง (สามารถอัปโหลด freesr-data.json ในแท็บ JSON เพื่อดูได้)
'; + } else { + lcs.slice(0, 100).forEach(lc => { + const item = document.createElement('div'); + item.className = 'card'; + item.style.padding = '0.75rem'; + item.style.display = 'flex'; + item.style.justifyContent = 'space-between'; + item.style.alignItems = 'center'; - const equipped = lc.equip_avatar ? getAvatarInfo(lc.equip_avatar).name : 'ไม่ได้สวมใส่'; - item.innerHTML = ` -
-
Lightcone ID: ${lc.item_id} (UID: ${lc.internal_uid})
-
- Lv. ${lc.level} • ขัดเกลา S${lc.rank} • สวมใส่โดย: ${equipped} + const equipped = lc.equip_avatar ? getAvatarInfo(lc.equip_avatar).name : 'ไม่ได้สวมใส่'; + item.innerHTML = ` +
+
Lightcone ID: ${lc.item_id} (UID: ${lc.internal_uid})
+
+ Lv. ${lc.level} • ขัดเกลา S${lc.rank} • สวมใส่โดย: ${equipped} +
-
- `; - el.lightconesList.appendChild(item); - }); + `; + el.lightconesList.appendChild(item); + }); + } // Render Relics el.relicsList.innerHTML = ''; - relics.slice(0, 100).forEach(r => { - const item = document.createElement('div'); - item.className = 'card'; - item.style.padding = '0.75rem'; - item.style.display = 'flex'; - item.style.justifyContent = 'space-between'; - item.style.alignItems = 'center'; + if (relics.length === 0) { + el.relicsList.innerHTML = '
ยังไม่มีข้อมูลรีลิกส์ (สามารถอัปโหลด freesr-data.json ในแท็บ JSON เพื่อดูได้)
'; + } else { + relics.slice(0, 100).forEach(r => { + const item = document.createElement('div'); + item.className = 'card'; + item.style.padding = '0.75rem'; + item.style.display = 'flex'; + item.style.justifyContent = 'space-between'; + item.style.alignItems = 'center'; - const equipped = r.equip_avatar ? getAvatarInfo(r.equip_avatar).name : 'ไม่ได้สวมใส่'; - const slotNum = r.relic_id % 10; - item.innerHTML = ` -
-
Relic ID: ${r.relic_id} (Slot: ${slotNum})
-
- Lv. ${r.level} • สเตตัสหลัก: ${r.main_affix_id} • สวมใส่โดย: ${equipped} + const equipped = r.equip_avatar ? getAvatarInfo(r.equip_avatar).name : 'ไม่ได้สวมใส่'; + item.innerHTML = ` +
+
Relic ID: ${r.item_id} (UID: ${r.internal_uid})
+
+ Lv. ${r.level} • สวมใส่โดย: ${equipped} +
-
- `; - el.relicsList.appendChild(item); - }); - + `; + el.relicsList.appendChild(item); + }); + } } catch (err) { - showToast(`โหลดคลังอุปกรณ์ไม่สำเร็จ: ${err.message}`, 'error'); + console.warn('Equipment loading error:', err); } } -// Load Persistent Save Settings +// Load Persistent Save Settings (Exclusive to GaC-hkrpg-go) async function loadPersistentSettings() { + if (state.serverMode !== 'gac') return; try { const res = await apiCall('/api/persistent'); - if (!res || !res.persistent) return; - const p = res.persistent; - state.persistentData = p; + if (!res || !res.data) return; + state.persistentData = res.data; - if (p.main_character) { - el.persistentMcSelect.value = p.main_character; + if (res.data.main_character) { + el.persistentMcSelect.value = res.data.main_character; } - if (p.march_type) { - el.persistentMarchSelect.value = p.march_type; + if (res.data.march_type) { + el.persistentMarchSelect.value = res.data.march_type; } - el.toggleSwGlobal.checked = !!p.enable_sw_global; - el.toggleCastoriceGlobal.checked = !!p.enable_castorice_global; + + el.toggleSwGlobal.checked = res.data.enable_sw_global !== false; + el.toggleCastoriceGlobal.checked = res.data.enable_castorice_global !== false; } catch (err) { - console.warn('Persistent settings not available in current mode:', err.message); + console.warn('Persistent settings loading error:', err); } } -// Render Ex-Ranks +// Render Ex-Rank Cards function renderExRanks(exList) { + if (!el.exrankList) return; + el.exrankList.innerHTML = ''; if (!exList || exList.length === 0) { - el.exrankList.innerHTML = '
ไม่มีข้อมูลตัวละคร Ex-Rank
'; + el.exrankList.innerHTML = '
ไม่มีข้อมูล Ex-Rank
'; return; } - el.exrankList.innerHTML = ''; - exList.forEach(item => { - const card = document.createElement('div'); - card.className = 'ex-switch-card'; - const localizedMeta = getAvatarInfo(item.avatar_id); - - card.innerHTML = ` -
-
- ${localizedMeta.name} - -
-
-
${localizedMeta.name}
-
- Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id} -
-
-
- - `; - - card.querySelector('input[type="checkbox"]').addEventListener('change', async (e) => { - const unlocked = e.target.checked; - const avatarId = item.avatar_id; - try { - await apiCall('/api/exrank/toggle', 'POST', { avatar_id: avatarId, unlocked }); - const statusText = unlocked ? t('enabled') : t('disabled'); - showToast(t('exrankToggleSuccess', { name: localizedMeta.name, status: statusText }), 'success'); - } catch (err) { - e.target.checked = !unlocked; - showToast(`Error: ${err.message}`, 'error'); - } - }); - - el.exrankList.appendChild(card); - }); -} - -// Render Teleport Presets -function renderTeleportPresets() { - el.teleportPresetsList.innerHTML = ''; - PRESET_TELEPORTS.forEach(preset => { + exList.forEach(ex => { const item = document.createElement('div'); - item.className = 'card'; - item.style.padding = '0.85rem'; + item.className = 'card exrank-item'; + item.style.padding = '1.25rem'; item.style.display = 'flex'; item.style.justifyContent = 'space-between'; item.style.alignItems = 'center'; + item.style.flexWrap = 'wrap'; + item.style.gap = '1rem'; - const displayName = getTeleportName(preset); + const info = getAvatarInfo(ex.avatar_id); + const elemClass = getElementClass(info.element); item.innerHTML = ` -
-
${displayName}
-
- Plane: ${preset.plane_id} • Floor: ${preset.floor_id} • Entry: ${preset.entry_id} +
+
+ ${info.name} +
+
+
${info.name}
+
+ ${info.element} + ${info.path} + Ex-Rank ID: ${ex.ex_rank_id} +
- +
+ +
`; - item.querySelector('.btn-warp').addEventListener('click', async () => { - await executeTeleport({ - plane_id: preset.plane_id, - floor_id: preset.floor_id, - entry_id: preset.entry_id, - x: preset.x, - y: preset.y, - z: preset.z - }, displayName); + item.querySelector('.btn-toggle-ex').addEventListener('click', async (e) => { + const aid = parseInt(e.target.getAttribute('data-avatar'), 10); + const cur = e.target.getAttribute('data-unlocked') === 'true'; + await toggleSingleExRank(aid, !cur); }); - el.teleportPresetsList.appendChild(item); + el.exrankList.appendChild(item); }); } -// Execute Teleport API -async function executeTeleport(payload, label = '') { +async function toggleSingleExRank(avatarId, unlock) { try { - await apiCall('/api/teleport', 'POST', payload); - showToast(t('warpSuccess', { label: label || `Plane ${payload.plane_id}` }), 'success'); + await apiCall('/api/exrank/toggle', 'POST', { avatar_id: avatarId, unlocked: unlock }); + showToast(unlock ? 'ปลดล็อค Ex-Rank สำเร็จ!' : 'ปิดใช้งาน Ex-Rank เรียบร้อย', 'success'); refreshStatus(); } catch (err) { - showToast(t('warpFailed', { err: err.message }), 'error'); + showToast(`Error: ${err.message}`, 'error'); } } -// Load and Switch JSON +// Load and Switch JSON Tab Data async function loadJsonData(mode) { state.currentJsonMode = mode; if (mode === 'freesr') { el.btnSelectFreesr.className = 'btn btn-sm btn-primary active'; el.btnSelectPersistent.className = 'btn btn-sm btn-secondary'; - el.jsonStatusInfo.textContent = t('jsonStatusOpening', { name: 'freesr-data.json' }); + el.jsonStatusInfo.textContent = 'กำลังโหลด freesr-data.json...'; try { - const res = await apiCall('/api/freesr'); - el.jsonEditor.value = JSON.stringify(res.data, null, 2); + if (state.fullFreesrData) { + el.jsonEditor.value = JSON.stringify(state.fullFreesrData, null, 2); + el.jsonStatusInfo.textContent = 'freesr-data.json (Loaded from memory)'; + } else { + const res = await apiCall('/api/freesr'); + state.fullFreesrData = res.data; + el.jsonEditor.value = JSON.stringify(res.data, null, 2); + el.jsonStatusInfo.textContent = 'freesr-data.json (Loaded from server)'; + } } catch (err) { - showToast(`Load failed: ${err.message}`, 'error'); + el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`; + if (!el.jsonEditor.value) { + el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2); + } } } else { - el.btnSelectPersistent.className = 'btn btn-sm btn-primary active'; el.btnSelectFreesr.className = 'btn btn-sm btn-secondary'; - el.jsonStatusInfo.textContent = t('jsonStatusOpening', { name: 'persistent' }); + el.btnSelectPersistent.className = 'btn btn-sm btn-primary active'; + el.jsonStatusInfo.textContent = 'กำลังโหลด persistent...'; try { - const res = await apiCall('/api/persistent'); - el.jsonEditor.value = JSON.stringify(res.persistent, null, 2); + if (state.persistentData) { + el.jsonEditor.value = JSON.stringify(state.persistentData, null, 2); + el.jsonStatusInfo.textContent = 'persistent (Loaded from memory)'; + } else { + const res = await apiCall('/api/persistent'); + state.persistentData = res.data; + el.jsonEditor.value = JSON.stringify(res.data, null, 2); + el.jsonStatusInfo.textContent = 'persistent (Loaded from server)'; + } } catch (err) { - showToast(`Load failed: ${err.message}`, 'error'); + el.jsonStatusInfo.textContent = `Offline: สามารถพิมพ์หรือวาง JSON เองได้`; + if (!el.jsonEditor.value) { + el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2); + } } } } -// Save JSON to Server +// Save JSON Tab Data async function saveJsonData() { - const text = el.jsonEditor.value.trim(); - if (!text) { - showToast('ไม่มีข้อมูล JSON ให้บันทึก', 'error'); - return; - } - - let parsed; try { - parsed = JSON.parse(text); - } catch (err) { - showToast(`${t('jsonInvalid')}: ${err.message}`, 'error'); - return; - } - - const endpoint = state.currentJsonMode === 'freesr' ? '/api/freesr' : '/api/persistent'; - try { - await apiCall(endpoint, 'POST', parsed); - showToast(t('jsonSaveSuccess', { name: state.currentJsonMode }), 'success'); + const parsed = JSON.parse(el.jsonEditor.value); + if (state.currentJsonMode === 'freesr') { + state.fullFreesrData = parsed; + if (state.isConnected) { + await saveFreesrChangesToServer(); + } + // Sync loaded characters to local list + if (parsed.avatars) { + for (const aid in parsed.avatars) { + const av = parsed.avatars[aid]; + const found = state.avatarsList.find(a => a.avatar_id === parseInt(aid, 10)); + if (found) { + found.level = av.level || 80; + found.promotion = av.promotion || 6; + found.rank = av.data?.rank ?? 6; + } + } + renderAvatarsGrid(); + } + showToast('บันทึก freesr-data.json เรียบร้อย', 'success'); + } else { + state.persistentData = parsed; + if (state.isConnected && state.serverMode === 'gac') { + await apiCall('/api/persistent', 'POST', parsed); + } + if (parsed.lineups) { + state.statusData = state.statusData || {}; + state.statusData.lineups = parsed.lineups; + renderLineup(parsed.lineups); + } + showToast('บันทึกไฟล์ persistent เรียบร้อย', 'success'); + } refreshStatus(); } catch (err) { - showToast(`Save failed: ${err.message}`, 'error'); + showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error'); } } @@ -947,8 +1055,8 @@ function setupTabs() { state.activeTab = targetId; // On-demand tab refresh - if (targetId === 'tab-avatars' && state.avatarsList.length === 0) { - loadAvatars(); + if (targetId === 'tab-avatars') { + renderAvatarsGrid(); } else if (targetId === 'tab-equipment') { loadEquipment(); } else if (targetId === 'tab-persistent') { @@ -961,7 +1069,7 @@ function setupTabs() { }); } -// Event Listeners setup +// Setup Event Listeners function setupEvents() { // Quick Ping & Refresh el.btnQuickPing.addEventListener('click', () => { @@ -984,7 +1092,7 @@ function setupEvents() { state.apiUrl = newUrl; localStorage.setItem('gac_srtool_api_url', newUrl); el.modalSettings.classList.remove('active'); - showToast(t('settingsSaved'), 'info'); + showToast(t('settingsSaved'), 'success'); refreshStatus(); } }); @@ -994,18 +1102,43 @@ function setupEvents() { el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails); el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar); + // Quick Preset Buttons for Character Modal + document.querySelectorAll('.btn-lvl-preset').forEach(btn => { + btn.addEventListener('click', () => { + const lvl = parseInt(btn.getAttribute('data-lvl'), 10); + el.modalCharLevel.value = lvl; + if (lvl > 70) el.modalCharPromotion.value = 6; + else if (lvl > 60) el.modalCharPromotion.value = 5; + else if (lvl > 50) el.modalCharPromotion.value = 4; + else if (lvl > 40) el.modalCharPromotion.value = 3; + else if (lvl > 20) el.modalCharPromotion.value = 1; + else el.modalCharPromotion.value = 0; + }); + }); + + document.querySelectorAll('.btn-prom-preset').forEach(btn => { + btn.addEventListener('click', () => { + el.modalCharPromotion.value = btn.getAttribute('data-prom'); + }); + }); + + el.btnModalQuickE0?.addEventListener('click', () => { + if (state.selectedAvatarDetail) state.selectedAvatarDetail.rank = 0; + renderModalEidolonPips(0); + }); + + el.btnModalQuickE6?.addEventListener('click', () => { + if (state.selectedAvatarDetail) state.selectedAvatarDetail.rank = 6; + renderModalEidolonPips(6); + }); + // Lineup Picker Modal Controls el.btnClosePickerModal.addEventListener('click', () => el.modalLineupPicker.classList.remove('active')); el.pickerSearch.addEventListener('input', renderPickerGrid); - // Quick Action Buttons + // Quick Action Buttons on Dashboard el.btnQuickMaxAll.addEventListener('click', maxAllAvatars); el.btnMaxAllAvatars.addEventListener('click', maxAllAvatars); - - el.btnQuickWarpExpress.addEventListener('click', () => { - const express = PRESET_TELEPORTS[0]; - executeTeleport(express, getTeleportName(express)); - }); el.btnQuickSyncPersistent.addEventListener('click', async () => { try { @@ -1077,20 +1210,6 @@ function setupEvents() { } }); - // Custom Teleport Form - el.formCustomTeleport.addEventListener('submit', (e) => { - e.preventDefault(); - const payload = { - plane_id: parseInt(el.tpPlane.value, 10), - floor_id: parseInt(el.tpFloor.value, 10), - entry_id: parseInt(el.tpEntry.value, 10), - x: el.tpX.value ? parseInt(el.tpX.value, 10) : 0, - y: el.tpY.value ? parseInt(el.tpY.value, 10) : 0, - z: el.tpZ.value ? parseInt(el.tpZ.value, 10) : 0 - }; - executeTeleport(payload, `Plane ${payload.plane_id}`); - }); - // JSON Tab Controls el.btnSelectFreesr.addEventListener('click', () => loadJsonData('freesr')); el.btnSelectPersistent.addEventListener('click', () => loadJsonData('persistent')); @@ -1125,7 +1244,30 @@ function setupEvents() { try { const parsed = JSON.parse(event.target.result); el.jsonEditor.value = JSON.stringify(parsed, null, 2); - showToast(`โหลดเนื้อหาไฟล์ ${file.name} เรียบร้อย`, 'success'); + + // If freesr-data.json was uploaded + if (parsed.avatars) { + state.fullFreesrData = parsed; + for (const aid in parsed.avatars) { + const av = parsed.avatars[aid]; + const found = state.avatarsList.find(a => a.avatar_id === parseInt(aid, 10)); + if (found) { + found.level = av.level || 80; + found.promotion = av.promotion || 6; + found.rank = av.data?.rank ?? 6; + } + } + renderAvatarsGrid(); + } + + // If persistent or has lineups + if (parsed.lineups) { + state.statusData = state.statusData || {}; + state.statusData.lineups = parsed.lineups; + renderLineup(parsed.lineups); + } + + showToast(`โหลดไฟล์ ${file.name} สำเร็จและซิงค์ข้อมูลแล้ว`, 'success'); } catch (err) { showToast(t('jsonInvalid'), 'error'); } @@ -1159,9 +1301,27 @@ function setupEvents() { showToast(t('settingsReset'), 'info'); refreshStatus(); }); + + // Modal Backdrop Click to close + [el.modalAvatarDetail, el.modalLineupPicker, el.modalSettings].forEach(modal => { + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.classList.remove('active'); + } + }); + } + }); + + // ESC Key to close any active modal + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + document.querySelectorAll('.modal-overlay.active').forEach(m => m.classList.remove('active')); + } + }); } -// Background ping loop +// Background Ping Loop function startPingLoop() { setInterval(() => { refreshStatus(); @@ -1170,13 +1330,14 @@ function startPingLoop() { // Initialize Application function init() { + initAvatarsList(); setupServerModes(); setupLanguageSelector(); setupTabs(); setupEvents(); applyTranslations(); refreshStatus(); - loadAvatars(); + renderAvatarsGrid(); startPingLoop(); } diff --git a/js/avatars-data.js b/js/avatars-data.js index c501f50..c896cf2 100644 --- a/js/avatars-data.js +++ b/js/avatars-data.js @@ -534,6 +534,144 @@ export const AVATAR_METADATA = { path: "Remembrance", element: "Wind", rarity: 5 + }, + 1014: { + names: { en: "Saber", th: "Saber (เซเบอร์)", "zh-CN": "阿尔托莉雅·潘德拉贡 (塞巴)", "zh-TW": "阿爾托莉雅·潘德拉貢", ja: "セイバー", ko: "세이버", ru: "Сэйбер" }, + path: "Destruction", + element: "Wind", + rarity: 5 + }, + 1015: { + names: { en: "Archer", th: "Archer (อาร์เชอร์)", "zh-CN": "卫宫士郎 (红A/阿茶)", "zh-TW": "衛宮士郎 (紅A)", ja: "アーチャー", ko: "아처", ru: "Арчер" }, + path: "Hunt", + element: "Quantum", + rarity: 5 + }, + 1321: { + names: { en: "Dahlia", th: "Dahlia (ดาเลีย)", "zh-CN": "达莉亚", "zh-TW": "達莉亞", ja: "ダリア", ko: "달리아", ru: "Далия" }, + path: "Nihility", + element: "Fire", + rarity: 5 + }, + 1406: { + names: { en: "Cipher", th: "Cipher (ไซเฟอร์)", "zh-CN": "赛弗", "zh-TW": "賽弗", ja: "サイファー", ko: "사이퍼", ru: "Сайфер" }, + path: "Nihility", + element: "Quantum", + rarity: 5 + }, + 1412: { + names: { en: "Cerydra", th: "Cerydra (เซอรีดรา)", "zh-CN": "塞利德拉", "zh-TW": "塞利德拉", ja: "セリドラ", ko: "세리드라", ru: "Серидра" }, + path: "Harmony", + element: "Wind", + rarity: 5 + }, + 1413: { + names: { en: "Evernight", th: "Evernight (เอเวอร์ไนท์)", "zh-CN": "永夜", "zh-TW": "永夜", ja: "エバーナイト", ko: "에버나이트", ru: "Эвернайт" }, + path: "Remembrance", + element: "Ice", + rarity: 5 + }, + 1414: { + names: { en: "Dan Heng • Permeating Thunder", th: "Dan Heng PT (ดันเฮงร่างใหม่)", "zh-CN": "丹恒·行者", "zh-TW": "丹恆·行者", ja: "丹恒・行者", ko: "단항·행자", ru: "Дань Хэн • Странник" }, + path: "Preservation", + element: "Physical", + rarity: 5 + }, + 1415: { + names: { en: "Cyrene", th: "Cyrene (ซีรีน)", "zh-CN": "赛琳", "zh-TW": "賽琳", ja: "サイリーン", ko: "키레네", ru: "Кирена" }, + path: "Remembrance", + element: "Ice", + rarity: 5 + }, + 1501: { + names: { en: "Sparxie", th: "Sparxie (สปาร์กซี่)", "zh-CN": "斯帕克茜", "zh-TW": "斯帕克茜", ja: "スパーキー", ko: "스팍시", ru: "Спаркси" }, + path: "Elation", + element: "Fire", + rarity: 5 + }, + 1502: { + names: { en: "Yaoguang", th: "Yaoguang (เย่ากวง)", "zh-CN": "瑶光", "zh-TW": "瑤光", ja: "瑤光", ko: "요광", ru: "Яогуан" }, + path: "Elation", + element: "Physical", + rarity: 5 + }, + 1503: { + names: { en: "Pearl", th: "Pearl (เพิร์ล)", "zh-CN": "佩尔 (Pearl)", "zh-TW": "佩爾", ja: "パール", ko: "펄", ru: "Перл" }, + path: "Elation", + element: "Ice", + rarity: 5 + }, + 1504: { + names: { en: "Ashveil", th: "Ashveil (แอชเวล)", "zh-CN": "灰纱", "zh-TW": "灰紗", ja: "アッシュヴェイル", ko: "애쉬베일", ru: "Эшвейл" }, + path: "Hunt", + element: "Lightning", + rarity: 5 + }, + 1505: { + names: { en: "Evanescia", th: "Evanescia (เอวาเนสเซีย)", "zh-CN": "埃瓦内西亚", "zh-TW": "埃瓦內西亞", ja: "エヴァネシア", ko: "에바네시아", ru: "Эванесия" }, + path: "Elation", + element: "Physical", + rarity: 5 + }, + 1506: { + names: { en: "Silver Wolf Lv.999", th: "Silver Wolf Lv.999 (ซิลเวอร์วูล์ฟ Lv.999)", "zh-CN": "银狼 Lv.999", "zh-TW": "銀狼 Lv.999", ja: "銀狼 Lv.999", ko: "은랑 Lv.999", ru: "Серебряный Волк Lv.999" }, + path: "Elation", + element: "Imaginary", + rarity: 5 + }, + 1507: { + names: { en: "Mortenax Blade", th: "Mortenax Blade (มอร์เทแนกซ์ เบลด)", "zh-CN": "摩天之刃", "zh-TW": "摩天之刃", ja: "摩天の刃", ko: "모르테낙스 블레이드", ru: "Мортенакс Блейд" }, + path: "Nihility", + element: "Fire", + rarity: 5 + }, + 1508: { + names: { en: "Tohsaka Rin", th: "Tohsaka Rin (โทซากะ ริน)", "zh-CN": "远坂凛", "zh-TW": "遠坂凜", ja: "遠坂凛", ko: "토오사카 린", ru: "Рин Тосака" }, + path: "Erudition", + element: "Quantum", + rarity: 5 + }, + 1509: { + names: { en: "Gilgamesh", th: "Gilgamesh (กิลกาเมช)", "zh-CN": "吉尔伽美什 (金闪闪)", "zh-TW": "吉爾伽美什", ja: "ギルガメッシュ", ko: "길가메시", ru: "Гильгамеш" }, + path: "Destruction", + element: "Lightning", + rarity: 5 + }, + 1510: { + names: { en: "Himeko Nova", th: "Himeko Nova (ฮิเมโกะ โนวา)", "zh-CN": "姬子·新星", "zh-TW": "姬子·新星", ja: "姫子・ノヴァ", ko: "히메코 노바", ru: "Химеко Нова" }, + path: "Erudition", + element: "Fire", + rarity: 5 + }, + 1511: { + names: { en: "Nihilux", th: "Nihilux (นิฮิลักซ์)", "zh-CN": "虚无之神·尼希卢克斯", "zh-TW": "虛無之神·尼希盧克斯", ja: "ニヒラックス", ko: "니힐룩스", ru: "Нихилюкс" }, + path: "Elation", + element: "Quantum", + rarity: 5 + }, + 1512: { + names: { en: "Robin • Summeretto", th: "Robin • Summeretto (โรบิน ซัมเมอร์)", "zh-CN": "知更鸟·夏日", "zh-TW": "知更鳥·夏日", ja: "ロビン・サマー", ko: "로빈·서머", ru: "Зарянка • Лето" }, + path: "Remembrance", + element: "Wind", + rarity: 5 + }, + 1513: { + names: { en: "Aventurine • Waveflair", th: "Aventurine • Waveflair (อเวนทูรีน คลื่นทะเล)", "zh-CN": "砂金·夏日浪潮", "zh-TW": "砂金·夏日浪潮", ja: "アベンチュリン・サマー", ko: "어벤츄린·웨이브", ru: "Авантюрин • Волна" }, + path: "Elation", + element: "Quantum", + rarity: 5 + }, + 8009: { + names: { en: "Trailblazer (Elation ♂)", th: "ผู้บุกเบิก (ปีติ ♂)", "zh-CN": "开拓者 (欢愉 ♂)", "zh-TW": "開拓者 (歡愉 ♂)", ja: "開拓者 (愉悦 ♂)", ko: "개척자 (환락 ♂)", ru: "Первопроходец (Радость ♂)" }, + path: "Elation", + element: "Lightning", + rarity: 5 + }, + 8010: { + names: { en: "Trailblazer (Elation ♀)", th: "ผู้บุกเบิก (ปีติ ♀)", "zh-CN": "开拓者 (欢愉 ♀)", "zh-TW": "開拓者 (歡愉 ♀)", ja: "開拓者 (愉悦 ♀)", ko: "개척자 (환락 ♀)", ru: "Первопроходец (Радость ♀)" }, + path: "Elation", + element: "Lightning", + rarity: 5 } }; diff --git a/js/i18n.js b/js/i18n.js index 292fbd5..f6c7402 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -119,7 +119,8 @@ export const TRANSLATIONS = { Preservation: "อนุรักษ์", Abundance: "เฟื่องฟู", Remembrance: "ความทรงจำ", - Unknown: "ไม่ระบุ" + Unknown: "ไม่ระบุ", + Elation: "ปีติ (Elation)" } }, @@ -215,7 +216,8 @@ export const TRANSLATIONS = { Preservation: "Preservation", Abundance: "Abundance", Remembrance: "Remembrance", - Unknown: "Unknown" + Unknown: "Unknown", + Elation: "Elation" } }, @@ -311,7 +313,8 @@ export const TRANSLATIONS = { Preservation: "存护", Abundance: "丰饶", Remembrance: "记忆", - Unknown: "未知" + Unknown: "未知", + Elation: "欢愉" } }, @@ -407,7 +410,8 @@ export const TRANSLATIONS = { Preservation: "存護", Abundance: "豐饒", Remembrance: "記憶", - Unknown: "未知" + Unknown: "未知", + Elation: "歡愉" } }, @@ -503,7 +507,8 @@ export const TRANSLATIONS = { Preservation: "存護", Abundance: "豊穣", Remembrance: "記憶", - Unknown: "不明" + Unknown: "不明", + Elation: "愉悦" } }, @@ -599,7 +604,8 @@ export const TRANSLATIONS = { Preservation: "보존", Abundance: "풍요", Remembrance: "기억", - Unknown: "알 수 없음" + Unknown: "알 수 없음", + Elation: "환락" } }, @@ -695,7 +701,8 @@ export const TRANSLATIONS = { Preservation: "Bảo Hộ", Abundance: "Trù Phú", Remembrance: "Ký Ức", - Unknown: "Không xác định" + Unknown: "Không xác định", + Elation: "Vui Vẻ" } }, @@ -791,7 +798,8 @@ export const TRANSLATIONS = { Preservation: "Pelestarian", Abundance: "Kelimpahan", Remembrance: "Ingatan", - Unknown: "Tidak Diketahui" + Unknown: "Tidak Diketahui", + Elation: "Kegembiraan" } }, @@ -887,7 +895,8 @@ export const TRANSLATIONS = { Preservation: "Conservación", Abundance: "Abundancia", Remembrance: "Reminiscencia", - Unknown: "Desconocido" + Unknown: "Desconocido", + Elation: "Exultación" } }, @@ -983,7 +992,8 @@ export const TRANSLATIONS = { Preservation: "Préservation", Abundance: "Abondance", Remembrance: "Souvenir", - Unknown: "Inconnu" + Unknown: "Inconnu", + Elation: "Allégresse" } }, @@ -1079,7 +1089,8 @@ export const TRANSLATIONS = { Preservation: "Bewahrung", Abundance: "Überfluss", Remembrance: "Erinnerung", - Unknown: "Unbekannt" + Unknown: "Unbekannt", + Elation: "Frohsinn" } }, @@ -1175,7 +1186,8 @@ export const TRANSLATIONS = { Preservation: "Сохранение", Abundance: "Изобилие", Remembrance: "Память", - Unknown: "Неизвестно" + Unknown: "Неизвестно", + Elation: "Радость" } }, @@ -1271,7 +1283,8 @@ export const TRANSLATIONS = { Preservation: "Preservação", Abundance: "Abundância", Remembrance: "Recordação", - Unknown: "Desconhecido" + Unknown: "Desconhecido", + Elation: "Alegria" } } }; diff --git a/js/teleports-data.js b/js/teleports-data.js deleted file mode 100644 index f006a30..0000000 --- a/js/teleports-data.js +++ /dev/null @@ -1,128 +0,0 @@ -import { getCurrentLanguage } from './i18n.js'; - -export const PRESET_TELEPORTS = [ - { - names: { - th: "Astral Express - ขบวนรถไฟแอสทรัล (ห้องรับรอง)", - en: "Astral Express - Parlor Car", - "zh-CN": "星穹列车 - 观景车厢", - "zh-TW": "星穹列車 - 觀景車廂", - ja: "星穹列車 - 客室", - ko: "은하열차 - 객실", - ru: "Звёздный экспресс - Общий вагон" - }, - plane_id: 20001, - floor_id: 20001001, - entry_id: 2000101, - x: 0, - y: 0, - z: 0 - }, - { - names: { - th: "สถานีอวกาศ Herta - โซนควบคุมหลัก (Master Control Zone)", - en: "Herta Space Station - Master Control Zone", - "zh-CN": "空间站「黑塔」 - 主控舱段", - "zh-TW": "太空站「黑塔」 - 主控艙段", - ja: "宇宙ステーション「ヘルタ」 - 主制御部分", - ko: "우주정거장 「헤르타」 - 메인 컨트롤 구역", - ru: "Космическая станция «Герта» - Главная контрольная зона" - }, - plane_id: 10001, - floor_id: 10001001, - entry_id: 1000101, - x: 0, - y: 0, - z: 0 - }, - { - names: { - th: "Jarilo-VI (Belobog) - เขตการปกครอง (Administrative District)", - en: "Jarilo-VI - Administrative District", - "zh-CN": "雅利洛-VI - 行政区", - "zh-TW": "雅利洛-VI - 行政區", - ja: "ヤリーロ-VI - 行政区", - ko: "야릴로-VI - 행정구역", - ru: "Ярило-VI - Административный район" - }, - plane_id: 10101, - floor_id: 10101001, - entry_id: 1010101, - x: 10250, - y: 51, - z: 32670 - }, - { - names: { - th: "Jarilo-VI (Underworld) - เมือง Boulder (Boulder Town)", - en: "Jarilo-VI - Boulder Town", - "zh-CN": "雅利洛-VI - 磐岩镇", - "zh-TW": "雅利洛-VI - 磐岩鎮", - ja: "ヤリーロ-VI - ボルダータウン", - ko: "야릴로-VI - 볼더 타운", - ru: "Ярило-VI - Камнеград" - }, - plane_id: 10102, - floor_id: 10102001, - entry_id: 1010201, - x: -20560, - y: -286, - z: 57320 - }, - { - names: { - th: "Xianzhou Luofu - ท่าเรือเรือบินดาวกลาง (Central Starskiff Haven)", - en: "Xianzhou Luofu - Central Starskiff Haven", - "zh-CN": "仙舟「罗浮」 - 星槎海中枢", - "zh-TW": "仙舟「羅浮」 - 星槎海中樞", - ja: "仙舟「羅浮」 - 星槎海中枢", - ko: "선주 「나부」 - 별뗏목 바다 중추", - ru: "Сяньчжоу: Лофу - Центральная гавань Приюта звёздных яликов" - }, - plane_id: 10202, - floor_id: 10202001, - entry_id: 1020201, - x: 8320, - y: -3486, - z: -25080 - }, - { - names: { - th: "Penacony - The Reverie (Dreamscape Hotel)", - en: "Penacony - The Reverie (Dreamscape)", - "zh-CN": "匹诺康尼 - 「白日梦」酒店-梦境", - "zh-TW": "匹諾康尼 - 「白日夢」飯店-夢境", - ja: "ピノコニー - 「レバリー」ホテル-夢境", - ko: "페나코니 - 「레버리」 호텔-꿈속", - ru: "Пенакония - Отель «Грёзы» (Мир грёз)" - }, - plane_id: 10301, - floor_id: 10301001, - entry_id: 1030101, - x: 0, - y: 0, - z: 0 - }, - { - names: { - th: "Penacony - ชั่วโมงทอง (Golden Hour)", - en: "Penacony - Golden Hour", - "zh-CN": "匹诺康尼 - 黄金的时刻", - "zh-TW": "匹諾康尼 - 黃金的時刻", - ja: "ピノコニー - 黄金の刻", - ko: "페나코니 - 황금의 순간", - ru: "Пенакония - Золотой миг" - }, - plane_id: 10302, - floor_id: 10302001, - entry_id: 1030201, - x: -3556, - y: 1539, - z: -378220 - } -]; - -export function getTeleportName(preset, langOverride = null) { - const lang = langOverride || getCurrentLanguage(); - return preset.names?.[lang] || preset.names?.en || preset.names?.th || "Teleport Point"; -} diff --git a/scripts/fetch_avatars.js b/scripts/fetch_avatars.js index 50fbacd..c9286c2 100644 --- a/scripts/fetch_avatars.js +++ b/scripts/fetch_avatars.js @@ -8,7 +8,10 @@ const ids = [ 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1217, 1218, 1220, 1221, 1222, 1223, 1225, 1005, 1006, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1312, 1313, 1314, 1315, 1317, - 1401, 1402, 1403, 1404, 1405, 1407, 1408, 1409, 1410 + 1401, 1402, 1403, 1404, 1405, 1407, 1408, 1409, 1410, + 1014, 1015, 1321, 1406, 1412, 1413, 1414, 1415, + 1501, 1502, 1503, 1504, 1505, 1506, 1507, 1508, 1509, 1510, 1511, 1512, 1513, + 8009, 8010 ]; const targetDir = path.join(__dirname, '..', 'assets', 'avatars'); @@ -53,12 +56,20 @@ async function processId(id) { ok = await fetchUrl(rawUrl, dest); if (ok) return { id, status: 'ok (raw)' }; - // Fallbacks for trailblazer female variants if not directly mapped - if ([8004, 8006, 8008].includes(id) && fs.existsSync(path.join(targetDir, '8002.png'))) { + // Fallbacks for special & collab variants + if (id === 1506 && fs.existsSync(path.join(targetDir, '1006.png'))) { + fs.copyFileSync(path.join(targetDir, '1006.png'), dest); + return { id, status: 'fallback 1006 (Silver Wolf)' }; + } + if (id === 1511 && fs.existsSync(path.join(targetDir, '1308.png'))) { + fs.copyFileSync(path.join(targetDir, '1308.png'), dest); + return { id, status: 'fallback 1308 (Acheron)' }; + } + if ([8004, 8006, 8008, 8010].includes(id) && fs.existsSync(path.join(targetDir, '8002.png'))) { fs.copyFileSync(path.join(targetDir, '8002.png'), dest); return { id, status: 'fallback 8002' }; } - if ([8003, 8005, 8007].includes(id) && fs.existsSync(path.join(targetDir, '8001.png'))) { + if ([8003, 8005, 8007, 8009].includes(id) && fs.existsSync(path.join(targetDir, '8001.png'))) { fs.copyFileSync(path.join(targetDir, '8001.png'), dest); return { id, status: 'fallback 8001' }; }