change icon stats, new plana, redesign some ui, fix JSON managment
Deploy GaC-Srtool to GitHub Pages / deploy (push) Canceled after 0s

This commit is contained in:
CAtlas0784 committed 2026-11-02 10:07:19 +07:00
1 parent 083848ee6e
commit 45d8414e60
19 files changed
+911 -222

No files matched your search

+301 -61
View File
@@ -1,10 +1,10 @@
import { COMMANDS, usage, inspectCommand, completeCommand, setCommandLanguage, describeCommand } from './command-assistant.mjs';
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetInfo } from './equipment-data.js?v=2.23';
import { initializeItemCatalog } from './item-catalog.js?v=2';
import { initializeUnstuck } from './unstuck.js?v=1';
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js?v=2.21';
import { LIGHTCONE_METADATA, RELIC_SET_METADATA, getLightconeInfo, getRelicSetInfo } from './equipment-data.js?v=2.27';
import { initializeItemCatalog } from './item-catalog.js?v=5';
import { initializeUnstuck } from './unstuck.js?v=4';
import { PRYDWEN_BUILD_GUIDES } from './build-guides-data.js';
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js?v=2.17';
import { SUPPORTED_LANGUAGES, TRANSLATIONS, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js?v=2.21';
import {
getAvatarIconUrl,
getLightconeArtUrl,
@@ -480,6 +480,7 @@ function setConnectionStatus(connected, extraText = '') {
if (connected !== toolSessionConnected) syncToolSession(connected);
state.isConnected = connected;
if (connectionChanged && state.activeTab === 'tab-monster-settings') loadMonsterSettings();
updateCameraSaveStatus();
if (connected) {
el.statusDot.classList.add('connected');
el.connectionStatusText.textContent = extraText || t('online');
@@ -604,7 +605,7 @@ function areCapturedSubAffixesValid(setId, slot, mainAffixId, subAffixes) {
// Official in-game property icons (same set used by the overview tab)
function getRelicImage(setId, slot = 5) {
return RELIC_SET_METADATA[setId]?.images?.[slot] || RELIC_SET_METADATA[setId]?.images?.[5] || `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png`;
return RELIC_SET_METADATA[setId]?.setIcon || RELIC_SET_METADATA[setId]?.images?.[slot] || RELIC_SET_METADATA[setId]?.images?.[5] || `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/relic/${setId}.png`;
}
function getLightconeIconSrc(id) {
return LIGHTCONE_METADATA[id]?.images?.thumbnail || `https://static.nanoka.cc/assets/hsr/lightconemediumicon/${id}.webp`;
@@ -1015,7 +1016,26 @@ function applyTranslations() {
if (icons) {
elem.textContent = val.replace(/^[🎯⚡🔨🛡️\s]+/u, '');
icons.slice().reverse().forEach(icon => elem.insertAdjacentHTML('afterbegin', getStatIconHtml(icon, 16, '')));
} else elem.textContent = val;
} else {
elem.textContent = val;
const equipmentIcons = {
tabEquipment: './assets/lightcones/22009-icon.webp',
modalAddLcTitle: './assets/lightcones/22009-icon.webp',
eqLcTitle: './assets/lightcones/22009-icon.webp',
overviewEquippedLc: './assets/lightcones/22009-icon.webp',
modalAddRelicTitle: './assets/relic-slots/1.png',
eqRelicTitle: './assets/relic-slots/1.png',
overviewEquippedRelics: './assets/relic-slots/1.png',
activeSetEffects: './assets/relic-slots/1.png'
};
if (equipmentIcons[key]) {
const icon = document.createElement('img');
icon.src = equipmentIcons[key];
icon.alt = '';
icon.className = 'game-inline-icon';
elem.prepend(icon, ' ');
}
}
}
});
@@ -1071,6 +1091,11 @@ function applyTranslations() {
updateAvatarAssignCurrent();
renderAvatarAssignGrid();
}
// Keep the camera panel (mode cards, distance readout, save state) translated.
renderCameraModeOptions();
syncCameraUi();
loadEquipment();
if (analystState.currentReport) {
renderDamageAnalyst();
@@ -2928,6 +2953,7 @@ const SIGNATURE_LIGHTCONES = {
// 1: Flat HP, 2: Flat ATK, 3: Flat DEF, 4: HP%, 5: ATK%, 6: DEF%
// 7: SPD, 8: CRIT Rate%, 9: CRIT DMG%, 10: EHR%, 11: Effect RES%, 12: Break Effect%
const CHARACTER_OPTIMAL_BUILDS = {
1511: { cavern: 133, planar: 329, body: 4, feet: 4, sphere: 9, rope: 4, substats: [8, 9, 7, 5] }, // Aeon ★ Aha
1308: { cavern: 117, planar: 313, body: 4, feet: 2, sphere: 7, rope: 4, substats: [8, 9, 7, 5] }, // Acheron
1310: { cavern: 119, planar: 315, body: 2, feet: 4, sphere: 2, rope: 1, substats: [12, 7, 5, 11] }, // Firefly
1220: { cavern: 120, planar: 314, body: 4, feet: 4, sphere: 8, rope: 4, substats: [8, 9, 7, 5] }, // Feixiao
@@ -3369,17 +3395,17 @@ function populateModalChangeLcDropdown(avatarPath) {
// Slot full definitions for relics
const SLOT_NAMES_FULL = {
1: { nameKey: 'relicSlotHead', shortKey: 'slotHeadShort', icon: "🧢", defaultStat: "HP (Flat)" },
2: { nameKey: 'relicSlotHands', shortKey: 'slotHandsShort', icon: "🧤", defaultStat: "ATK (Flat)" },
3: { nameKey: 'relicSlotBody', shortKey: 'slotBodyShort', icon: "🦺", defaultStat: "CRIT Rate / DMG" },
4: { nameKey: 'relicSlotFeet', shortKey: 'slotFeetShort', icon: "👢", defaultStat: "SPD / ATK%" },
5: { nameKey: 'relicSlotSphere', shortKey: 'slotSphereShort', icon: "🔮", defaultStat: "Elemental DMG%" },
6: { nameKey: 'relicSlotRope', shortKey: 'slotRopeShort', icon: "🔗", defaultStat: "ERR / Break / ATK%" }
1: { nameKey: 'relicSlotHead', shortKey: 'slotHeadShort', icon: './assets/relic-slots/1.png', defaultStat: "HP (Flat)" },
2: { nameKey: 'relicSlotHands', shortKey: 'slotHandsShort', icon: './assets/relic-slots/2.png', defaultStat: "ATK (Flat)" },
3: { nameKey: 'relicSlotBody', shortKey: 'slotBodyShort', icon: './assets/relic-slots/3.png', defaultStat: "CRIT Rate / DMG" },
4: { nameKey: 'relicSlotFeet', shortKey: 'slotFeetShort', icon: './assets/relic-slots/4.png', defaultStat: "SPD / ATK%" },
5: { nameKey: 'relicSlotSphere', shortKey: 'slotSphereShort', icon: './assets/relic-slots/5.png', defaultStat: "Elemental DMG%" },
6: { nameKey: 'relicSlotRope', shortKey: 'slotRopeShort', icon: './assets/relic-slots/6.png', defaultStat: "ERR / Break / ATK%" }
};
function getSlotInfo(slot) {
const info = SLOT_NAMES_FULL[slot];
if (!info) return { name: `${t('slot')} ${slot}`, short: `${t('slot')} ${slot}`, icon: '🛡️', defaultStat: '' };
if (!info) return { name: `${t('slot')} ${slot}`, short: `${t('slot')} ${slot}`, icon: '', defaultStat: '' };
const name = t(info.nameKey);
return { ...info, name, short: t(info.shortKey) };
}
@@ -3963,7 +3989,7 @@ function renderModalRelicsTab(avatarId) {
<div class="relic-slot-icon-wrap">
<img src="${getRelicImage(setId, slot)}"
onerror="this.src='./assets/logo.png'" alt="Relic" style="width: 100%; height: 100%; object-fit: contain;">
<span class="relic-slot-badge">${slotInfo.icon} ${slotInfo.short}</span>
<span class="relic-slot-badge" aria-label="${t('relicSlotBadgeLabel', { slot: slotInfo.short })}"><img class="relic-slot-type-icon" src="${slotInfo.icon}" alt=""> ${slotInfo.short}</span>
</div>
<div class="relic-slot-content">
<div style="display: flex; justify-content: space-between; align-items: center; padding-right: 1.5rem;">
@@ -3995,8 +4021,8 @@ function renderModalRelicsTab(avatarId) {
box.className = 'relic-slot-box empty';
box.innerHTML = `
<div class="relic-slot-icon-wrap" style="opacity: 0.4;">
<span style="font-size: 1.5rem;">${slotInfo.icon}</span>
<span class="relic-slot-badge">${slotInfo.name.split(' ')[0]}</span>
<img class="relic-slot-empty-icon" src="${slotInfo.icon}" alt="">
<span class="relic-slot-badge" aria-label="${t('relicSlotBadgeLabel', { slot: slotInfo.short })}">${slotInfo.short}</span>
</div>
<div class="relic-slot-content">
<div class="relic-slot-title" style="color: var(--text-muted);">${slotInfo.name}</div>
@@ -4020,9 +4046,9 @@ function renderModalRelicsTab(avatarId) {
const cnt = setCountMap[sId];
const sInfo = getRelicSetInfo(sId);
if (cnt >= 4) {
activeSets.push(`🌟 <strong>${sInfo.name} (4-Piece Set)</strong>: ${sInfo.desc4 || t('relicSet4Fallback')}`);
activeSets.push(`<img class="game-inline-icon" src="${getRelicImage(sId)}" alt=""> <strong>${sInfo.name} (${t('relicSetPieces', { count: 4 })})</strong>: ${sInfo.desc4 || t('relicSet4Fallback')}`);
} else if (cnt >= 2) {
activeSets.push(`✨ <strong>${sInfo.name} (2-Piece Set)</strong>: ${sInfo.desc2 || t('relicSet2Fallback')}`);
activeSets.push(`<img class="game-inline-icon" src="${getRelicImage(sId)}" alt=""> <strong>${sInfo.name} (${t('relicSetPieces', { count: 2 })})</strong>: ${sInfo.desc2 || t('relicSet2Fallback')}`);
}
});
@@ -5164,17 +5190,17 @@ async function renderModalSkillsTab(avatarId) {
// Traces
if (a2Node) {
if (el.traceIconA2) el.traceIconA2.src = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/${a2Node.icon}`;
if (el.traceNameA2) el.traceNameA2.textContent = `🌟 ${a2Node.name || t('traceFallbackA2')}`;
if (el.traceNameA2) el.traceNameA2.textContent = a2Node.name || t('traceFallbackA2');
if (el.traceDescA2) el.traceDescA2.innerHTML = formatSkillDesc(a2Node.desc, a2Node.params, 1);
}
if (a4Node) {
if (el.traceIconA4) el.traceIconA4.src = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/${a4Node.icon}`;
if (el.traceNameA4) el.traceNameA4.textContent = `🌟 ${a4Node.name || t('traceFallbackA4')}`;
if (el.traceNameA4) el.traceNameA4.textContent = a4Node.name || t('traceFallbackA4');
if (el.traceDescA4) el.traceDescA4.innerHTML = formatSkillDesc(a4Node.desc, a4Node.params, 1);
}
if (a6Node) {
if (el.traceIconA6) el.traceIconA6.src = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/${a6Node.icon}`;
if (el.traceNameA6) el.traceNameA6.textContent = `🌟 ${a6Node.name || t('traceFallbackA6')}`;
if (el.traceNameA6) el.traceNameA6.textContent = a6Node.name || t('traceFallbackA6');
if (el.traceDescA6) el.traceDescA6.innerHTML = formatSkillDesc(a6Node.desc, a6Node.params, 1);
}
}
@@ -5657,7 +5683,7 @@ function renderShowcaseCard(avatarId) {
}
if (activeSetBadges.length === 0) {
const rec = RECOMMENDED_RELIC_BUILDS[canonPath] || RECOMMENDED_RELIC_BUILDS.Preservation;
const rec = getOptimalBuildForAvatar(aid);
activeSetBadges.push({ name: getRelicSetInfo(rec.cavern).name, count: 4 });
activeSetBadges.push({ name: getRelicSetInfo(rec.planar).name, count: 2 });
}
@@ -5711,7 +5737,7 @@ function renderShowcaseCard(avatarId) {
let relicsHtml = '';
for (let slot = 1; slot <= 6; slot++) {
const equipped = avatarRelics.find(r => (r.relic_id % 10 === slot));
const rec = RECOMMENDED_RELIC_BUILDS[canonPath] || RECOMMENDED_RELIC_BUILDS.Preservation;
const rec = getOptimalBuildForAvatar(aid);
const setId = equipped ? Math.floor((equipped.relic_id % 10000) / 10) : (slot <= 4 ? rec.cavern : rec.planar);
const rInfo = getRelicSetInfo(setId);
const slotInfo = slotNames[slot];
@@ -8182,10 +8208,7 @@ async function loadPersistentSettings() {
}
if (!data) return;
const cameraMode = document.getElementById('camera-mode');
const cameraDistance = document.getElementById('camera-distance');
if (cameraMode) cameraMode.value = data.camera_settings?.mode || 'freecam';
if (cameraDistance) cameraDistance.value = data.camera_settings?.distance ?? 2;
applyCameraSettings(data.camera_settings);
if (data.main_character && el.persistentMcSelect) {
el.persistentMcSelect.value = data.main_character;
@@ -8202,6 +8225,177 @@ async function loadPersistentSettings() {
}
}
/**
* Freecam / FaceCam settings panel.
* The native <select> and range input stay in the DOM as the single source of
* truth for the save payload, while the visible controls are custom cards.
*/
const cameraSettingsCache = { busy: false, dirty: false, error: false };
function getCameraModeSelect() {
return document.getElementById('camera-mode');
}
function getCameraDistanceInput() {
return document.getElementById('camera-distance');
}
function clampCameraDistance(value) {
const numeric = Number(value);
const safe = Number.isFinite(numeric) ? numeric : 2;
return Math.min(5, Math.max(0.5, Math.round(safe * 10) / 10));
}
function renderCameraModeOptions() {
const container = document.getElementById('camera-mode-options');
const select = getCameraModeSelect();
if (!container || !select) return;
const choices = [
{ value: 'freecam', icon: '🎥', title: t('cameraModeFreecam'), detail: t('cameraModeFreecamDesc') },
{ value: 'facecam', icon: '🙂', title: t('cameraModeFacecam'), detail: t('cameraModeFacecamDesc') }
];
container.replaceChildren();
for (const choice of choices) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'camera-mode-option';
button.dataset.value = choice.value;
button.setAttribute('aria-pressed', 'false');
const icon = document.createElement('span');
icon.className = 'camera-mode-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = choice.icon;
const text = document.createElement('span');
text.className = 'camera-mode-text';
const title = document.createElement('span');
title.className = 'camera-mode-title';
title.textContent = choice.title;
const detail = document.createElement('span');
detail.className = 'camera-mode-detail';
detail.textContent = choice.detail;
text.append(title, detail);
const check = document.createElement('span');
check.className = 'camera-mode-check';
check.setAttribute('aria-hidden', 'true');
check.textContent = '✓';
button.append(icon, text, check);
container.append(button);
}
container.setAttribute('aria-label', t('cameraMode'));
container.onclick = event => {
const button = event.target.closest('.camera-mode-option');
if (!button) return;
select.value = button.dataset.value;
syncCameraUi({ dirty: true });
};
syncCameraModeSelection();
}
function syncCameraModeSelection() {
const container = document.getElementById('camera-mode-options');
const select = getCameraModeSelect();
if (!container || !select) return;
for (const button of container.querySelectorAll('.camera-mode-option')) {
const selected = button.dataset.value === select.value;
button.classList.toggle('is-selected', selected);
button.setAttribute('aria-pressed', String(selected));
}
}
function readCameraForm() {
const select = getCameraModeSelect();
const input = getCameraDistanceInput();
return {
mode: select?.value === 'facecam' ? 'facecam' : 'freecam',
distance: clampCameraDistance(input?.value)
};
}
function setCameraSaveStatus(stateName, messageKey) {
const wrapper = document.getElementById('camera-save-state');
const text = document.getElementById('camera-save-state-text');
if (wrapper) wrapper.dataset.state = stateName;
if (text) text.textContent = t(messageKey);
}
function updateCameraSaveStatus() {
const button = document.getElementById('btn-save-camera');
const online = state.isConnected && state.serverMode === 'gac';
if (cameraSettingsCache.busy) {
setCameraSaveStatus('saving', 'cameraStatusSaving');
if (button) button.disabled = true;
return;
}
if (!online) {
setCameraSaveStatus('offline', 'cameraStatusOffline');
if (button) button.disabled = true;
return;
}
if (cameraSettingsCache.error) setCameraSaveStatus('error', 'cameraStatusError');
else if (cameraSettingsCache.dirty) setCameraSaveStatus('unsaved', 'cameraStatusUnsaved');
else if (state.persistentData?.camera_settings) setCameraSaveStatus('saved', 'cameraStatusSaved');
else setCameraSaveStatus('idle', 'cameraStatusIdle');
if (button) button.disabled = false;
}
function cameraFormDiffersFromSaved() {
const saved = state.persistentData?.camera_settings;
if (!saved) return true;
const current = readCameraForm();
const savedMode = saved.mode === 'facecam' ? 'facecam' : 'freecam';
return current.mode !== savedMode
|| Math.abs(current.distance - clampCameraDistance(saved.distance)) > 0.001;
}
function syncCameraUi({ dirty = false } = {}) {
const distanceInput = getCameraDistanceInput();
const distanceValue = document.getElementById('camera-distance-value');
const distanceBlock = document.getElementById('camera-distance-block');
const distanceNote = document.getElementById('camera-distance-note');
if (!distanceInput) return;
const settings = readCameraForm();
if (dirty) {
cameraSettingsCache.dirty = cameraFormDiffersFromSaved();
cameraSettingsCache.error = false;
}
if (distanceValue) distanceValue.textContent = t('cameraDistanceValue', { value: settings.distance.toFixed(1) });
if (distanceBlock) distanceBlock.classList.toggle('is-inactive', settings.mode !== 'facecam');
if (distanceNote) {
distanceNote.textContent = t(settings.mode === 'facecam' ? 'cameraDistanceHint' : 'cameraDistanceHintFreecam');
}
syncCameraModeSelection();
updateCameraSaveStatus();
}
function applyCameraSettings(settings) {
const select = getCameraModeSelect();
const distanceInput = getCameraDistanceInput();
if (select) select.value = settings?.mode === 'facecam' ? 'facecam' : 'freecam';
if (distanceInput) distanceInput.value = String(clampCameraDistance(settings?.distance ?? 2));
cameraSettingsCache.dirty = false;
cameraSettingsCache.error = false;
syncCameraUi();
}
function initCameraSettings() {
if (!getCameraModeSelect()) return;
renderCameraModeOptions();
getCameraDistanceInput()?.addEventListener('input', () => syncCameraUi({ dirty: true }));
syncCameraUi();
}
// Render Ex-Rank Cards
function renderExRanks(exList) {
if (!el.exrankList) return;
@@ -8273,19 +8467,30 @@ async function toggleSingleExRank(avatarId, unlock) {
}
// Load and Switch JSON Tab Data
let jsonLoadGeneration = 0;
let jsonLoadPending = false;
async function loadJsonData(mode) {
const generation = ++jsonLoadGeneration;
jsonLoadPending = true;
state.currentJsonMode = mode;
// Never leave the previous file visible under the newly selected filename.
el.jsonEditor.value = '';
el.jsonEditor.readOnly = true;
if (el.btnJsonSave) el.btnJsonSave.disabled = true;
try {
if (mode === 'freesr') {
el.btnSelectFreesr.className = 'btn btn-sm btn-primary active';
if (el.btnSelectPersistent) el.btnSelectPersistent.className = 'btn btn-sm btn-secondary';
if (el.btnSelectDatagame) el.btnSelectDatagame.className = 'btn btn-sm btn-secondary';
el.jsonStatusInfo.textContent = t('jsonStatusLoading', { file: 'freesr-data.json' });
try {
if (state.fullFreesrData) {
if (state.fullFreesrData && !state.isConnected) {
el.jsonEditor.value = JSON.stringify(state.fullFreesrData, null, 2);
el.jsonStatusInfo.textContent = t('jsonStatusMemory', { file: 'freesr-data.json' });
} else if (state.isConnected) {
const res = await apiCall('/api/freesr');
if (generation !== jsonLoadGeneration) return;
if (!res.data || typeof res.data !== 'object' || Array.isArray(res.data)) throw new Error('invalid freesr response');
state.fullFreesrData = res.data;
saveFreesrCache(res.data);
el.jsonEditor.value = JSON.stringify(res.data, null, 2);
@@ -8298,13 +8503,11 @@ async function loadJsonData(mode) {
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
el.jsonStatusInfo.textContent = t('jsonStatusBrowserCache', { file: 'freesr-data.json' });
} else {
el.jsonStatusInfo.textContent = t('jsonOfflineNoCache');
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2);
}
el.jsonStatusInfo.textContent = t('jsonDataUnavailable', { file: 'freesr-data.json' });
}
}
} catch (err) {
if (generation !== jsonLoadGeneration) return;
const cached = localStorage.getItem('gac_srtool_freesr_cache');
if (cached) {
try {
@@ -8314,10 +8517,7 @@ async function loadJsonData(mode) {
el.jsonStatusInfo.textContent = t('jsonStatusBrowserCache', { file: 'freesr-data.json' });
} catch (e) {}
} else {
el.jsonStatusInfo.textContent = t('jsonOfflinePaste');
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ avatars: {}, lightcones: [], relics: [], battle_config: {} }, null, 2);
}
el.jsonStatusInfo.textContent = t('jsonDataUnavailable', { file: 'freesr-data.json' });
}
}
} else if (mode === 'datagame') {
@@ -8326,11 +8526,12 @@ async function loadJsonData(mode) {
if (el.btnSelectDatagame) el.btnSelectDatagame.className = 'btn btn-sm btn-primary active';
el.jsonStatusInfo.textContent = t('jsonStatusLoading', { file: 'data-in-game.json' });
try {
if (state.dataGameData) {
if (state.dataGameData && !state.isConnected) {
el.jsonEditor.value = JSON.stringify(state.dataGameData, null, 2);
el.jsonStatusInfo.textContent = t('jsonStatusMemory', { file: 'data-in-game.json' });
} else if (state.isConnected) {
const res = await apiCall('/api/data-in-game').catch(() => null);
if (generation !== jsonLoadGeneration) return;
if (res && (res.data || res.lineups)) {
const val = res.data || res;
state.dataGameData = val;
@@ -8354,6 +8555,7 @@ async function loadJsonData(mode) {
}
}
} catch (err) {
if (generation !== jsonLoadGeneration) return;
const cached = localStorage.getItem('gac_srtool_datagame_cache');
if (cached) {
try {
@@ -8374,14 +8576,17 @@ async function loadJsonData(mode) {
if (el.btnSelectDatagame) el.btnSelectDatagame.className = 'btn btn-sm btn-secondary';
el.jsonStatusInfo.textContent = t('jsonStatusLoading', { file: 'persistent' });
try {
if (state.persistentData) {
if (state.persistentData && !state.isConnected) {
el.jsonEditor.value = JSON.stringify(state.persistentData, null, 2);
el.jsonStatusInfo.textContent = t('jsonStatusMemory', { file: 'persistent' });
} else if (state.isConnected) {
const res = await apiCall('/api/persistent');
state.persistentData = res.data;
savePersistentCache(res.data);
el.jsonEditor.value = JSON.stringify(res.data, null, 2);
if (generation !== jsonLoadGeneration) return;
const persistent = res.persistent ?? res.data;
if (!persistent || typeof persistent !== 'object' || Array.isArray(persistent)) throw new Error('invalid persistent response');
state.persistentData = persistent;
savePersistentCache(persistent);
el.jsonEditor.value = JSON.stringify(persistent, null, 2);
el.jsonStatusInfo.textContent = t('jsonStatusServer', { file: 'persistent' });
} else {
const cached = localStorage.getItem('gac_srtool_persistent_cache');
@@ -8391,13 +8596,11 @@ async function loadJsonData(mode) {
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
el.jsonStatusInfo.textContent = t('jsonStatusBrowserCache', { file: 'persistent' });
} else {
el.jsonStatusInfo.textContent = t('jsonOfflineNoCache');
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2);
}
el.jsonStatusInfo.textContent = t('jsonDataUnavailable', { file: 'persistent' });
}
}
} catch (err) {
if (generation !== jsonLoadGeneration) return;
const cached = localStorage.getItem('gac_srtool_persistent_cache');
if (cached) {
try {
@@ -8407,13 +8610,17 @@ async function loadJsonData(mode) {
el.jsonStatusInfo.textContent = t('jsonStatusBrowserCache', { file: 'persistent' });
} catch (e) {}
} else {
el.jsonStatusInfo.textContent = t('jsonOfflinePaste');
if (!el.jsonEditor.value) {
el.jsonEditor.value = JSON.stringify({ lineups: DEFAULT_LINEUP, main_character: "FemaleHarmony", march_type: "MarchHunt" }, null, 2);
}
el.jsonStatusInfo.textContent = t('jsonDataUnavailable', { file: 'persistent' });
}
}
}
} finally {
if (generation === jsonLoadGeneration) {
jsonLoadPending = false;
el.jsonEditor.readOnly = false;
if (el.btnJsonSave) el.btnJsonSave.disabled = !el.jsonEditor.value.trim();
}
}
}
// Save JSON Tab Data
@@ -9015,18 +9222,46 @@ function setupEvents() {
// Persistent Toggles
document.getElementById('btn-save-camera')?.addEventListener('click', async () => {
const button = document.getElementById('btn-save-camera');
const mode = document.getElementById('camera-mode').value;
const distance = Number(document.getElementById('camera-distance').value);
const select = getCameraModeSelect();
const distanceInput = getCameraDistanceInput();
if (!button || !select || !distanceInput || cameraSettingsCache.busy) return;
const mode = select.value === 'facecam' ? 'facecam' : 'freecam';
const distance = Number(distanceInput.value);
if (!Number.isFinite(distance) || distance < 0.5 || distance > 5) {
showToast(t('cameraDistanceError'), 'error'); return;
showToast(t('cameraDistanceError'), 'error');
return;
}
button.disabled = true;
if (!state.isConnected || state.serverMode !== 'gac') {
showToast(t('cameraSaveOfflineError'), 'error');
updateCameraSaveStatus();
return;
}
cameraSettingsCache.busy = true;
updateCameraSaveStatus();
button.textContent = t('cameraSaving');
try {
const saved = await apiCall('/api/camera', 'POST', { mode, distance });
if (state.persistentData) state.persistentData.camera_settings = saved;
const payload = saved?.camera_settings || saved || {};
const settings = {
mode: payload.mode === 'facecam' ? 'facecam' : (payload.mode === 'freecam' ? 'freecam' : mode),
distance: clampCameraDistance(payload.distance ?? distance)
};
state.persistentData = state.persistentData || {};
state.persistentData.camera_settings = settings;
savePersistentCache(state.persistentData);
applyCameraSettings(settings);
showToast(t('cameraSaved'), 'success');
} catch (err) { showToast(err.message, 'error'); }
finally { button.disabled = false; }
} catch (err) {
cameraSettingsCache.dirty = true;
cameraSettingsCache.error = true;
showToast(t('toastErrorMessage', { message: err.message }), 'error');
} finally {
cameraSettingsCache.busy = false;
button.textContent = t('cameraSave');
updateCameraSaveStatus();
}
});
document.getElementById('btn-save-aha-help')?.addEventListener('click', async () => {
const select = document.getElementById('aha-help-level');
@@ -9192,6 +9427,9 @@ function setupEvents() {
});
el.btnJsonSave.addEventListener('click', saveJsonData);
el.jsonEditor.addEventListener('input', () => {
if (!jsonLoadPending) el.btnJsonSave.disabled = !el.jsonEditor.value.trim();
});
el.btnJsonDownload.addEventListener('click', () => {
const text = el.jsonEditor.value;
@@ -9211,6 +9449,7 @@ function setupEvents() {
try {
const parsed = JSON.parse(event.target.result);
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
if (!jsonLoadPending) el.btnJsonSave.disabled = false;
let recognized = false;
let isFreesr = false;
@@ -9247,7 +9486,7 @@ function setupEvents() {
}
// If persistent or has lineups
if (parsed.main_character || parsed.march_type || (parsed.lineups && !parsed.avatars)) {
if (!isFreesr && (parsed.main_character || parsed.march_type || parsed.lineups)) {
recognized = true;
isPersistent = true;
state.persistentData = parsed;
@@ -10082,9 +10321,9 @@ function renderDamageAnalyst() {
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap; padding-top: 0.65rem; border-top: 1px solid rgba(255,255,255,0.06); font-size: 0.75rem; color: var(--text-muted);">
<div>⏱️ ${t('analystTurnsTaken')}: <strong style="color: #fff;">${hasAvatarStats ? a.total_turns || 0 : '—'}</strong> ${hasAvatarStats && a.extra_turns ? `(+${a.extra_turns})` : ''}</div>
<div>🛡️ ${t('analystShield')}: <strong style="color: #38bdf8;">${hasAvatarStats ? formatNumber(a.total_shield) : '—'}</strong></div>
<div>💚 ${t('analystHealing')}: <strong style="color: #34d399;">${hasAvatarStats ? formatNumber(a.total_heal) : '—'}</strong></div>
<div>💔 ${t('analystDamageTaken')}: <strong style="color: #f87171;">${hasAvatarStats ? formatNumber(a.total_damage_taken) : '—'}</strong></div>
<div>${getStatIconHtml('IconDefence', 16)} ${t('analystShield')}: <strong style="color: #38bdf8;">${hasAvatarStats ? formatNumber(a.total_shield) : '—'}</strong></div>
<div>${getStatIconHtml('IconHealRatio', 16)} ${t('analystHealing')}: <strong style="color: #34d399;">${hasAvatarStats ? formatNumber(a.total_heal) : '—'}</strong></div>
<div>${getStatIconHtml('IconMaxHP', 16)} ${t('analystDamageTaken')}: <strong style="color: #f87171;">${hasAvatarStats ? formatNumber(a.total_damage_taken) : '—'}</strong></div>
</div>
</div>
`;
@@ -10376,6 +10615,7 @@ async function init() {
setupTabs();
setupModalSubtabs();
setupEvents();
initCameraSettings();
setupInventoryEvents();
setupDamageAnalystEvents();
setupGMConsoleEvents();
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.17";
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.21";
export const AVATAR_METADATA = {
"1001": {
+3 -3
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.17";
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.21";
export const LIGHTCONE_METADATA = {
"20000": {
@@ -4325,8 +4325,8 @@ export const RELIC_SET_METADATA = {
// Set data is backed by the captured Client relic and affix tables documented in the server resources.
Object.assign(RELIC_SET_METADATA, {
329: { clientCaptureRequired: true, names: { en: "God's Moment of Joy" }, desc2: "Increases the wearer's SPD by 6%. When the wearer's Elation reaches 40% or higher, the wearer's action advances by 40% immediately when entering combat.", images: { 5: './assets/relics/329-5.webp', 6: './assets/relics/329-6.webp' } },
330: { clientCaptureRequired: true, names: { en: 'Subspace ASC Center' }, desc2: "Increases the wearer's SPD by 6%. When the wearer's SPD reaches 145 or higher, increases all allies' Break Effect by 18% after entering combat. This effect cannot stack.", images: { 5: './assets/relics/330-5.webp', 6: './assets/relics/330-6.webp' } }
329: { clientCaptureRequired: true, names: { en: "God's Moment of Joy" }, desc2: "Increases the wearer's SPD by 6%. When the wearer's Elation reaches 40% or higher, the wearer's action advances by 40% immediately when entering combat.", setIcon: './assets/relics/329-set.png', images: { 5: './assets/relics/329-5.webp', 6: './assets/relics/329-6.webp' } },
330: { clientCaptureRequired: true, names: { en: 'Subspace ASC Center' }, desc2: "Increases the wearer's SPD by 6%. When the wearer's SPD reaches 145 or higher, increases all allies' Break Effect by 18% after entering combat. This effect cannot stack.", setIcon: './assets/relics/330-set.png', images: { 5: './assets/relics/330-5.webp', 6: './assets/relics/330-6.webp' } }
});
export function getLightconeInfo(lcId, langOverride = null) {
+180 -144
View File
@@ -129,8 +129,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "เพิ่มกรวยแสง 5 ดาวประจำตัวละครทั้งหมดเข้าคลังแล้ว",
"toastTeleportedTrain": "วาร์ปตัวละครกลับสู่ Astral Express Parlor Car เรียบร้อยแล้ว",
"toastLineupCleared": "ล้างตัวละครในทีมเรียบร้อยแล้ว",
"modalAddLcTitle": "🗡️ เพิ่มกรวยแสงลง FreeSR",
"modalAddRelicTitle": "🛡️ เพิ่มรีลิกส์ลง FreeSR",
"modalAddLcTitle": "เพิ่มกรวยแสงลง FreeSR",
"modalAddRelicTitle": "เพิ่มรีลิกส์ลง FreeSR",
"lblSelectLc": "เลือกกรวยแสง",
"lblSelectRelicSet": "เลือกเซ็ตรีลิกส์",
"lblRelicSlot": "ชิ้นส่วน (Slot)",
@@ -152,7 +152,7 @@ export const TRANSLATIONS = {
"connectFailed": "เชื่อมต่อไม่สำเร็จ",
"tabDashboard": "📊 แดชบอร์ด & จัดทีม",
"tabAvatars": "👥 ตัวละคร",
"tabEquipment": "🗡️ กรวยแสง & รีลิกส์",
"tabEquipment": "กรวยแสง & รีลิกส์",
"tabInventory": "🎒 ไอเทมในกระเป๋า",
"inventoryTitle": "🎒 จัดการจำนวนไอเทม",
"inventoryDescription": "ค้นหาด้วย Item ID แล้วแก้จำนวนหลายรายการก่อนบันทึกครั้งเดียว รายการมาจาก catalog ของ Client ที่เซิร์ฟเวอร์ใช้ ช่องว่างหมายถึงใช้ค่าเริ่มต้นของเซิร์ฟเวอร์",
@@ -211,10 +211,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "ยืนยันปรับตัวละครทั้งหมดให้เป็นเลเวล 80 และปลด E6 หรือไม่?",
"maxAllSuccess": "ปรับเต็มทุกตัวละครสำเร็จแล้ว!",
"avatarRankSuccess": "ปรับตัวละคร {id} เป็น E{rank} สำเร็จ",
"eqLcTitle": "🗡️ กรวยแสงในคลัง",
"eqLcTitle": "กรวยแสงในคลัง",
"eqLcSubtitle": "รายการกรวยแสงทั้งหมดจาก freesr-data.json",
"eqLcLoading": "กำลังโหลดข้อมูลกรวยแสง...",
"eqRelicTitle": "🛡️ รีลิกส์ในคลัง",
"eqRelicTitle": "รีลิกส์ในคลัง",
"eqRelicSubtitle": "รายการรีลิกส์ทั้งหมดจาก freesr-data.json",
"eqRelicLoading": "กำลังโหลดข้อมูลรีลิกส์...",
"itemCount": "{n} ชิ้น",
@@ -296,9 +296,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "พรสวรรค์",
"modalTracesTitle": "ร่องรอยและเทคนิค",
"modalTraceTechnique": "⚡ เทคนิค — เปิดใช้งานก่อนเริ่มการต่อสู้",
"traceA2": "🌟 ร่องรอยพิเศษ A2",
"traceA4": "🌟 ร่องรอยพิเศษ A4",
"traceA6": "🌟 ร่องรอยพิเศษ A6",
"traceA2": "ร่องรอยพิเศษ A2",
"traceA4": "ร่องรอยพิเศษ A4",
"traceA6": "ร่องรอยพิเศษ A6",
"traceStatNodes": "💎 โหนดสเตตัสเสริมทั้งหมด (10 โหนด)",
"lblLevelRange": "ระดับเลเวล (1-80)",
"lblPromotionRange": "การเลื่อนขั้น (0-6)",
@@ -400,13 +400,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "แนะนำ",
"overviewStatsTitle": "⚔️ ค่าพลังการต่อสู้ (Combat Stats)",
"overviewStatsNote": "รวมโบนัสกรวยแสงและรีลิกส์",
"overviewEquippedLc": "🗡️ กรวยแสงที่สวมใส่",
"overviewEquippedRelics": "🛡️ รีลิกส์และเซ็ต",
"overviewEquippedLc": "กรวยแสงที่สวมใส่",
"overviewEquippedRelics": "รีลิกส์และเซ็ต",
"modalRelicsTitle": "🛡️ สวมใส่รีลิกส์ (Relics 6 ชิ้น)",
"modalRelicsSubtitle": "คลิกแต่ละช่องเพื่อเลือกเปลี่ยน หรือสวมใส่เซ็ตแนะนำ",
"btnAutoEquipRelics": "⚡ สวมใส่เซ็ตแนะนำครบชุด",
"btnClearAllRelics": "🧹 ถอดทั้งหมด",
"activeSetEffects": "✨ เอฟเฟกต์เซ็ตที่เปิดใช้งาน",
"activeSetEffects": "เอฟเฟกต์เซ็ตที่เปิดใช้งาน",
"modalColLevel": "เลเวล",
"lblPromotion": "ขั้นเลื่อนขั้น",
"slotPickerTitle": "🛡️ เลือกสวมใส่รีลิกส์",
@@ -625,8 +625,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Added all 5★ signature light cones to inventory",
"toastTeleportedTrain": "Teleported to Astral Express Parlor Car",
"toastLineupCleared": "Lineup cleared successfully",
"modalAddLcTitle": "🗡️ Add Light Cone to FreeSR",
"modalAddRelicTitle": "🛡️ Add Relic to FreeSR",
"modalAddLcTitle": "Add Light Cone to FreeSR",
"modalAddRelicTitle": "Add Relic to FreeSR",
"lblSelectLc": "Select Light Cone",
"lblSelectRelicSet": "Select Relic Set",
"lblRelicSlot": "Relic Slot",
@@ -648,7 +648,7 @@ export const TRANSLATIONS = {
"connectFailed": "Connection Failed",
"tabDashboard": "📊 Dashboard & Party",
"tabAvatars": "👥 Characters",
"tabEquipment": "🗡️ Lightcones & Relics",
"tabEquipment": "Lightcones & Relics",
"tabInventory": "🎒 Inventory",
"inventoryTitle": "🎒 Item quantities",
"inventoryDescription": "Search by item ID, edit several quantities, then save once. Items come from the Client catalog used by the server. Leave a field empty to use the server default.",
@@ -707,10 +707,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Upgrade all characters to Level 80 and unlock E6?",
"maxAllSuccess": "All characters maximized successfully!",
"avatarRankSuccess": "Updated character {id} to E{rank}",
"eqLcTitle": "🗡️ Lightcones in Inventory",
"eqLcTitle": "Lightcones in Inventory",
"eqLcSubtitle": "All lightcones from freesr-data.json",
"eqLcLoading": "Loading lightcone data...",
"eqRelicTitle": "🛡️ Relics in Inventory",
"eqRelicTitle": "Relics in Inventory",
"eqRelicSubtitle": "All relics from freesr-data.json",
"eqRelicLoading": "Loading relic data...",
"itemCount": "{n} items",
@@ -792,9 +792,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Talent",
"modalTracesTitle": "Traces & Technique",
"modalTraceTechnique": "⚡ Technique — Pre-battle active",
"traceA2": "🌟 Bonus Trace A2",
"traceA4": "🌟 Bonus Trace A4",
"traceA6": "🌟 Bonus Trace A6",
"traceA2": "Bonus Trace A2",
"traceA4": "Bonus Trace A4",
"traceA6": "Bonus Trace A6",
"traceStatNodes": "💎 All Stat Bonus Nodes (10 nodes)",
"lblLevelRange": "Level (1-80)",
"lblPromotionRange": "Ascension / Promotion (0-6)",
@@ -896,13 +896,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Recommended",
"overviewStatsTitle": "⚔️ Combat Stats",
"overviewStatsNote": "Includes Lightcone & Relic bonuses",
"overviewEquippedLc": "🗡️ Equipped Lightcone",
"overviewEquippedRelics": "🛡️ Relics & Set Bonuses",
"overviewEquippedLc": "Equipped Lightcone",
"overviewEquippedRelics": "Relics & Set Bonuses",
"modalRelicsTitle": "🛡️ Equip Relics (6 Slots)",
"modalRelicsSubtitle": "Click each slot to change or equip recommended sets",
"btnAutoEquipRelics": "⚡ Equip Recommended Sets",
"btnClearAllRelics": "🧹 Unequip All",
"activeSetEffects": "✨ Active Set Bonuses",
"activeSetEffects": "Active Set Bonuses",
"modalColLevel": "Level",
"lblPromotion": "Ascension Rank",
"slotPickerTitle": "🛡️ Select Relic to Equip",
@@ -1005,8 +1005,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "已添加全部五星光锥至背包",
"toastTeleportedTrain": "已传送至星穹列车观景车厢",
"toastLineupCleared": "已清空编队角色",
"modalAddLcTitle": "🗡️ 添加光锥至 FreeSR",
"modalAddRelicTitle": "🛡️ 添加遗器至 FreeSR",
"modalAddLcTitle": "添加光锥至 FreeSR",
"modalAddRelicTitle": "添加遗器至 FreeSR",
"lblSelectLc": "选择光锥",
"lblSelectRelicSet": "选择遗器套装",
"lblRelicSlot": "遗器部位",
@@ -1028,7 +1028,7 @@ export const TRANSLATIONS = {
"connectFailed": "连接失败",
"tabDashboard": "📊 仪表盘与配队",
"tabAvatars": "👥 角色列表",
"tabEquipment": "🗡️ 光锥与遗器",
"tabEquipment": "光锥与遗器",
"tabPersistent": "💾 GaCy 存档管理",
"tabExrank": "⚡ Ex-Rank 特殊技能",
"tabJson": "📝 JSON 文件管理",
@@ -1064,10 +1064,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "确定将全部角色升至80级并解锁满命E6吗?",
"maxAllSuccess": "全角色养成满级成功!",
"avatarRankSuccess": "已调整角色 {id} 为星魂 {rank}",
"eqLcTitle": "🗡️ 背包中的光锥",
"eqLcTitle": "背包中的光锥",
"eqLcSubtitle": "来自 freesr-data.json 的所有光锥列表",
"eqLcLoading": "正在加载光锥数据...",
"eqRelicTitle": "🛡️ 背包中的遗器",
"eqRelicTitle": "背包中的遗器",
"eqRelicSubtitle": "来自 freesr-data.json 的所有遗器列表",
"eqRelicLoading": "正在加载遗器数据...",
"itemCount": "{n} 件",
@@ -1125,9 +1125,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "天赋",
"modalTracesTitle": "行迹与秘技",
"modalTraceTechnique": "⚡ 秘技 — 进入战斗前生效",
"traceA2": "🌟 额外能力 A2",
"traceA4": "🌟 额外能力 A4",
"traceA6": "🌟 额外能力 A6",
"traceA2": "额外能力 A2",
"traceA4": "额外能力 A4",
"traceA6": "额外能力 A6",
"traceStatNodes": "💎 全部属性加成节点 (10个)",
"lblLevelRange": "角色等级 (1-80)",
"lblPromotionRange": "晋阶等级 (0-6)",
@@ -1266,13 +1266,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "推荐",
"overviewStatsTitle": "⚔️ 战斗面板 (Combat Stats)",
"overviewStatsNote": "已计入光锥与遗器属性加成",
"overviewEquippedLc": "🗡️ 当前装备光锥",
"overviewEquippedRelics": "🛡️ 遗器套装效果",
"overviewEquippedLc": "当前装备光锥",
"overviewEquippedRelics": "遗器套装效果",
"modalRelicsTitle": "🛡️ 遗器配装 (6 部位)",
"modalRelicsSubtitle": "点击单个部位更换,或一键穿戴推荐套装",
"btnAutoEquipRelics": "⚡ 一键穿戴推荐套装",
"btnClearAllRelics": "🧹 一键卸下全部",
"activeSetEffects": "✨ 当前生效的套装效果",
"activeSetEffects": "当前生效的套装效果",
"modalColLevel": "等级",
"lblPromotion": "突破阶段",
"slotPickerTitle": "🛡️ 选择要装备的遗器",
@@ -1355,8 +1355,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "已新增全部五星光錐至背包",
"toastTeleportedTrain": "已傳送至星穹列車觀景車廂",
"toastLineupCleared": "已清空隊伍角色",
"modalAddLcTitle": "🗡️ 新增光錐至 FreeSR",
"modalAddRelicTitle": "🛡️ 新增遺器至 FreeSR",
"modalAddLcTitle": "新增光錐至 FreeSR",
"modalAddRelicTitle": "新增遺器至 FreeSR",
"lblSelectLc": "選擇光錐",
"lblSelectRelicSet": "選擇遺器套裝",
"lblRelicSlot": "遺器部位",
@@ -1378,7 +1378,7 @@ export const TRANSLATIONS = {
"connectFailed": "連線失敗",
"tabDashboard": "📊 儀表板與配隊",
"tabAvatars": "👥 角色列表",
"tabEquipment": "🗡️ 光錐與遺器",
"tabEquipment": "光錐與遺器",
"tabPersistent": "💾 GaCy 存檔管理",
"tabExrank": "⚡ Ex-Rank 特殊技能",
"tabJson": "📝 JSON 檔案管理",
@@ -1414,10 +1414,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "確定將全部角色升至80級並解鎖滿魂E6嗎?",
"maxAllSuccess": "全角色養成滿級成功!",
"avatarRankSuccess": "已調整角色 {id} 為星魂 {rank}",
"eqLcTitle": "🗡️ 背包中的光錐",
"eqLcTitle": "背包中的光錐",
"eqLcSubtitle": "來自 freesr-data.json 的所有光錐列表",
"eqLcLoading": "正在載入光錐資料...",
"eqRelicTitle": "🛡️ 背包中的遺器",
"eqRelicTitle": "背包中的遺器",
"eqRelicSubtitle": "來自 freesr-data.json 的所有遺器列表",
"eqRelicLoading": "正在載入遺器資料...",
"itemCount": "{n} 件",
@@ -1475,9 +1475,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "天賦",
"modalTracesTitle": "行跡與秘技",
"modalTraceTechnique": "⚡ 秘技 — 進入戰鬥前生效",
"traceA2": "🌟 額外能力 A2",
"traceA4": "🌟 額外能力 A4",
"traceA6": "🌟 額外能力 A6",
"traceA2": "額外能力 A2",
"traceA4": "額外能力 A4",
"traceA6": "額外能力 A6",
"traceStatNodes": "💎 全部屬性加成節點 (10個)",
"lblLevelRange": "角色等級 (1-80)",
"lblPromotionRange": "晉階等級 (0-6)",
@@ -1616,13 +1616,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "推薦",
"overviewStatsTitle": "⚔️ 戰鬥面板 (Combat Stats)",
"overviewStatsNote": "已計入光錐與遺器屬性加成",
"overviewEquippedLc": "🗡️ 當前裝備光錐",
"overviewEquippedRelics": "🛡️ 遺器套裝效果",
"overviewEquippedLc": "當前裝備光錐",
"overviewEquippedRelics": "遺器套裝效果",
"modalRelicsTitle": "🛡️ 遺器配裝 (6 部位)",
"modalRelicsSubtitle": "點擊單一部位更換,或一鍵穿戴推薦套裝",
"btnAutoEquipRelics": "⚡ 一鍵穿戴推薦套裝",
"btnClearAllRelics": "🧹 一鍵卸下全部",
"activeSetEffects": "✨ 當前生效的套裝效果",
"activeSetEffects": "當前生效的套裝效果",
"modalColLevel": "等級",
"lblPromotion": "突破階段",
"slotPickerTitle": "🛡️ 選擇要裝備的遺器",
@@ -1705,8 +1705,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "全★5光円錐を追加しました",
"toastTeleportedTrain": "星穹列車の客車へテレポートしました",
"toastLineupCleared": "編成をクリアしました",
"modalAddLcTitle": "🗡️ FreeSRに光円錐を追加",
"modalAddRelicTitle": "🛡️ FreeSRに遺物を追加",
"modalAddLcTitle": "FreeSRに光円錐を追加",
"modalAddRelicTitle": "FreeSRに遺物を追加",
"lblSelectLc": "光円錐を選択",
"lblSelectRelicSet": "遺物セットを選択",
"lblRelicSlot": "部位",
@@ -1728,7 +1728,7 @@ export const TRANSLATIONS = {
"connectFailed": "接続失敗",
"tabDashboard": "📊 ダッシュボード・編成",
"tabAvatars": "👥 キャラクター",
"tabEquipment": "🗡️ 光円錐と遺物",
"tabEquipment": "光円錐と遺物",
"tabPersistent": "💾 GaCy セーブ管理",
"tabExrank": "⚡ Ex-Rank 特殊スキル",
"tabJson": "📝 JSON 管理",
@@ -1764,10 +1764,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "全キャラクターをレベル80・星魂E6まで育成しますか?",
"maxAllSuccess": "全キャラクターの最大化が完了しました!",
"avatarRankSuccess": "キャラクター {id} を星魂 {rank} に変更しました",
"eqLcTitle": "🗡️ 所持光円錐一覧",
"eqLcTitle": "所持光円錐一覧",
"eqLcSubtitle": "freesr-data.json からの全光円錐一覧",
"eqLcLoading": "光円錐データを読み込み中...",
"eqRelicTitle": "🛡️ 所持遺物一覧",
"eqRelicTitle": "所持遺物一覧",
"eqRelicSubtitle": "freesr-data.json からの全遺物一覧",
"eqRelicLoading": "遺物データを読み込み中...",
"itemCount": "{n} 個",
@@ -1825,9 +1825,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "天賦",
"modalTracesTitle": "軌跡と秘技",
"modalTraceTechnique": "⚡ 秘技 — 戦闘突入前に有効化",
"traceA2": "🌟 追加能力 A2",
"traceA4": "🌟 追加能力 A4",
"traceA6": "🌟 追加能力 A6",
"traceA2": "追加能力 A2",
"traceA4": "追加能力 A4",
"traceA6": "追加能力 A6",
"traceStatNodes": "💎 全ステータスボーナス (10個)",
"lblLevelRange": "レベル (1-80)",
"lblPromotionRange": "昇格段階 (0-6)",
@@ -1966,13 +1966,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "推奨",
"overviewStatsTitle": "⚔️ 戦闘ステータス (Combat Stats)",
"overviewStatsNote": "光円錐および遺物のボーナスを含みます",
"overviewEquippedLc": "🗡️ 装備中の光円錐",
"overviewEquippedRelics": "🛡️ 遺物&セット効果",
"overviewEquippedLc": "装備中の光円錐",
"overviewEquippedRelics": "遺物&セット効果",
"modalRelicsTitle": "🛡️ 遺物装備 (全6スロット)",
"modalRelicsSubtitle": "各スロットをクリックして変更、または推奨セットを一括装備",
"btnAutoEquipRelics": "⚡ 推奨セットを一括装備",
"btnClearAllRelics": "🧹 すべて外す",
"activeSetEffects": "✨ 発動中のセット効果",
"activeSetEffects": "発動中のセット効果",
"modalColLevel": "レベル",
"lblPromotion": "昇格段階",
"slotPickerTitle": "🛡️ 装備する遺物を選択",
@@ -2055,8 +2055,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "모든 5★ 광추를 추가했습니다",
"toastTeleportedTrain": "은하열차 객차로 워프했습니다",
"toastLineupCleared": "파티를 초기화했습니다",
"modalAddLcTitle": "🗡️ FreeSR에 광추 추가",
"modalAddRelicTitle": "🛡️ FreeSR에 유물 추가",
"modalAddLcTitle": "FreeSR에 광추 추가",
"modalAddRelicTitle": "FreeSR에 유물 추가",
"lblSelectLc": "광추 선택",
"lblSelectRelicSet": "유물 세트 선택",
"lblRelicSlot": "부위",
@@ -2078,7 +2078,7 @@ export const TRANSLATIONS = {
"connectFailed": "연결 실패",
"tabDashboard": "📊 대시보드 및 파티",
"tabAvatars": "👥 캐릭터",
"tabEquipment": "🗡️ 광추 및 유물",
"tabEquipment": "광추 및 유물",
"tabPersistent": "💾 GaCy 세이브 관리",
"tabExrank": "⚡ Ex-Rank 특수 스킬",
"tabJson": "📝 JSON 관리",
@@ -2114,10 +2114,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "모든 캐릭터를 레벨 80 및 성흔 6으로 육성하시겠습니까?",
"maxAllSuccess": "모든 캐릭터 최대치 달성 완료!",
"avatarRankSuccess": "캐릭터 {id} 성흔을 {rank}(으)로 변경했습니다",
"eqLcTitle": "🗡️ 보유 광추 목록",
"eqLcTitle": "보유 광추 목록",
"eqLcSubtitle": "freesr-data.json의 모든 광추 목록",
"eqLcLoading": "광추 데이터 불러오는 중...",
"eqRelicTitle": "🛡️ 보유 유물 목록",
"eqRelicTitle": "보유 유물 목록",
"eqRelicSubtitle": "freesr-data.json의 모든 유물 목록",
"eqRelicLoading": "유물 데이터 불러오는 중...",
"itemCount": "{n}개",
@@ -2175,9 +2175,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "특성",
"modalTracesTitle": "행적 및 비술",
"modalTraceTechnique": "⚡ 비술 — 전투 진입 전 활성화",
"traceA2": "🌟 추가 능력 A2",
"traceA4": "🌟 추가 능력 A4",
"traceA6": "🌟 추가 능력 A6",
"traceA2": "추가 능력 A2",
"traceA4": "추가 능력 A4",
"traceA6": "추가 능력 A6",
"traceStatNodes": "💎 모든 스탯 보너스 노드 (10개)",
"lblLevelRange": "레벨 (1-80)",
"lblPromotionRange": "승급 단계 (0-6)",
@@ -2316,13 +2316,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "추천",
"overviewStatsTitle": "⚔️ 전투 스탯 (Combat Stats)",
"overviewStatsNote": "광추 및 유물 보너스 포함",
"overviewEquippedLc": "🗡️ 장착 중인 광추",
"overviewEquippedRelics": "🛡️ 유물 및 세트 효과",
"overviewEquippedLc": "장착 중인 광추",
"overviewEquippedRelics": "유물 및 세트 효과",
"modalRelicsTitle": "🛡️ 유물 장착 (6개 부위)",
"modalRelicsSubtitle": "각 부위를 클릭하여 변경하거나 추천 세트를 일괄 장착",
"btnAutoEquipRelics": "⚡ 추천 세트 일괄 장착",
"btnClearAllRelics": "🧹 모두 해제",
"activeSetEffects": "✨ 활성화된 세트 효과",
"activeSetEffects": "활성화된 세트 효과",
"modalColLevel": "레벨",
"lblPromotion": "승급 단계",
"slotPickerTitle": "🛡️ 장착할 유물 선택",
@@ -2405,8 +2405,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Đã thêm toàn bộ Nón Ánh Sáng 5 sao",
"toastTeleportedTrain": "Đã dịch chuyển về Toa Tàu Astral",
"toastLineupCleared": "Đã xóa sạch đội hình",
"modalAddLcTitle": "🗡️ Thêm Nón Ánh Sáng vào FreeSR",
"modalAddRelicTitle": "🛡️ Thêm Di Vật vào FreeSR",
"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",
@@ -2428,7 +2428,7 @@ export const TRANSLATIONS = {
"connectFailed": "Kết nối thất bại",
"tabDashboard": "📊 Tổng quan & Đội hình",
"tabAvatars": "👥 Nhân vật",
"tabEquipment": "🗡️ Nón Ánh Sáng & Di Vật",
"tabEquipment": "Nón Ánh Sáng & Di Vật",
"tabPersistent": "💾 Quản lý lưu GaCy",
"tabExrank": "⚡ Kỹ Năng Ex-Rank",
"tabJson": "📝 Quản lý JSON",
@@ -2464,10 +2464,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Nâng cấp tất cả nhân vật lên Cấp 80 và Tinh Hồn 6?",
"maxAllSuccess": "Tối đa tất cả nhân vật thành công!",
"avatarRankSuccess": "Đã chỉnh nhân vật {id} thành Tinh Hồn {rank}",
"eqLcTitle": "🗡️ Nón Ánh Sáng trong kho",
"eqLcTitle": "Nón Ánh Sáng trong kho",
"eqLcSubtitle": "Tất cả Nón Ánh Sáng từ freesr-data.json",
"eqLcLoading": "Đang tải dữ liệu Nón Ánh Sáng...",
"eqRelicTitle": "🛡️ Di Vật trong kho",
"eqRelicTitle": "Di Vật trong kho",
"eqRelicSubtitle": "Tất cả Di Vật từ freesr-data.json",
"eqRelicLoading": "Đang tải dữ liệu Di Vật...",
"itemCount": "{n} món",
@@ -2525,9 +2525,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Thiên Phú",
"modalTracesTitle": "Vết Tích & Bí Kỹ",
"modalTraceTechnique": "⚡ Bí Kỹ — Kích hoạt trước trận",
"traceA2": "🌟 Vết Tích Phụ A2",
"traceA4": "🌟 Vết Tích Phụ A4",
"traceA6": "🌟 Vết Tích Phụ A6",
"traceA2": "Vết Tích Phụ A2",
"traceA4": "Vết Tích Phụ A4",
"traceA6": "Vết Tích Phụ A6",
"traceStatNodes": "💎 Tất cả nhánh tăng chỉ số (10 nhánh)",
"lblLevelRange": "Cấp độ (1-80)",
"lblPromotionRange": "Đột phá (0-6)",
@@ -2666,13 +2666,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Đề Xuất",
"overviewStatsTitle": "⚔️ Chỉ số chiến đấu (Combat Stats)",
"overviewStatsNote": "Bao gồm chỉ số từ Nón Ánh Sáng và Di Vật",
"overviewEquippedLc": "🗡️ Nón Ánh Sáng đang trang bị",
"overviewEquippedRelics": "🛡️ Hiệu ứng bộ Di Vật",
"overviewEquippedLc": "Nón Ánh Sáng đang trang bị",
"overviewEquippedRelics": "Hiệu ứng bộ Di Vật",
"modalRelicsTitle": "🛡️ Trang bị Di Vật (6 vị trí)",
"modalRelicsSubtitle": "Nhấn vào từng ô để thay đổi hoặc trang bị bộ đề xuất",
"btnAutoEquipRelics": "⚡ Trang bị bộ đề xuất",
"btnClearAllRelics": "🧹 Tháo toàn bộ",
"activeSetEffects": "✨ Hiệu ứng bộ đang kích hoạt",
"activeSetEffects": "Hiệu ứng bộ đang kích hoạt",
"modalColLevel": "Cấp độ",
"lblPromotion": "Cấp đột phá",
"slotPickerTitle": "🛡️ Chọn Di Vật để trang bị",
@@ -2755,8 +2755,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Berhasil menambahkan semua Light Cone 5★",
"toastTeleportedTrain": "Teleportasi ke Parlor Car berhasil",
"toastLineupCleared": "Susunan tim berhasil dikosongkan",
"modalAddLcTitle": "🗡️ Tambah Light Cone ke FreeSR",
"modalAddRelicTitle": "🛡️ Tambah Relic ke FreeSR",
"modalAddLcTitle": "Tambah Light Cone ke FreeSR",
"modalAddRelicTitle": "Tambah Relic ke FreeSR",
"lblSelectLc": "Pilih Light Cone",
"lblSelectRelicSet": "Pilih Set Relic",
"lblRelicSlot": "Slot Relic",
@@ -2778,7 +2778,7 @@ export const TRANSLATIONS = {
"connectFailed": "Koneksi Gagal",
"tabDashboard": "📊 Dasbor & Tim",
"tabAvatars": "👥 Karakter",
"tabEquipment": "🗡️ Light Cone & Relic",
"tabEquipment": "Light Cone & Relic",
"tabPersistent": "💾 Pengelola Simpan GaCy",
"tabExrank": "⚡ Keahlian Ex-Rank",
"tabJson": "📝 Manajemen JSON",
@@ -2814,10 +2814,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Tingkatkan semua karakter ke Lv. 80 dan buka E6?",
"maxAllSuccess": "Semua karakter berhasil dimaksimalkan!",
"avatarRankSuccess": "Karakter {id} diperbarui ke E{rank}",
"eqLcTitle": "🗡️ Light Cone di Inventaris",
"eqLcTitle": "Light Cone di Inventaris",
"eqLcSubtitle": "Semua light cone dari freesr-data.json",
"eqLcLoading": "Memuat data light cone...",
"eqRelicTitle": "🛡️ Relic di Inventaris",
"eqRelicTitle": "Relic di Inventaris",
"eqRelicSubtitle": "Semua relic dari freesr-data.json",
"eqRelicLoading": "Memuat data relic...",
"itemCount": "{n} item",
@@ -2875,9 +2875,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Talent",
"modalTracesTitle": "Traces & Technique",
"modalTraceTechnique": "⚡ Technique — Aktif sebelum tempur",
"traceA2": "🌟 Bonus Trace A2",
"traceA4": "🌟 Bonus Trace A4",
"traceA6": "🌟 Bonus Trace A6",
"traceA2": "Bonus Trace A2",
"traceA4": "Bonus Trace A4",
"traceA6": "Bonus Trace A6",
"traceStatNodes": "💎 Semua Node Bonus Stat (10 node)",
"lblLevelRange": "Level (1-80)",
"lblPromotionRange": "Promosi (0-6)",
@@ -3016,13 +3016,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Rekomendasi",
"overviewStatsTitle": "⚔️ Statistik Tempur (Combat Stats)",
"overviewStatsNote": "Termasuk bonus Light Cone & Relic",
"overviewEquippedLc": "🗡️ Light Cone Terpasang",
"overviewEquippedRelics": "🛡️ Bonus Set Relic",
"overviewEquippedLc": "Light Cone Terpasang",
"overviewEquippedRelics": "Bonus Set Relic",
"modalRelicsTitle": "🛡️ Pasang Relic (6 Slot)",
"modalRelicsSubtitle": "Klik setiap slot untuk mengganti atau pasang set rekomendasi",
"btnAutoEquipRelics": "⚡ Pasang Set Rekomendasi",
"btnClearAllRelics": "🧹 Lepas Semua",
"activeSetEffects": "✨ Efek Set yang Aktif",
"activeSetEffects": "Efek Set yang Aktif",
"modalColLevel": "Level",
"lblPromotion": "Peringkat Ascension",
"slotPickerTitle": "🛡️ Pilih Relic untuk Dipasang",
@@ -3105,8 +3105,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Todos los conos de 5★ añadidos",
"toastTeleportedTrain": "Teletransportado al Expreso Astral",
"toastLineupCleared": "Equipo vaciado correctamente",
"modalAddLcTitle": "🗡️ Añadir cono de luz a FreeSR",
"modalAddRelicTitle": "🛡️ Añadir reliquia a FreeSR",
"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",
@@ -3128,7 +3128,7 @@ export const TRANSLATIONS = {
"connectFailed": "Conexión fallida",
"tabDashboard": "📊 Panel y equipo",
"tabAvatars": "👥 Personajes",
"tabEquipment": "🗡️ Conos de luz y artefactos",
"tabEquipment": "Conos de luz y artefactos",
"tabPersistent": "💾 Gestor de guardado GaCy",
"tabExrank": "⚡ Habilidades Ex-Rank",
"tabJson": "📝 Gestión de JSON",
@@ -3164,10 +3164,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "¿Subir todos los personajes a nivel 80 y desbloquear E6?",
"maxAllSuccess": "¡Todos los personajes maximizados con éxito!",
"avatarRankSuccess": "Personaje {id} actualizado a E{rank}",
"eqLcTitle": "🗡️ Conos de luz en inventario",
"eqLcTitle": "Conos de luz en inventario",
"eqLcSubtitle": "Todos los conos de luz de freesr-data.json",
"eqLcLoading": "Cargando datos de conos de luz...",
"eqRelicTitle": "🛡️ Artefactos en inventario",
"eqRelicTitle": "Artefactos en inventario",
"eqRelicSubtitle": "Todos los artefactos de freesr-data.json",
"eqRelicLoading": "Cargando datos de artefactos...",
"itemCount": "{n} objetos",
@@ -3225,9 +3225,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Talento",
"modalTracesTitle": "Rastros y técnica",
"modalTraceTechnique": "⚡ Técnica — Activa antes de la batalla",
"traceA2": "🌟 Habilidad adicional A2",
"traceA4": "🌟 Habilidad adicional A4",
"traceA6": "🌟 Habilidad adicional A6",
"traceA2": "Habilidad adicional A2",
"traceA4": "Habilidad adicional A4",
"traceA6": "Habilidad adicional A6",
"traceStatNodes": "💎 Todos los nodos de atributos (10 nodos)",
"lblLevelRange": "Nivel (1-80)",
"lblPromotionRange": "Ascensión (0-6)",
@@ -3366,13 +3366,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Recomendado",
"overviewStatsTitle": "⚔️ Estadísticas de combate",
"overviewStatsNote": "Incluye bonificaciones de Cono y Artefactos",
"overviewEquippedLc": "🗡️ Cono de Luz equipado",
"overviewEquippedRelics": "🛡️ Artefactos y efectos de set",
"overviewEquippedLc": "Cono de Luz equipado",
"overviewEquippedRelics": "Artefactos y efectos de set",
"modalRelicsTitle": "🛡️ Equipar artefactos (6 ranuras)",
"modalRelicsSubtitle": "Haz clic en cada ranura para cambiar o equipar sets recomendados",
"btnAutoEquipRelics": "⚡ Equipar sets recomendados",
"btnClearAllRelics": "🧹 Desequipar todo",
"activeSetEffects": "✨ Efectos de set activos",
"activeSetEffects": "Efectos de set activos",
"modalColLevel": "Nivel",
"lblPromotion": "Rango de ascensión",
"slotPickerTitle": "🛡️ Seleccionar artefacto para equipar",
@@ -3455,8 +3455,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Tous les cônes de lumière 5★ ajoutés",
"toastTeleportedTrain": "Téléporté au wagon-salon de l'Astral Express",
"toastLineupCleared": "Équipe réinitialisée",
"modalAddLcTitle": "🗡️ Ajouter cône de lumière à FreeSR",
"modalAddRelicTitle": "🛡️ Ajouter relique à FreeSR",
"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",
@@ -3478,7 +3478,7 @@ export const TRANSLATIONS = {
"connectFailed": "Échec de connexion",
"tabDashboard": "📊 Tableau de bord et équipe",
"tabAvatars": "👥 Personnages",
"tabEquipment": "🗡️ Cônes de lumière et reliques",
"tabEquipment": "Cônes de lumière et reliques",
"tabPersistent": "💾 Gestionnaire de sauvegarde GaCy",
"tabExrank": "⚡ Compétences Ex-Rank",
"tabJson": "📝 Gestion JSON",
@@ -3514,10 +3514,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Améliorer tous les personnages au niveau 80 et débloquer E6 ?",
"maxAllSuccess": "Tous les personnages sont maximisés avec succès !",
"avatarRankSuccess": "Personnage {id} mis à jour au rang E{rank}",
"eqLcTitle": "🗡️ Cônes de lumière dans l'inventaire",
"eqLcTitle": "Cônes de lumière dans l'inventaire",
"eqLcSubtitle": "Tous les cônes de lumière de freesr-data.json",
"eqLcLoading": "Chargement des cônes de lumière...",
"eqRelicTitle": "🛡️ Reliques dans l'inventaire",
"eqRelicTitle": "Reliques dans l'inventaire",
"eqRelicSubtitle": "Toutes les reliques de freesr-data.json",
"eqRelicLoading": "Chargement des reliques...",
"itemCount": "{n} objets",
@@ -3575,9 +3575,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Talent",
"modalTracesTitle": "Traces et technique",
"modalTraceTechnique": "⚡ Technique — Active avant le combat",
"traceA2": "🌟 Capacité bonus A2",
"traceA4": "🌟 Capacité bonus A4",
"traceA6": "🌟 Capacité bonus A6",
"traceA2": "Capacité bonus A2",
"traceA4": "Capacité bonus A4",
"traceA6": "Capacité bonus A6",
"traceStatNodes": "💎 Tous les nœuds de statistiques (10 nœuds)",
"lblLevelRange": "Niveau (1-80)",
"lblPromotionRange": "Élévation (0-6)",
@@ -3716,13 +3716,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Recommandé",
"overviewStatsTitle": "⚔️ Statistiques de combat",
"overviewStatsNote": "Inclut les bonus de Cône et de Reliques",
"overviewEquippedLc": "🗡️ Cône de Lumière équipé",
"overviewEquippedRelics": "🛡️ Reliques et bonus de set",
"overviewEquippedLc": "Cône de Lumière équipé",
"overviewEquippedRelics": "Reliques et bonus de set",
"modalRelicsTitle": "🛡️ Équiper reliques (6 emplacements)",
"modalRelicsSubtitle": "Cliquez sur chaque emplacement pour modifier ou équiper les sets recommandés",
"btnAutoEquipRelics": "⚡ Équiper sets recommandés",
"btnClearAllRelics": "🧹 Tout déséquiper",
"activeSetEffects": "✨ Bonus de set actifs",
"activeSetEffects": "Bonus de set actifs",
"modalColLevel": "Niveau",
"lblPromotion": "Rang d'élévation",
"slotPickerTitle": "🛡️ Sélectionner une relique à équiper",
@@ -3805,8 +3805,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Alle 5★-Lichtkegel hinzugefügt",
"toastTeleportedTrain": "Zum Astralexpress teleportiert",
"toastLineupCleared": "Team-Plätze geleert",
"modalAddLcTitle": "🗡️ Lichtkegel zu FreeSR hinzufügen",
"modalAddRelicTitle": "🛡️ Relikt zu FreeSR hinzufügen",
"modalAddLcTitle": "Lichtkegel zu FreeSR hinzufügen",
"modalAddRelicTitle": "Relikt zu FreeSR hinzufügen",
"lblSelectLc": "Lichtkegel auswählen",
"lblSelectRelicSet": "Reliktset auswählen",
"lblRelicSlot": "Reliktslot",
@@ -3828,7 +3828,7 @@ export const TRANSLATIONS = {
"connectFailed": "Verbindung fehlgeschlagen",
"tabDashboard": "📊 Dashboard & Team",
"tabAvatars": "👥 Charaktere",
"tabEquipment": "🗡️ Lichtkegel & Relikte",
"tabEquipment": "Lichtkegel & Relikte",
"tabPersistent": "💾 GaCy-Speicher-Manager",
"tabExrank": "⚡ Ex-Rank-Fähigkeiten",
"tabJson": "📝 JSON-Verwaltung",
@@ -3864,10 +3864,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Alle Charaktere auf Stufe 80 aufwerten und E6 freischalten?",
"maxAllSuccess": "Alle Charaktere erfolgreich maximiert!",
"avatarRankSuccess": "Charakter {id} auf E{rank} aktualisiert",
"eqLcTitle": "🗡️ Lichtkegel im Inventar",
"eqLcTitle": "Lichtkegel im Inventar",
"eqLcSubtitle": "Alle Lichtkegel aus freesr-data.json",
"eqLcLoading": "Lichtkegeldaten werden geladen...",
"eqRelicTitle": "🛡️ Relikte im Inventar",
"eqRelicTitle": "Relikte im Inventar",
"eqRelicSubtitle": "Alle Relikte aus freesr-data.json",
"eqRelicLoading": "Reliktdaten werden geladen...",
"itemCount": "{n} Gegenstände",
@@ -3925,9 +3925,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Talent",
"modalTracesTitle": "Spuren & Technik",
"modalTraceTechnique": "⚡ Technik — Vor dem Kampf aktiv",
"traceA2": "🌟 Zusatzfähigkeit A2",
"traceA4": "🌟 Zusatzfähigkeit A4",
"traceA6": "🌟 Zusatzfähigkeit A6",
"traceA2": "Zusatzfähigkeit A2",
"traceA4": "Zusatzfähigkeit A4",
"traceA6": "Zusatzfähigkeit A6",
"traceStatNodes": "💎 Alle Attributs-Bonus-Knoten (10 Knoten)",
"lblLevelRange": "Stufe (1-80)",
"lblPromotionRange": "Aufstieg (0-6)",
@@ -4066,13 +4066,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Empfohlen",
"overviewStatsTitle": "⚔️ Kampfwerte (Combat Stats)",
"overviewStatsNote": "Enthält Boni von Lichtkegel und Relikten",
"overviewEquippedLc": "🗡️ Ausgerüsteter Lichtkegel",
"overviewEquippedRelics": "🛡️ Relikte & Set-Boni",
"overviewEquippedLc": "Ausgerüsteter Lichtkegel",
"overviewEquippedRelics": "Relikte & Set-Boni",
"modalRelicsTitle": "🛡️ Relikte ausrüsten (6 Plätze)",
"modalRelicsSubtitle": "Klicken Sie auf einen Platz zum Wechseln oder empfohlene Sets ausrüsten",
"btnAutoEquipRelics": "⚡ Empfohlene Sets anlegen",
"btnClearAllRelics": "🧹 Alles ablegen",
"activeSetEffects": "✨ Aktive Set-Boni",
"activeSetEffects": "Aktive Set-Boni",
"modalColLevel": "Stufe",
"lblPromotion": "Aufstiegsrang",
"slotPickerTitle": "🛡️ Relikt zum Ausrüsten wählen",
@@ -4155,8 +4155,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Все 5★ световые конусы добавлены",
"toastTeleportedTrain": "Телепортирован в общий вагон экспресса",
"toastLineupCleared": "Слоты отряда очищены",
"modalAddLcTitle": "🗡️ Добавить световой конус в FreeSR",
"modalAddRelicTitle": "🛡️ Добавить реликвию в FreeSR",
"modalAddLcTitle": "Добавить световой конус в FreeSR",
"modalAddRelicTitle": "Добавить реликвию в FreeSR",
"lblSelectLc": "Выберите световой конус",
"lblSelectRelicSet": "Выберите набор реликвий",
"lblRelicSlot": "Слот реликвии",
@@ -4178,7 +4178,7 @@ export const TRANSLATIONS = {
"connectFailed": "Ошибка подключения",
"tabDashboard": "📊 Панель и отряд",
"tabAvatars": "👥 Персонажи",
"tabEquipment": "🗡️ Световые конусы и реликвии",
"tabEquipment": "Световые конусы и реликвии",
"tabPersistent": "💾 Управление сохранениями GaCy",
"tabExrank": "⚡ Особые навыки Ex-Rank",
"tabJson": "📝 Управление JSON",
@@ -4214,10 +4214,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Повысить всех персонажей до 80 уровня и разблокировать E6?",
"maxAllSuccess": "Все персонажи успешно прокачаны!",
"avatarRankSuccess": "Персонаж {id} повышен до эйдолона {rank}",
"eqLcTitle": "🗡️ Световые конусы в инвентаре",
"eqLcTitle": "Световые конусы в инвентаре",
"eqLcSubtitle": "Все световые конусы из freesr-data.json",
"eqLcLoading": "Загрузка конусов...",
"eqRelicTitle": "🛡️ Реликвии в инвентаре",
"eqRelicTitle": "Реликвии в инвентаре",
"eqRelicSubtitle": "Все реликвии из freesr-data.json",
"eqRelicLoading": "Загрузка реликвий...",
"itemCount": "{n} шт.",
@@ -4275,9 +4275,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Талант",
"modalTracesTitle": "Следы и техника",
"modalTraceTechnique": "⚡ Техника — Активна перед боем",
"traceA2": "🌟 Доп. способность A2",
"traceA4": "🌟 Доп. способность A4",
"traceA6": "🌟 Доп. способность A6",
"traceA2": "Доп. способность A2",
"traceA4": "Доп. способность A4",
"traceA6": "Доп. способность A6",
"traceStatNodes": "💎 Все узлы усиления параметров (10 узлов)",
"lblLevelRange": "Уровень (1-80)",
"lblPromotionRange": "Возвышение (0-6)",
@@ -4416,13 +4416,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Рекомендуется",
"overviewStatsTitle": "⚔️ Боевые характеристики",
"overviewStatsNote": "Включает бонусы конуса и реликвий",
"overviewEquippedLc": "🗡️ Надетый Световой конус",
"overviewEquippedRelics": "🛡️ Реликвии и бонусы комплектов",
"overviewEquippedLc": "Надетый Световой конус",
"overviewEquippedRelics": "Реликвии и бонусы комплектов",
"modalRelicsTitle": "🛡️ Экипировка реликвий (6 слотов)",
"modalRelicsSubtitle": "Нажмите на слот для смены или наденьте рекомендованные комплекты",
"btnAutoEquipRelics": "⚡ Надеть рекомендованные сеты",
"btnClearAllRelics": "🧹 Снять всё",
"activeSetEffects": "✨ Активные эффекты комплектов",
"activeSetEffects": "Активные эффекты комплектов",
"modalColLevel": "Уровень",
"lblPromotion": "Ранг возвышения",
"slotPickerTitle": "🛡️ Выбор реликвии для экипировки",
@@ -4505,8 +4505,8 @@ export const TRANSLATIONS = {
"toastAddedAllLcs": "Todos os Cones de Luz 5★ adicionados",
"toastTeleportedTrain": "Teleportado para o Expresso Astral",
"toastLineupCleared": "Equipe limpa com sucesso",
"modalAddLcTitle": "🗡️ Adicionar Cone de Luz ao FreeSR",
"modalAddRelicTitle": "🛡️ Adicionar Relíquia ao FreeSR",
"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",
@@ -4528,7 +4528,7 @@ export const TRANSLATIONS = {
"connectFailed": "Falha na Conexão",
"tabDashboard": "📊 Painel e Equipe",
"tabAvatars": "👥 Personagens",
"tabEquipment": "🗡️ Cones de Luz e Relíquias",
"tabEquipment": "Cones de Luz e Relíquias",
"tabPersistent": "💾 Gerenciador de Salvamento GaCy",
"tabExrank": "⚡ Habilidades Ex-Rank",
"tabJson": "📝 Gerenciamento de JSON",
@@ -4564,10 +4564,10 @@ export const TRANSLATIONS = {
"confirmMaxAll": "Evoluir todos os personagens para o Nível 80 e desbloquear E6?",
"maxAllSuccess": "Todos os personagens foram maximizados com sucesso!",
"avatarRankSuccess": "Personagem {id} atualizado para E{rank}",
"eqLcTitle": "🗡️ Cones de Luz no Inventário",
"eqLcTitle": "Cones de Luz no Inventário",
"eqLcSubtitle": "Todos os cones de luz de freesr-data.json",
"eqLcLoading": "Carregando dados dos cones de luz...",
"eqRelicTitle": "🛡️ Relíquias no Inventário",
"eqRelicTitle": "Relíquias no Inventário",
"eqRelicSubtitle": "Todas as relíquias de freesr-data.json",
"eqRelicLoading": "Carregando dados das relíquias...",
"itemCount": "{n} itens",
@@ -4625,9 +4625,9 @@ export const TRANSLATIONS = {
"modalSkillTalent": "Talento",
"modalTracesTitle": "Rastros e Técnica",
"modalTraceTechnique": "⚡ Técnica — Ativa pré-combate",
"traceA2": "🌟 Habilidade Bônus A2",
"traceA4": "🌟 Habilidade Bônus A4",
"traceA6": "🌟 Habilidade Bônus A6",
"traceA2": "Habilidade Bônus A2",
"traceA4": "Habilidade Bônus A4",
"traceA6": "Habilidade Bônus A6",
"traceStatNodes": "💎 Todos os nós de atributos bônus (10 nós)",
"lblLevelRange": "Nível (1-80)",
"lblPromotionRange": "Ascensão (0-6)",
@@ -4766,13 +4766,13 @@ export const TRANSLATIONS = {
"recommendedBadge": "Recomendado",
"overviewStatsTitle": "⚔️ Atributos de Combate",
"overviewStatsNote": "Inclui bônus de Cone de Luz e Relíquias",
"overviewEquippedLc": "🗡️ Cone de Luz Equipado",
"overviewEquippedRelics": "🛡️ Relíquias e Efeitos de Conjunto",
"overviewEquippedLc": "Cone de Luz Equipado",
"overviewEquippedRelics": "Relíquias e Efeitos de Conjunto",
"modalRelicsTitle": "🛡️ Equipar Relíquias (6 Espaços)",
"modalRelicsSubtitle": "Clique em cada espaço para alterar ou equipar conjuntos recomendados",
"btnAutoEquipRelics": "⚡ Equipar Conjuntos Recomendados",
"btnClearAllRelics": "🧹 Desequipar Tudo",
"activeSetEffects": "✨ Efeitos de Conjunto Ativos",
"activeSetEffects": "Efeitos de Conjunto Ativos",
"modalColLevel": "Nível",
"lblPromotion": "Classificação de Ascensão",
"slotPickerTitle": "🛡️ Selecionar Relíquia para Equipar",
@@ -6388,7 +6388,7 @@ const UI_COMPLETION_TRANSLATIONS = {
id: {
appSubtitle: 'Pengelola server dan FreeSR Star Rail', offline: 'Luring', btnPersistent: '💾 Simpanan persistent', slot: 'Posisi',
analystDmgNormal: '🗡️ Serangan Dasar', analystDmgSkill: '⚡ Skill Tempur', analystDmgUlt: '💥 Serangan Pamungkas', analystDmgFollowup: '🔄 Serangan Lanjutan', analystMonsterName: 'Musuh #{id}',
modalSkillBasic: 'Serangan Dasar', modalSkillSkill: 'Skill Tempur', modalSkillTalent: 'Talenta', traceA2: '🌟 Jejak Bonus A2', traceA4: '🌟 Jejak Bonus A4', traceA6: '🌟 Jejak Bonus A6', btnResetE0: 'Atur Ulang E0', modalEnergy50: 'Atur 50%', modalEnergy100: 'Atur 100%',
modalSkillBasic: 'Serangan Dasar', modalSkillSkill: 'Skill Tempur', modalSkillTalent: 'Talenta', traceA2: 'Jejak Bonus A2', traceA4: 'Jejak Bonus A4', traceA6: 'Jejak Bonus A6', btnResetE0: 'Atur Ulang E0', modalEnergy50: 'Atur 50%', modalEnergy100: 'Atur 100%',
equipmentSearchPlaceholder: '🔍 Cari Light Cone, Relik, atau ID item…', filter3Star: '3 Bintang', filterAllStatus: 'Semua Status', filterEquipped: 'Dipakai', filterInInventory: 'Di Inventaris', filterAllTypes: 'Semua Jenis', filterAllRelicSlots: 'Semua Slot Relik',
globalFeatureSilverWolf: 'Fitur Global Silver Wolf', globalFeatureCastorice: 'Fitur Global Castorice', globalFeatureAha: 'Fitur Global Aeon ★ Aha', lightConeSearchPlaceholder: '🔍 Cari Light Cone berdasarkan nama atau ID…',
superimpositionLabel: 'Superimposisi', superimpositionSelectTitle: 'Pilih superimposisi saat dipasang', superimpositionRank: 'S{rank} (Superimposisi {rank})',
@@ -7129,7 +7129,24 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
cameraFaceCamExperimental: 'FaceCam (ทดลอง)',
cameraDistanceError: 'ระยะ FaceCam ต้องอยู่ระหว่าง 0.5 ถึง 5',
cameraSaved: 'บันทึกกล้องแล้ว',
cameraModeFreecam: 'Freecam',
cameraModeFreecamDesc: 'กล้องอิสระ — ขยับมุมมองรอบตัวละครได้ตามต้องการ',
cameraModeFacecam: 'FaceCam',
cameraModeFacecamDesc: 'กล้องโคลสอัพใบหน้าตัวละคร (ฟีเจอร์ทดลอง)',
cameraDistanceValue: '{value}×',
cameraDistanceHint: 'ใช้กับโหมด FaceCam เท่านั้น — ล้อเมาส์ในเกมปรับระยะชั่วคราวได้',
cameraDistanceHintFreecam: 'เปิดโหมด FaceCam เพื่อใช้ระยะนี้',
cameraStatusIdle: 'พร้อมบันทึก',
cameraStatusSaved: 'บันทึกแล้ว',
cameraStatusUnsaved: 'มีการแก้ไขที่ยังไม่ถูกบันทึก',
cameraStatusOffline: 'เชื่อมต่อ GaCy GO เพื่อบันทึก',
cameraStatusSaving: 'กำลังบันทึก...',
cameraStatusError: 'บันทึกไม่สำเร็จ',
cameraSaving: 'กำลังบันทึก...',
cameraSaveOfflineError: 'ยังไม่ได้เชื่อมต่อเซิร์ฟเวอร์ GaCy GO จึงยังบันทึกกล้องไม่ได้',
lcPassiveFallback: 'เพิ่มค่าสถานะพื้นฐานตามสาย <strong>{path}</strong> และเพิ่มประสิทธิภาพความเสียหายและการสนับสนุน <strong>{value}%</strong> เมื่อเข้าสู่การต่อสู้',
relicSetPieces: '{count} ชิ้น',
relicSlotBadgeLabel: 'ช่องรีลิกส์: {slot}',
slotHeadShort: 'ส่วนหัว',
slotHandsShort: 'ส่วนมือ',
slotBodyShort: 'ลำตัว',
@@ -7150,6 +7167,7 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
jsonStatusBrowserCache: '{file} (โหลดจากแคชของเบราว์เซอร์)',
jsonStatusCache: '{file} (โหลดจากแคช)',
jsonStatusDump: '{file} (โครงสร้าง Firefly dump)',
jsonDataUnavailable: 'โหลด {file} ไม่ได้และไม่มีแคช กรุณาตรวจการเชื่อมต่อ หรือวาง JSON เอง',
jsonOfflineNoCache: 'Offline: ไม่มีแคช สามารถอัปโหลดหรือพิมพ์ JSON เองได้',
jsonOfflinePaste: 'Offline: สามารถพิมพ์หรือวาง JSON เองได้',
jsonCacheUnavailable: 'ไม่สามารถตรวจสอบได้',
@@ -7242,7 +7260,24 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
cameraFaceCamExperimental: 'FaceCam (experimental)',
cameraDistanceError: 'FaceCam distance must be between 0.5 and 5',
cameraSaved: 'Camera saved',
cameraModeFreecam: 'Freecam',
cameraModeFreecamDesc: 'Free camera — orbit the view around your character',
cameraModeFacecam: 'FaceCam',
cameraModeFacecamDesc: 'Close-up face camera (experimental)',
cameraDistanceValue: '{value}×',
cameraDistanceHint: 'FaceCam only — the mouse wheel in game adjusts the distance temporarily',
cameraDistanceHintFreecam: 'Switch to FaceCam to use this distance',
cameraStatusIdle: 'Ready to save',
cameraStatusSaved: 'Saved',
cameraStatusUnsaved: 'Unsaved changes',
cameraStatusOffline: 'Connect to GaCy GO to save',
cameraStatusSaving: 'Saving...',
cameraStatusError: 'Save failed',
cameraSaving: 'Saving...',
cameraSaveOfflineError: 'Not connected to a GaCy GO server, so the camera settings cannot be saved',
lcPassiveFallback: 'Increases basic stats following the <strong>{path}</strong> path and boosts damage and support performance by <strong>{value}%</strong> in battle',
relicSetPieces: '{count}-Piece Set',
relicSlotBadgeLabel: 'Relic slot: {slot}',
slotHeadShort: 'Head',
slotHandsShort: 'Hands',
slotBodyShort: 'Body',
@@ -7263,6 +7298,7 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
jsonStatusBrowserCache: '{file} (from browser cache)',
jsonStatusCache: '{file} (from cache)',
jsonStatusDump: '{file} (Firefly dump structure)',
jsonDataUnavailable: 'Could not load {file} and no cache is available. Check the connection or paste JSON.',
jsonOfflineNoCache: 'Offline: no cache. You can upload or paste JSON.',
jsonOfflinePaste: 'Offline: you can paste or type JSON.',
jsonCacheUnavailable: 'Unavailable',
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.17';
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.21';
const CATALOG_URL = new URL('../assets/item-catalog.json?v=2', import.meta.url);
const PAGE_SIZE = 48;
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage } from './i18n.js?v=2.17';
import { getCurrentLanguage } from './i18n.js?v=2.21';
const strings = {
en: {