change icon stats, new plana, redesign some ui, fix JSON managment
Deploy GaC-Srtool to GitHub Pages / deploy (push) Canceled after 0s
No files matched your search
@@ -2,6 +2,8 @@
|
||||
|
||||
## Localization (required)
|
||||
|
||||
Every change or addition must include an update to `js/i18n.js`: review and update the translations and localization behavior affected by the change.
|
||||
|
||||
Every user-facing string must go through `js/i18n.js`. Never hardcode Thai or English UI text in HTML or JS.
|
||||
|
||||
- Use `data-i18n`, `data-i18n-placeholder`, `data-i18n-title`, `data-i18n-alt` or `data-i18n-aria-label` in `index.html`.
|
||||
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -851,6 +851,257 @@ body {
|
||||
border-color: rgba(99, 120, 160, 0.45);
|
||||
}
|
||||
|
||||
/* Freecam / FaceCam camera settings */
|
||||
.camera-settings-card {
|
||||
display: block;
|
||||
padding: clamp(0.9rem, 1.6vw, 1.25rem);
|
||||
}
|
||||
|
||||
.camera-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.camera-settings-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.camera-settings-emblem {
|
||||
display: grid;
|
||||
flex: 0 0 38px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(56, 189, 248, 0.32);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(145deg, rgba(56, 189, 248, 0.18), rgba(56, 189, 248, 0.03));
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.camera-settings-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.camera-settings-title {
|
||||
color: var(--text-highlight);
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.camera-settings-hint {
|
||||
margin-top: 0.15rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.camera-mode-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 168px), 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.camera-mode-option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
padding: 0.7rem 2.1rem 0.7rem 0.75rem;
|
||||
border: 1px solid rgba(110, 123, 148, 0.3);
|
||||
border-radius: 12px;
|
||||
background: rgba(9, 13, 22, 0.6);
|
||||
color: var(--text-main);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.camera-mode-option:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(56, 189, 248, 0.5);
|
||||
background: rgba(23, 32, 46, 0.9);
|
||||
}
|
||||
|
||||
.camera-mode-option:focus-visible {
|
||||
outline: 3px solid rgba(56, 189, 248, 0.8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.camera-mode-option.is-selected {
|
||||
border-color: rgba(56, 189, 248, 0.85);
|
||||
background: linear-gradient(145deg, rgba(12, 74, 110, 0.5), rgba(15, 23, 36, 0.9));
|
||||
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.16), 0 6px 18px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.camera-mode-icon {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.camera-mode-text {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
.camera-mode-title {
|
||||
color: var(--text-highlight);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.camera-mode-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.camera-mode-check {
|
||||
position: absolute;
|
||||
top: 0.55rem;
|
||||
right: 0.6rem;
|
||||
display: grid;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 50%;
|
||||
color: transparent;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.camera-mode-option.is-selected .camera-mode-check {
|
||||
border-color: var(--accent-cyan);
|
||||
background: var(--accent-cyan);
|
||||
color: #06121c;
|
||||
}
|
||||
|
||||
.camera-native-select {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.camera-distance-block {
|
||||
padding: 0.7rem 0.85rem 0.75rem;
|
||||
border: 1px solid rgba(110, 123, 148, 0.22);
|
||||
border-radius: 12px;
|
||||
background: rgba(8, 11, 18, 0.5);
|
||||
}
|
||||
|
||||
.camera-distance-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.camera-field-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.71rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.camera-distance-value {
|
||||
min-width: 58px;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border: 1px solid rgba(56, 189, 248, 0.32);
|
||||
border-radius: 999px;
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
color: var(--accent-cyan);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.camera-distance-range {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
margin: 0.6rem 0 0.15rem;
|
||||
accent-color: var(--accent-cyan);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.camera-distance-scale {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.camera-distance-note {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.73rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.camera-distance-block.is-inactive .camera-distance-head,
|
||||
.camera-distance-block.is-inactive .camera-distance-range,
|
||||
.camera-distance-block.is-inactive .camera-distance-scale {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.camera-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.camera-save-state {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.camera-state-dot {
|
||||
flex: 0 0 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.camera-save-state[data-state="saved"] .camera-state-dot { background: #34d399; }
|
||||
.camera-save-state[data-state="unsaved"] .camera-state-dot { background: #fbbf24; }
|
||||
.camera-save-state[data-state="error"] .camera-state-dot { background: #fb7185; }
|
||||
.camera-save-state[data-state="offline"] .camera-state-dot { background: #94a3b8; }
|
||||
|
||||
.camera-save-button {
|
||||
min-height: 38px;
|
||||
padding: 0.45rem 0.95rem;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.camera-save-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Aha's Help level picker */
|
||||
.aha-help-card {
|
||||
position: relative;
|
||||
@@ -2723,6 +2974,26 @@ input:checked + .slider:before {
|
||||
padding: 0.05rem 0;
|
||||
}
|
||||
|
||||
.relic-slot-type-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.relic-slot-empty-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.game-inline-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.relic-slot-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GaCy-Srtool</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=2.29">
|
||||
<link rel="stylesheet" href="css/styles.css?v=2.32">
|
||||
<link rel="stylesheet" href="css/item-catalog.css?v=2">
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||
@@ -677,15 +677,45 @@
|
||||
<div class="card" id="card-persistent-global">
|
||||
<div class="card-title" data-i18n="persistentGlobalTitle">⚙️ สวิตช์ระบบพิเศษของ GaCy</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem;">
|
||||
<div class="ex-switch-card">
|
||||
<div>
|
||||
<div style="font-weight: 700;">Freecam / FaceCam</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);" data-i18n="cameraHint">เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง</div>
|
||||
<label for="camera-mode" data-i18n="cameraMode">โหมดกล้อง</label>
|
||||
<select id="camera-mode"><option value="freecam">Freecam</option><option value="facecam" data-i18n="cameraFaceCamExperimental">FaceCam (ทดลอง)</option></select>
|
||||
<label for="camera-distance" data-i18n="cameraFaceCamDistance">ระยะ FaceCam</label>
|
||||
<input id="camera-distance" type="number" min="0.5" max="5" step="0.1" value="2">
|
||||
<button type="button" class="btn btn-success" id="btn-save-camera" data-i18n="cameraSave">บันทึกกล้อง</button>
|
||||
<div class="ex-switch-card camera-settings-card">
|
||||
<div class="camera-settings">
|
||||
<div class="camera-settings-head">
|
||||
<span class="camera-settings-emblem" aria-hidden="true">📷</span>
|
||||
<div class="camera-settings-copy">
|
||||
<div class="camera-settings-title">Freecam / FaceCam</div>
|
||||
<div class="camera-settings-hint" data-i18n="cameraHint">เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="camera-mode-options" class="camera-mode-options" role="group" data-i18n-aria-label="cameraMode">
|
||||
<!-- Injected by JS -->
|
||||
</div>
|
||||
<select id="camera-mode" class="camera-native-select" aria-hidden="true" tabindex="-1">
|
||||
<option value="freecam">Freecam</option>
|
||||
<option value="facecam" data-i18n="cameraFaceCamExperimental">FaceCam (ทดลอง)</option>
|
||||
</select>
|
||||
|
||||
<div class="camera-distance-block" id="camera-distance-block">
|
||||
<div class="camera-distance-head">
|
||||
<label class="camera-field-label" for="camera-distance" data-i18n="cameraFaceCamDistance">ระยะ FaceCam</label>
|
||||
<output class="camera-distance-value" id="camera-distance-value" for="camera-distance">2.0×</output>
|
||||
</div>
|
||||
<input id="camera-distance" class="camera-distance-range" type="range" min="0.5" max="5" step="0.1" value="2"
|
||||
data-i18n-aria-label="cameraFaceCamDistance">
|
||||
<div class="camera-distance-scale" aria-hidden="true">
|
||||
<span>0.5×</span>
|
||||
<span>5×</span>
|
||||
</div>
|
||||
<p class="camera-distance-note" id="camera-distance-note" data-i18n="cameraDistanceHint">ใช้กับโหมด FaceCam เท่านั้น — ล้อเมาส์ในเกมปรับระยะชั่วคราวได้</p>
|
||||
</div>
|
||||
|
||||
<div class="camera-actions">
|
||||
<span class="camera-save-state" id="camera-save-state" data-state="idle">
|
||||
<span class="camera-state-dot" aria-hidden="true"></span>
|
||||
<span id="camera-save-state-text" data-i18n="cameraStatusIdle">พร้อมบันทึก</span>
|
||||
</span>
|
||||
<button type="button" class="btn btn-success camera-save-button" id="btn-save-camera" data-i18n="cameraSave">บันทึกกล้อง</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ex-switch-card">
|
||||
@@ -1858,6 +1888,6 @@
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<!-- Script Modules -->
|
||||
<script type="module" src="js/app.js?v=3.20"></script>
|
||||
<script type="module" src="js/app.js?v=3.26"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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,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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.17';
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.21';
|
||||
|
||||
const strings = {
|
||||
en: {
|
||||
|
||||
@@ -2,6 +2,7 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
test('4.6 equipment metadata and bundled artwork are available', async () => {
|
||||
const { LIGHTCONE_METADATA, RELIC_SET_METADATA, getRelicSetInfo } = await import('../js/equipment-data.js');
|
||||
@@ -15,10 +16,58 @@ test('4.6 equipment metadata and bundled artwork are available', async () => {
|
||||
assert.equal(getRelicSetInfo(id, 'en').name, name);
|
||||
assert.ok(getRelicSetInfo(id, 'en').desc2);
|
||||
assert.notEqual(relic.creationAvailable, false);
|
||||
assert.ok(fs.existsSync(path.resolve(__dirname, '..', relic.setIcon)));
|
||||
for (const slot of [5, 6]) assert.ok(fs.existsSync(path.resolve(__dirname, '..', relic.images[slot])));
|
||||
}
|
||||
});
|
||||
|
||||
test('Aha recommends Dreamlit Actor and the captured God\'s Moment of Joy set', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
||||
const start = source.indexOf('const CHARACTER_OPTIMAL_BUILDS =');
|
||||
const end = source.indexOf('// Recommended Relic Builds by Path', start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const context = vm.createContext({});
|
||||
vm.runInContext(source.slice(start, end) + '\nthis.build = getOptimalBuildForAvatar(1511);', context);
|
||||
assert.equal(context.build.cavern, 133);
|
||||
assert.equal(context.build.planar, 329);
|
||||
const capture = require('../assets/relic-client-capture.json');
|
||||
for (const [slot, id] of [[5, context.build.sphere], [6, context.build.rope]]) {
|
||||
const piece = capture.relics.find(row => row.SetID === context.build.planar && row.Type === slot);
|
||||
assert.ok(piece && capture.main_affixes.some(row => row.GroupID === piece.MainAffixGroup && row.AffixID === id));
|
||||
}
|
||||
});
|
||||
|
||||
test('all six relic slots use bundled game artwork and localized badge labels', async () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
||||
const start = source.indexOf('const SLOT_NAMES_FULL =');
|
||||
const end = source.indexOf('function getRelicMainAffixName', start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const context = vm.createContext({ t: key => key });
|
||||
vm.runInContext(source.slice(start, end) + '\nthis.slots = SLOT_NAMES_FULL;', context);
|
||||
const icons = new Set();
|
||||
for (let slot = 1; slot <= 6; slot++) {
|
||||
const icon = context.slots[slot].icon;
|
||||
assert.ok(fs.existsSync(path.resolve(__dirname, '..', icon)));
|
||||
icons.add(icon);
|
||||
}
|
||||
assert.equal(icons.size, 6);
|
||||
assert.match(source, /relic-slot-type-icon/);
|
||||
assert.match(source, /relic-slot-empty-icon/);
|
||||
const { TRANSLATIONS } = await import('../js/i18n.js');
|
||||
for (const language of ['th', 'en']) assert.match(TRANSLATIONS[language].relicSlotBadgeLabel, /\{slot\}/);
|
||||
});
|
||||
|
||||
test('equipment and trace labels do not embed emoji in any supported translation', async () => {
|
||||
const { TRANSLATIONS } = await import('../js/i18n.js');
|
||||
const keys = ['tabEquipment', 'modalAddLcTitle', 'modalAddRelicTitle', 'eqLcTitle', 'eqRelicTitle', 'overviewEquippedLc', 'overviewEquippedRelics', 'activeSetEffects', 'traceA2', 'traceA4', 'traceA6'];
|
||||
for (const [language, translations] of Object.entries(TRANSLATIONS)) {
|
||||
for (const key of keys) {
|
||||
if (translations[key]) assert.doesNotMatch(translations[key], /^[🌟✨🗡🛡]/u, `${language}: ${key}`);
|
||||
}
|
||||
}
|
||||
for (const language of ['th', 'en']) assert.match(TRANSLATIONS[language].relicSetPieces, /\{count\}/);
|
||||
});
|
||||
|
||||
test('captured relic templates reference matching client affix properties', () => {
|
||||
const capture = require('../assets/relic-client-capture.json');
|
||||
assert.deepEqual(capture.relics.map(row => row.ID), [63295, 63296, 63305, 63306]);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function createEditor(apiCall, connected = true) {
|
||||
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
||||
const start = source.indexOf('let jsonLoadGeneration =');
|
||||
const end = source.indexOf('// Save JSON Tab Data', start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const el = {
|
||||
btnSelectFreesr: {}, btnSelectPersistent: {}, btnSelectDatagame: {},
|
||||
jsonEditor: { value: '' }, jsonStatusInfo: {}, btnJsonSave: {}
|
||||
};
|
||||
const state = { isConnected: connected, fullFreesrData: null, persistentData: null, dataGameData: null };
|
||||
const context = vm.createContext({
|
||||
el, state, apiCall, t: (key, vars = {}) => `${key}:${vars.file || ''}`,
|
||||
savePersistentCache() {}, saveFreesrCache() {}, saveDataGameCache() {},
|
||||
localStorage: { getItem() { return null; } }, DEFAULT_DATA_GAME: {}, DEFAULT_LINEUP: {}
|
||||
});
|
||||
vm.runInContext(source.slice(start, end) + '\nthis.loadJsonData = loadJsonData;', context);
|
||||
return { el, state, load: context.loadJsonData };
|
||||
}
|
||||
|
||||
test('switching JSON files reads the persistent response and the correct endpoint', async () => {
|
||||
const paths = [];
|
||||
const editor = createEditor(async url => {
|
||||
paths.push(url);
|
||||
return url === '/api/freesr' ? { data: { avatars: { 1: {} } } } : { persistent: { scene: 42, ex_ranks_unlocked: { 1: false } } };
|
||||
});
|
||||
await editor.load('freesr');
|
||||
assert.ok(JSON.parse(editor.el.jsonEditor.value).avatars);
|
||||
await editor.load('persistent');
|
||||
assert.deepEqual(JSON.parse(editor.el.jsonEditor.value), { scene: 42, ex_ranks_unlocked: { 1: false } });
|
||||
assert.deepEqual(paths, ['/api/freesr', '/api/persistent']);
|
||||
assert.equal(editor.el.btnJsonSave.disabled, false);
|
||||
});
|
||||
|
||||
test('unavailable persistent data clears previous file and prevents saving it', async () => {
|
||||
const editor = createEditor(async url => url === '/api/freesr' ? { data: { avatars: {} } } : { status: 500, error: 'unavailable' });
|
||||
await editor.load('freesr');
|
||||
await editor.load('persistent');
|
||||
assert.equal(editor.el.jsonEditor.value, '');
|
||||
assert.equal(editor.el.btnJsonSave.disabled, true);
|
||||
assert.match(editor.el.jsonStatusInfo.textContent, /jsonDataUnavailable:persistent/);
|
||||
});
|
||||
|
||||
test('late response cannot replace the newly selected file', async () => {
|
||||
let resolvePersistent;
|
||||
const editor = createEditor(url => url === '/api/persistent'
|
||||
? new Promise(resolve => { resolvePersistent = resolve; })
|
||||
: Promise.resolve({ data: { avatars: { current: {} } } }));
|
||||
const first = editor.load('persistent');
|
||||
await editor.load('freesr');
|
||||
resolvePersistent({ persistent: { scene: 42 } });
|
||||
await first;
|
||||
assert.ok(JSON.parse(editor.el.jsonEditor.value).avatars.current);
|
||||
assert.equal(editor.state.currentJsonMode, 'freesr');
|
||||
assert.equal(editor.el.btnJsonSave.disabled, false);
|
||||
});
|
||||