diff --git a/AGENTS.md b/AGENTS.md index 08ea58a..0dd7992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. diff --git a/assets/relic-slots/1.png b/assets/relic-slots/1.png new file mode 100644 index 0000000..f2c33c6 Binary files /dev/null and b/assets/relic-slots/1.png differ diff --git a/assets/relic-slots/2.png b/assets/relic-slots/2.png new file mode 100644 index 0000000..6233d96 Binary files /dev/null and b/assets/relic-slots/2.png differ diff --git a/assets/relic-slots/3.png b/assets/relic-slots/3.png new file mode 100644 index 0000000..903038b Binary files /dev/null and b/assets/relic-slots/3.png differ diff --git a/assets/relic-slots/4.png b/assets/relic-slots/4.png new file mode 100644 index 0000000..7070ce5 Binary files /dev/null and b/assets/relic-slots/4.png differ diff --git a/assets/relic-slots/5.png b/assets/relic-slots/5.png new file mode 100644 index 0000000..921cc43 Binary files /dev/null and b/assets/relic-slots/5.png differ diff --git a/assets/relic-slots/6.png b/assets/relic-slots/6.png new file mode 100644 index 0000000..856b3c7 Binary files /dev/null and b/assets/relic-slots/6.png differ diff --git a/assets/relics/329-set.png b/assets/relics/329-set.png new file mode 100644 index 0000000..3fcc144 Binary files /dev/null and b/assets/relics/329-set.png differ diff --git a/assets/relics/330-set.png b/assets/relics/330-set.png new file mode 100644 index 0000000..d17e71e Binary files /dev/null and b/assets/relics/330-set.png differ diff --git a/css/styles.css b/css/styles.css index 3533f28..08647d6 100644 --- a/css/styles.css +++ b/css/styles.css @@ -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; diff --git a/index.html b/index.html index b270476..ed89dc4 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ GaCy-Srtool - + @@ -677,15 +677,45 @@
⚙️ สวิตช์ระบบพิเศษของ GaCy
-
-
-
Freecam / FaceCam
-
เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง
- - - - - +
+
+
+ +
+
Freecam / FaceCam
+
เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง
+
+
+ +
+ +
+ + +
+
+ + 2.0× +
+ + +

ใช้กับโหมด FaceCam เท่านั้น — ล้อเมาส์ในเกมปรับระยะชั่วคราวได้

+
+ +
+ + + พร้อมบันทึก + + +
@@ -1858,6 +1888,6 @@
- + diff --git a/js/app.js b/js/app.js index 0c8c884..0c2a067 100644 --- a/js/app.js +++ b/js/app.js @@ -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) {
Relic - ${slotInfo.icon} ${slotInfo.short} + ${slotInfo.short}
@@ -3995,8 +4021,8 @@ function renderModalRelicsTab(avatarId) { box.className = 'relic-slot-box empty'; box.innerHTML = `
- ${slotInfo.icon} - ${slotInfo.name.split(' ')[0]} + + ${slotInfo.short}
${slotInfo.name}
@@ -4020,9 +4046,9 @@ function renderModalRelicsTab(avatarId) { const cnt = setCountMap[sId]; const sInfo = getRelicSetInfo(sId); if (cnt >= 4) { - activeSets.push(`🌟 ${sInfo.name} (4-Piece Set): ${sInfo.desc4 || t('relicSet4Fallback')}`); + activeSets.push(` ${sInfo.name} (${t('relicSetPieces', { count: 4 })}): ${sInfo.desc4 || t('relicSet4Fallback')}`); } else if (cnt >= 2) { - activeSets.push(`✨ ${sInfo.name} (2-Piece Set): ${sInfo.desc2 || t('relicSet2Fallback')}`); + activeSets.push(` ${sInfo.name} (${t('relicSetPieces', { count: 2 })}): ${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