diff --git a/css/styles.css b/css/styles.css index b46dd0a..5c9253d 100644 --- a/css/styles.css +++ b/css/styles.css @@ -2502,5 +2502,193 @@ input:checked + .slider:before { } } +/* ========================================== + Team Combat Synergy & Role Balance + ========================================== */ +.synergy-elements-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(85px, 1fr)); + gap: 0.5rem; +} + +.synergy-element-chip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 0.5rem 0.35rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-sm); + transition: all 0.2s ease; + text-align: center; +} + +.synergy-element-chip.active { + background: rgba(56, 189, 248, 0.08); + border-color: rgba(56, 189, 248, 0.4); + box-shadow: 0 0 10px rgba(56, 189, 248, 0.15); +} + +.synergy-element-chip.inactive { + opacity: 0.35; + filter: grayscale(80%); +} + +.synergy-element-name { + font-size: 0.75rem; + font-weight: 700; + color: #fff; +} + +.synergy-element-chars { + font-size: 0.675rem; + color: var(--accent-cyan); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.synergy-metrics-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.5rem; +} + +.synergy-metric-box { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: var(--radius-sm); + padding: 0.5rem 0.4rem; + text-align: center; +} + +.synergy-metric-val { + font-size: 1.15rem; + font-weight: 800; + color: var(--accent-gold); + font-family: monospace; +} + +.synergy-metric-lbl { + font-size: 0.675rem; + color: var(--text-muted); + margin-top: 0.15rem; +} + +/* ========================================== + GM Command Dispatcher & Live Log Console + ========================================== */ +.console-quick-pills { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; +} + +.console-pill-btn { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #e2e8f0; + padding: 0.3rem 0.65rem; + border-radius: 9999px; + font-size: 0.775rem; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + display: inline-flex; + align-items: center; + gap: 0.3rem; +} + +.console-pill-btn:hover { + background: rgba(56, 189, 248, 0.15); + border-color: rgba(56, 189, 248, 0.4); + color: var(--accent-cyan); + transform: translateY(-1px); +} + +.console-terminal-wrapper { + background: #070b12; + border: 1px solid rgba(56, 189, 248, 0.25); + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.6); +} + +.console-terminal-header { + background: #0f172a; + padding: 0.4rem 0.75rem; + display: flex; + align-items: center; + gap: 0.4rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.terminal-dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; +} + +.dot-red { background: #ef4444; } +.dot-yellow { background: #f59e0b; } +.dot-green { background: #10b981; } + +.terminal-title { + font-size: 0.725rem; + color: var(--text-muted); + font-family: monospace; + margin-left: 0.35rem; +} + +.console-terminal-log { + height: 200px; + max-height: 220px; + overflow-y: auto; + padding: 0.75rem; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.775rem; + line-height: 1.55; + color: #cbd5e1; + display: flex; + flex-direction: column; + gap: 0.2rem; + white-space: pre-wrap; + word-break: break-word; +} + +.log-line { + display: flex; + gap: 0.5rem; + align-items: flex-start; +} + +.log-time { + color: #64748b; + flex-shrink: 0; + user-select: none; +} + +.log-tag { + font-weight: 700; + flex-shrink: 0; + padding: 0 0.3rem; + border-radius: 3px; + font-size: 0.7rem; +} + +.log-tag.tag-sys { background: rgba(56, 189, 248, 0.15); color: #38bdf8; } +.log-tag.tag-cmd { background: rgba(251, 191, 36, 0.15); color: #fbbf24; } +.log-tag.tag-ok { background: rgba(52, 211, 153, 0.15); color: #34d399; } +.log-tag.tag-err { background: rgba(248, 113, 113, 0.15); color: #f87171; } +.log-tag.tag-net { background: rgba(168, 85, 247, 0.15); color: #c084fc; } + +.log-msg { + flex: 1; +} + diff --git a/index.html b/index.html index 35e95ad..d0e7d03 100644 --- a/index.html +++ b/index.html @@ -104,109 +104,162 @@ -
- -
-
- 📍 āļŠāļ–āļēāļ™āļ°āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒ & āļžāļīāļāļąāļ”āļ›āļąāļˆāļˆāļļāļšāļąāļ™ - API Live +
+ +
+ +
+
+ 📍 āļŠāļ–āļēāļ™āļ°āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒ & āļžāļīāļāļąāļ”āļ›āļąāļˆāļˆāļļāļšāļąāļ™ + API Live +
+
+
+ āđ‚āļŦāļĄāļ”āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāļ—āļĩāđˆāđƒāļŠāđ‰āļ‡āļēāļ™ + GaC-hkrpg-go +
+
+ API Gateway Endpoint + - +
+
+
+ Plane ID + - +
+
+ Floor ID + - +
+
+
+ āļžāļīāļāļąāļ”āļ•āļąāļ§āļĨāļ°āļ„āļĢ XYZ + - +
+
-
-
- āđ‚āļŦāļĄāļ”āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāļ—āļĩāđˆāđƒāļŠāđ‰āļ‡āļēāļ™ - GaC-hkrpg-go + + +
+
+ + ⚔ïļ + āļ§āļīāđ€āļ„āļĢāļēāļ°āļŦāđŒ Synergy āļ—āļĩāļĄ & āļˆāļļāļ”āļ­āđˆāļ­āļ™ + + āļ„āļĢāļ­āļšāļ„āļĨāļļāļĄ 0/7 āļ˜āļēāļ•āļļ
-
- API Gateway Endpoint - - -
-
-
- Plane ID - - +

āļ§āļīāđ€āļ„āļĢāļēāļ°āļŦāđŒāļāļēāļĢāļ„āļĢāļ­āļšāļ„āļĨāļļāļĄ Weakness Break, āļ„āļ§āļēāļĄāļŠāļĄāļ”āļļāļĨāļ‚āļ­āļ‡āļšāļ—āļšāļēāļ— āđāļĨāļ°āļ„āļ§āļēāļĄāļžāļĢāđ‰āļ­āļĄāļ‚āļ­āļ‡āļ—āļĩāļĄ

+ + +
+
+ ðŸŽŊ āļāļēāļĢāļ—āļģāļĨāļēāļĒāļˆāļļāļ”āļ­āđˆāļ­āļ™ (Weakness Break Coverage)
-
- Floor ID - - +
+
+ + +
+
+ ðŸ›Ąïļ āļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļšāļ—āļšāļēāļ—āđƒāļ™āļ—āļĩāļĄ (Team Composition & Roles) +
+
+ +
+
+ +
+
+ +
- āļžāļīāļāļąāļ”āļ•āļąāļ§āļĨāļ°āļ„āļĢ XYZ - - +
+ 📊 āļ„āļ§āļēāļĄāļžāļĢāđ‰āļ­āļĄāļ‚āļ­āļ‡āļ­āļļāļ›āļāļĢāļ“āđŒāđāļĨāļ°āđ€āļĨāđ€āļ§āļĨāđ€āļ‰āļĨāļĩāđˆāļĒ +
+
+
+
-
+
āđ€āļĨāđ€āļ§āļĨāđ€āļ‰āļĨāļĩāđˆāļĒ
+
+
+
-
+
Eidolon āđ€āļ‰āļĨāļĩāđˆāļĒ
+
+
+
-
+
āļŠāļ§āļĄāļāļĢāļ§āļĒāđāļŠāļ‡
+
+
+
-
+
āļĢāļĩāļĨāļīāļāļŠāđŒāļ„āļĢāļšāļŠāļļāļ”
+
+
- -
-
- ⚡ āļ”āļģāđ€āļ™āļīāļ™āļāļēāļĢāļ”āđˆāļ§āļ™ - āļ„āļģāļŠāļąāđˆāļ‡āļ”āđˆāļ§āļ™ + +
+
+ + ðŸ’ŧ + āļ„āļ­āļ™āđ‚āļ‹āļĨāļ„āļģāļŠāļąāđˆāļ‡ GM & Log āļŠāļ” + +
+ + +
-

āđ€āļ„āļĢāļ·āđˆāļ­āļ‡āļĄāļ·āļ­āļˆāļąāļ”āļāļēāļĢāļ—āļĩāļĄāđāļĨāļ°āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāđāļšāļšāļ—āļąāļ™āļ—āļĩ

-
- - +

āļŠāđˆāļ‡āļ„āļģāļŠāļąāđˆāļ‡ GM / Server commands āđāļšāļšāđ€āļĢāļĩāļĒāļĨāđ„āļ—āļĄāđŒ āļžāļĢāđ‰āļ­āļĄāļ•āļīāļ”āļ•āļēāļĄāļŠāļ–āļēāļ™āļ°āđāļĨāļ° Packet Log

- - + +
+
+ ⚡ āļ„āļģāļŠāļąāđˆāļ‡āļ”āđˆāļ§āļ™āļ—āļĩāđˆāđƒāļŠāđ‰āļšāđˆāļ­āļĒ (Quick Commands) +
+
+ + + + + + + +
+
- - + +
+
+ + + + GaC-hkrpg-go Console Session (Interactive GM Dispatcher) +
+
+ +
+
- - - - - - - - - - - + +
+ ðŸ’Ą āļĢāļ­āļ‡āļĢāļąāļšāļāļ” â†‘ ↓ āđ€āļžāļ·āđˆāļ­āđ€āļĨāļ·āļ­āļāļ›āļĢāļ°āļ§āļąāļ•āļīāļ„āļģāļŠāļąāđˆāļ‡āļāđˆāļ­āļ™āļŦāļ™āđ‰āļē + ● āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāļ­āļ­āļ™āđ„āļĨāļ™āđŒ
diff --git a/js/app.js b/js/app.js index 3a63ed6..9891fad 100644 --- a/js/app.js +++ b/js/app.js @@ -72,12 +72,26 @@ const el = { lineupContainer: document.getElementById('lineup-container'), toggleCreativeMode: document.getElementById('toggle-creative-mode'), badgeCreativeMode: document.getElementById('badge-creative-mode'), - btnQuickLineupE0S1: document.getElementById('btn-quick-lineup-e0s1'), - btnQuickLineupRelics: document.getElementById('btn-quick-lineup-relics'), - btnQuickUnlockAllAvatars: document.getElementById('btn-quick-unlock-all-avatars'), - btnQuickAddAllLcs: document.getElementById('btn-quick-add-all-lcs'), - btnQuickTeleportTrain: document.getElementById('btn-quick-teleport-train'), - btnQuickClearLineup: document.getElementById('btn-quick-clear-lineup'), + // Team Combat Synergy + cardTeamSynergy: document.getElementById('card-team-synergy'), + badgeSynergyCoverage: document.getElementById('badge-synergy-coverage'), + synergyElementsContainer: document.getElementById('synergy-elements-container'), + synergyRolesContainer: document.getElementById('synergy-roles-container'), + synergyAdviceBox: document.getElementById('synergy-advice-box'), + synergyAvgLevel: document.getElementById('synergy-avg-level'), + synergyAvgEidolon: document.getElementById('synergy-avg-eidolon'), + synergyLcReady: document.getElementById('synergy-lc-ready'), + synergyRelicReady: document.getElementById('synergy-relic-ready'), + + // GM Command Dispatcher & Live Log Console + cardGmConsole: document.getElementById('card-gm-console'), + btnConsoleCopyLog: document.getElementById('btn-console-copy-log'), + btnConsoleClearLog: document.getElementById('btn-console-clear-log'), + consoleTerminalLog: document.getElementById('console-terminal-log'), + formConsoleCmd: document.getElementById('form-console-cmd'), + consoleCmdInput: document.getElementById('console-cmd-input'), + btnConsoleCmdSend: document.getElementById('btn-console-cmd-send'), + lblConsoleConnStatus: document.getElementById('lbl-console-conn-status'), // Gachabase Sync btnHeaderGachabaseSync: document.getElementById('btn-header-gachabase-sync'), btnQuickGachabaseSync: document.getElementById('btn-quick-gachabase-sync'), @@ -379,10 +393,18 @@ function setConnectionStatus(connected, extraText = '') { el.statusDot.classList.add('connected'); el.connectionStatusText.textContent = extraText || t('online'); el.connectionStatusText.style.color = 'var(--accent-green)'; + if (el.lblConsoleConnStatus) { + el.lblConsoleConnStatus.textContent = '● āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāļ­āļ­āļ™āđ„āļĨāļ™āđŒ'; + el.lblConsoleConnStatus.style.color = 'var(--accent-green)'; + } } else { el.statusDot.classList.remove('connected'); el.connectionStatusText.textContent = extraText || t('offline'); el.connectionStatusText.style.color = 'var(--accent-red)'; + if (el.lblConsoleConnStatus) { + el.lblConsoleConnStatus.textContent = '○ āļ­āļ­āļŸāđ„āļĨāļ™āđŒ (āļ„āļģāļŠāļąāđˆāļ‡āļ—āļģāļ‡āļēāļ™āļœāđˆāļēāļ™āđāļ„āļŠāļ āļēāļĒāđƒāļ™)'; + el.lblConsoleConnStatus.style.color = 'var(--text-muted)'; + } } } @@ -1015,6 +1037,413 @@ function renderLineup(lineups) { }); el.lineupContainer.appendChild(addSlotCard); } + + // Update Team Combat Synergy Analysis + renderTeamSynergy(currentLineup); +} + +// ========================================== +// Team Combat Synergy & Weakness Break Analysis +// ========================================== +const HSR_ELEMENTS = [ + { key: 'Physical', name: 'Physical', nameTh: 'āļāļēāļĒāļ āļēāļž', color: '#cbd5e1' }, + { key: 'Fire', name: 'Fire', nameTh: 'āđ„āļŸ', color: '#ef4444' }, + { key: 'Ice', name: 'Ice', nameTh: 'āļ™āđ‰āļģāđāļ‚āđ‡āļ‡', color: '#38bdf8' }, + { key: 'Lightning', name: 'Lightning', nameTh: 'āļŠāļēāļĒāļŸāđ‰āļē', color: '#c084fc' }, + { key: 'Wind', name: 'Wind', nameTh: 'āļĨāļĄ', color: '#34d399' }, + { key: 'Quantum', name: 'Quantum', nameTh: 'āļ„āļ§āļ­āļ™āļ•āļąāļĄ', color: '#818cf8' }, + { key: 'Imaginary', name: 'Imaginary', nameTh: 'āļˆāļīāļ™āļ•āļ āļēāļž', color: '#fbbf24' } +]; + +function renderTeamSynergy(lineups) { + if (!el.synergyElementsContainer) return; + const currentLineup = lineups || state.statusData?.lineups || DEFAULT_LINEUP; + + // Extract active character IDs + const activeIds = []; + for (const slot in currentLineup) { + const aid = parseInt(currentLineup[slot], 10); + if (aid && aid > 0 && !activeIds.includes(aid)) { + activeIds.push(aid); + } + } + + // 1. Weakness Break Coverage + const coveredElements = {}; + const roles = { + dps: [], // Destruction, Hunt, Erudition + specialist: [], // Remembrance, Elation + support: [], // Harmony, Nihility + sustain: [] // Preservation, Abundance + }; + + let totalLevel = 0; + let totalEidolon = 0; + let lcCount = 0; + let fullRelicCount = 0; + + activeIds.forEach(aid => { + const meta = getAvatarInfo(aid); + const av = state.fullFreesrData?.avatars?.[aid]; + const lvl = av?.level || 80; + const rank = av?.data?.rank ?? (state.avatarsList.find(a => a.avatar_id === aid)?.rank ?? 0); + + totalLevel += lvl; + totalEidolon += rank; + + // Element coverage + const elem = meta.element || 'Unknown'; + if (!coveredElements[elem]) { + coveredElements[elem] = []; + } + coveredElements[elem].push(meta.name); + + // Path / Role + const path = meta.path || ''; + if (['Destruction', 'Hunt', 'Erudition'].includes(path)) { + roles.dps.push(meta.name); + } else if (['Remembrance', 'Elation'].includes(path)) { + roles.specialist.push(meta.name); + } else if (['Harmony', 'Nihility'].includes(path)) { + roles.support.push(meta.name); + } else if (['Preservation', 'Abundance'].includes(path)) { + roles.sustain.push(meta.name); + } + + // LC check + const hasLc = (state.fullFreesrData?.lightcones || []).some(l => l.equip_avatar === aid); + if (hasLc) lcCount++; + + // Relics check (count how many relics equipped) + const relCount = (state.fullFreesrData?.relics || []).filter(r => r.equip_avatar === aid).length; + if (relCount >= 6) fullRelicCount++; + }); + + const count = activeIds.length || 1; + const avgLevel = activeIds.length ? (totalLevel / count).toFixed(1) : '-'; + const avgEidolon = activeIds.length ? `E${(totalEidolon / count).toFixed(1)}` : '-'; + + // Render Element Chips + const coveredCount = Object.keys(coveredElements).length; + if (el.badgeSynergyCoverage) { + el.badgeSynergyCoverage.textContent = `āļ„āļĢāļ­āļšāļ„āļĨāļļāļĄ ${coveredCount}/7 āļ˜āļēāļ•āļļ`; + el.badgeSynergyCoverage.style.color = coveredCount >= 4 ? 'var(--accent-green)' : (coveredCount >= 2 ? 'var(--accent-cyan)' : 'var(--accent-gold)'); + } + + el.synergyElementsContainer.innerHTML = HSR_ELEMENTS.map(elem => { + const isCovered = !!coveredElements[elem.key]; + const providers = coveredElements[elem.key] || []; + const providerText = isCovered ? providers.join(', ') : 'āđ„āļĄāđˆāļĄāļĩāđƒāļ™āļ—āļĩāļĄ'; + return ` +
+ ${getElementIconHtml(elem.key, 22)} + ${elem.nameTh} + ${providerText} +
+ `; + }).join(''); + + // Render Role Tags + if (el.synergyRolesContainer) { + el.synergyRolesContainer.innerHTML = ` +
+ ðŸ—Ąïļ DPS (${roles.dps.length}): ${roles.dps.join(', ') || '-'} +
+
+ ⚡ āļ‹āļąāļžāļžāļ­āļĢāđŒāļ•/āļ”āļĩāļšāļąāļŸ (${roles.support.length}): ${roles.support.join(', ') || '-'} +
+
+ ðŸ›Ąïļ Sustain āļŪāļĩāļĨ/āļŠāļīāļĨāļ”āđŒ (${roles.sustain.length}): ${roles.sustain.join(', ') || '-'} +
+ ${roles.specialist.length ? ` +
+ ðŸ”Ū āļžāļīāđ€āļĻāļĐ/FUA (${roles.specialist.length}): ${roles.specialist.join(', ')} +
+ ` : ''} + `; + } + + // Dynamic Advice Text + if (el.synergyAdviceBox) { + if (activeIds.length === 0) { + el.synergyAdviceBox.innerHTML = `āļĒāļąāļ‡āđ„āļĄāđˆāļĄāļĩāļ•āļąāļ§āļĨāļ°āļ„āļĢāđƒāļ™āļ—āļĩāļĄ āļāļĢāļļāļ“āļēāđ€āļĨāļ·āļ­āļāļ•āļąāļ§āļĨāļ°āļ„āļĢāđƒāļ™āļ—āļĩāļĄāļ”āđ‰āļēāļ™āļšāļ™`; + } else if (roles.sustain.length === 0) { + el.synergyAdviceBox.innerHTML = `⚠ïļ āļ‚āđ‰āļ­āđāļ™āļ°āļ™āļģ: āļ—āļĩāļĄāļĒāļąāļ‡āļ‚āļēāļ”āļ•āļąāļ§āļĨāļ°āļ„āļĢāļŠāļēāļĒāļ›āđ‰āļ­āļ‡āļāļąāļ™āļŦāļĢāļ·āļ­āļĢāļąāļāļĐāļē (Preservation / Abundance) āđāļ™āļ°āļ™āļģāđƒāļŠāđˆ 1 āļ•āļģāđāļŦāļ™āđˆāļ‡āđ€āļžāļ·āđˆāļ­āļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒāđƒāļ™āļāļēāļĢāđ€āļĨāđˆāļ™`; + el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)'; + } else if (roles.dps.length === 0) { + el.synergyAdviceBox.innerHTML = `ðŸ’Ą āļ‚āđ‰āļ­āđāļ™āļ°āļ™āļģ: āļ—āļĩāļĄāļĄāļĩāđāļ•āđˆāļ•āļąāļ§āļ‹āļąāļžāļžāļ­āļĢāđŒāļ• āļĒāļąāļ‡āđ„āļĄāđˆāļĄāļĩāļ•āļąāļ§āļ—āļģāļ”āļēāđ€āļĄāļˆāļŦāļĨāļąāļ (Destruction / Hunt / Erudition)`; + el.synergyAdviceBox.style.borderColor = 'rgba(56, 189, 248, 0.4)'; + } else if (roles.support.length === 0) { + el.synergyAdviceBox.innerHTML = `ðŸ’Ą āļ‚āđ‰āļ­āđāļ™āļ°āļ™āļģ: āļ—āļĩāļĄāļĄāļĩāļ•āļąāļ§āļ”āļēāđ€āļĄāļˆāđāļ•āđˆāļĒāļąāļ‡āđ„āļĄāđˆāļĄāļĩāļ•āļąāļ§āļšāļąāļŸāļŦāļĢāļ·āļ­āļ”āļĩāļšāļąāļŸ (Harmony / Nihility) āđ€āļŠāļĢāļīāļĄāļ•āļąāļ§āļ‹āļąāļžāļžāļ­āļĢāđŒāļ•āļˆāļ°āļŠāđˆāļ§āļĒāđ€āļĢāđˆāļ‡āļ”āļēāđ€āļĄāļˆāđ„āļ”āđ‰āļĄāļŦāļēāļĻāļēāļĨ`; + el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)'; + } else { + el.synergyAdviceBox.innerHTML = `✅ āļŠāļĄāļ”āļļāļĨāļĒāļ­āļ”āđ€āļĒāļĩāđˆāļĒāļĄ: āļ—āļĩāļĄāļĄāļĩāđ‚āļ„āļĢāļ‡āļŠāļĢāđ‰āļēāļ‡āļ„āļĢāļšāđ€āļ„āļĢāļ·āđˆāļ­āļ‡ āļ—āļąāđ‰āļ‡āļ•āļąāļ§āļ—āļģāļ”āļēāđ€āļĄāļˆ āļ‹āļąāļžāļžāļ­āļĢāđŒāļ•āļšāļąāļŸ/āļ”āļĩāļšāļąāļŸ āđāļĨāļ°āļ•āļąāļ§āļŪāļĩāļĨāđ€āļĨāļ­āļĢāđŒ/āļŠāļīāļĨāđ€āļ”āļ­āļĢāđŒ āļžāļĢāđ‰āļ­āļĄāļĨāļļāļĒāļ—āļļāļāļ„āļ­āļ™āđ€āļ—āļ™āļ•āđŒ!`; + el.synergyAdviceBox.style.borderColor = 'rgba(52, 211, 153, 0.4)'; + } + } + + // Metrics + if (el.synergyAvgLevel) el.synergyAvgLevel.textContent = activeIds.length ? `Lv.${avgLevel}` : '-'; + if (el.synergyAvgEidolon) el.synergyAvgEidolon.textContent = avgEidolon; + if (el.synergyLcReady) el.synergyLcReady.textContent = activeIds.length ? `${lcCount}/${activeIds.length}` : '-'; + if (el.synergyRelicReady) el.synergyRelicReady.textContent = activeIds.length ? `${fullRelicCount}/${activeIds.length}` : '-'; +} + +// ========================================== +// GM Command Dispatcher & Live Log Console +// ========================================== +const consoleState = { + history: [], + historyIndex: -1, + maxLogs: 200 +}; + +function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function logToConsole(message, tag = 'SYS') { + if (!el.consoleTerminalLog) return; + const now = new Date(); + const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; + + let tagClass = 'tag-sys'; + const upperTag = String(tag).toUpperCase(); + if (upperTag === 'CMD' || upperTag === 'GM') tagClass = 'tag-cmd'; + else if (upperTag === 'OK' || upperTag === 'SUCCESS') tagClass = 'tag-ok'; + else if (upperTag === 'ERR' || upperTag === 'ERROR') tagClass = 'tag-err'; + else if (upperTag === 'NET') tagClass = 'tag-net'; + + const lineEl = document.createElement('div'); + lineEl.className = 'log-line'; + lineEl.innerHTML = ` + ${timeStr} + [${escapeHtml(upperTag)}] + ${escapeHtml(message)} + `; + + el.consoleTerminalLog.appendChild(lineEl); + + // Trim excess logs if above maxLogs + while (el.consoleTerminalLog.children.length > consoleState.maxLogs) { + el.consoleTerminalLog.removeChild(el.consoleTerminalLog.firstChild); + } + + // Auto scroll to bottom + el.consoleTerminalLog.scrollTop = el.consoleTerminalLog.scrollHeight; +} + +async function dispatchGMCommand(rawCmd) { + const cmd = (rawCmd || '').trim(); + if (!cmd) return; + + // Save to history + if (consoleState.history[consoleState.history.length - 1] !== cmd) { + consoleState.history.push(cmd); + } + consoleState.historyIndex = -1; + + // Log user command + logToConsole(cmd, 'CMD'); + + // Handle local /help command immediately + if (cmd.toLowerCase() === '/help') { + logToConsole('āļ„āļģāļŠāļąāđˆāļ‡ GM āļ—āļĩāđˆāļžāļĢāđ‰āļ­āļĄāđƒāļŠāđ‰āļ‡āļēāļ™ (Available Commands):', 'SYS'); + logToConsole(' /heal - āļŸāļ·āđ‰āļ™āļŸāļđ HP/Energy āđ€āļ•āđ‡āļĄāļ•āļąāļ§āļĨāļ°āļ„āļĢāđƒāļ™āļ—āļĩāļĄāļ—āļąāđ‰āļ‡āļŦāļĄāļ”', 'SYS'); + logToConsole(' /max - āļ­āļąāļ›āļ•āļąāļ§āļĨāļ°āļ„āļĢāļ—āļąāđ‰āļ‡āļŦāļĄāļ”āđ€āļ›āđ‡āļ™ Lv.80, E6, Traces 10', 'SYS'); + logToConsole(' /teleport [floor] - āļ§āļēāļĢāđŒāļ›āđ„āļ›āļĒāļąāļ‡āļ‰āļēāļ/Plane ID (āđ€āļŠāđˆāļ™ 20001 āļĢāļ–āđ„āļŸ Express)', 'SYS'); + logToConsole(' /scene [floor] - āļ„āļģāļŠāļąāđˆāļ‡āļĒāđˆāļ­āļ‚āļ­āļ‡ /teleport', 'SYS'); + logToConsole(' /clear lineup - āļĨāđ‰āļēāļ‡āļ•āļąāļ§āļĨāļ°āļ„āļĢāđƒāļ™āļ—āļĩāļĄāļ›āļąāļˆāļˆāļļāļšāļąāļ™āļ—āļąāđ‰āļ‡āļŦāļĄāļ”', 'SYS'); + logToConsole(' /avatar [lvl] [rank] - āļ›āļĨāļ”āļĨāđ‡āļ­āļāļŦāļĢāļ·āļ­āļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ•āļąāļ§āļĨāļ°āļ„āļĢ (āđ€āļŠāđˆāļ™ /avatar 1308 80 6)', 'SYS'); + logToConsole(' /give [count] - āđ€āļžāļīāđˆāļĄāđ„āļ­āđ€āļ—āļĄāļŦāļĢāļ·āļ­āđ€āļ‡āļīāļ™ (āđ€āļŠāđˆāļ™ 102 Jades, 2 Credits)', 'SYS'); + logToConsole(' /help - āđāļŠāļ”āļ‡āļ„āļđāđˆāļĄāļ·āļ­āļĢāļēāļĒāļāļēāļĢāļ„āļģāļŠāļąāđˆāļ‡āļ™āļĩāđ‰', 'SYS'); + return; + } + + // If server is online, try dispatching to backend + if (state.isConnected) { + try { + const res = await apiCall('/api/command', 'POST', { command: cmd }); + if (res) { + const msg = res.message || res.msg || 'āļ„āļģāļŠāļąāđˆāļ‡āļ›āļĢāļ°āļĄāļ§āļĨāļœāļĨāļŠāļģāđ€āļĢāđ‡āļˆ'; + logToConsole(msg, 'OK'); + + // Command specific post-processing + const lowerCmd = cmd.toLowerCase(); + if (lowerCmd.startsWith('/heal')) { + showToast('āļŸāļ·āđ‰āļ™āļŸāļđ HP āđāļĨāļ° Energy āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'success'); + refreshStatus(); + } else if (lowerCmd.startsWith('/teleport') || lowerCmd.startsWith('/scene')) { + showToast('āļŠāđˆāļ‡āļ„āļģāļŠāļąāđˆāļ‡āđ€āļ›āļĨāļĩāđˆāļĒāļ™āļ‰āļēāļāđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'success'); + refreshStatus(); + } else if (lowerCmd.startsWith('/clear lineup')) { + showToast('āļĨāđ‰āļēāļ‡āļ•āļąāļ§āļĨāļ°āļ„āļĢāđƒāļ™āļ—āļĩāļĄāđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'info'); + refreshStatus(); + } else if (lowerCmd.startsWith('/max')) { + showToast('āļ›āļĢāļąāļš Max āđ€āļĨāđ€āļ§āļĨāđāļĨāļ° Traces āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'success'); + refreshStatus(); + } else if (lowerCmd.startsWith('/give')) { + showToast('āđ€āļžāļīāđˆāļĄāđ„āļ­āđ€āļ—āļĄ/āđ€āļ‡āļīāļ™āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'success'); + } else if (lowerCmd.startsWith('/avatar')) { + showToast('āļ­āļąāļ›āđ€āļ”āļ•āļ•āļąāļ§āļĨāļ°āļ„āļĢāđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'success'); + loadAvatars(); + } + return; + } + } catch (err) { + logToConsole(`Server Dispatcher: ${err.message} (āđ€āļ‚āđ‰āļēāļŠāļđāđˆāđ‚āļŦāļĄāļ” Local Fallback)`, 'NET'); + } + } + + // Fallback to client-side local executions if server is offline or doesn't support the command + const lowerCmd = cmd.toLowerCase(); + if (lowerCmd.startsWith('/heal')) { + logToConsole('Local Fallback: āļŸāļ·āđ‰āļ™āļŸāļđ Energy āļ‚āļ­āļ‡āļ•āļąāļ§āļĨāļ°āļ„āļĢāđƒāļ™āļ—āļĩāļĄāđ€āļ›āđ‡āļ™ 100%', 'OK'); + if (state.fullFreesrData?.avatars) { + for (const aid in state.fullFreesrData.avatars) { + state.fullFreesrData.avatars[aid].sp_value = 140; + } + saveFreesrChangesToServer(); + } + showToast('āļŸāļ·āđ‰āļ™āļŸāļđāļžāļĨāļąāļ‡āļ‡āļēāļ™āļ—āļĩāļĄāđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'success'); + } else if (lowerCmd.startsWith('/max')) { + logToConsole('Local Fallback: āļāļģāļĨāļąāļ‡āļĢāļąāļ™ maxAllAvatars()...', 'SYS'); + await maxAllAvatars(); + logToConsole('Local: āļ­āļąāļ›āđ€āļĨāđ€āļ§āļĨ 80 āđāļĨāļ° E6 āļ•āļąāļ§āļĨāļ°āļ„āļĢāļ—āļąāđ‰āļ‡āļŦāļĄāļ”āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'OK'); + } else if (lowerCmd.startsWith('/clear lineup')) { + logToConsole('Local Fallback: āļāļģāļĨāļąāļ‡āļĨāđ‰āļēāļ‡āļ—āļĩāļĄ...', 'SYS'); + await quickClearLineup(); + logToConsole('Local: āļĨāđ‰āļēāļ‡āļ—āļĩāļĄāđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒāđāļĨāđ‰āļ§', 'OK'); + } else if (lowerCmd.startsWith('/teleport') || lowerCmd.startsWith('/scene')) { + const parts = cmd.split(/\s+/); + const planeId = parseInt(parts[1], 10) || 20001; + const floorId = parseInt(parts[2], 10) || 1; + logToConsole(`Local Fallback: āļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ‰āļēāļāđ€āļ›āđ‡āļ™ Plane ID ${planeId}, Floor ${floorId}`, 'OK'); + if (state.fullFreesrData) { + state.fullFreesrData.scene = state.fullFreesrData.scene || {}; + state.fullFreesrData.scene.plane_id = planeId; + state.fullFreesrData.scene.floor_id = floorId; + saveFreesrChangesToServer(); + refreshStatus(); + } + showToast(`āđ€āļ›āļĨāļĩāđˆāļĒāļ™āļ‰āļēāļāđ€āļ›āđ‡āļ™ Plane ${planeId} āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ`, 'success'); + } else if (lowerCmd.startsWith('/avatar')) { + const parts = cmd.split(/\s+/); + const aid = parseInt(parts[1], 10); + const lvl = parseInt(parts[2], 10) || 80; + const rank = parseInt(parts[3], 10) || 6; + if (aid) { + if (!state.fullFreesrData) state.fullFreesrData = { avatars: {}, lightcones: [], relics: [] }; + state.fullFreesrData.avatars = state.fullFreesrData.avatars || {}; + state.fullFreesrData.avatars[aid] = { + owner_uid: 0, + avatar_id: aid, + level: lvl, + promotion: lvl > 70 ? 6 : (lvl > 60 ? 5 : 4), + data: { rank, skills: { 1: 10, 2: 10, 3: 10, 4: 10 } } + }; + saveFreesrChangesToServer(); + loadAvatars(); + logToConsole(`Local: āļ›āļĨāļ”āļĨāđ‡āļ­āļ/āļ­āļąāļ›āđ€āļ”āļ• Avatar ID ${aid} (Lv.${lvl}, E${rank}) āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ`, 'OK'); + showToast(`āļ­āļąāļ›āđ€āļ”āļ•āļ•āļąāļ§āļĨāļ°āļ„āļĢ ${aid} āļŠāļģāđ€āļĢāđ‡āļˆ`, 'success'); + } else { + logToConsole('āļĢāļđāļ›āđāļšāļšāļ„āļģāļŠāļąāđˆāļ‡āđ„āļĄāđˆāļ–āļđāļāļ•āđ‰āļ­āļ‡: /avatar [level] [rank]', 'ERR'); + } + } else if (lowerCmd.startsWith('/give')) { + const parts = cmd.split(/\s+/); + const itemId = parseInt(parts[1], 10); + const count = parseInt(parts[2], 10) || 1; + if (itemId) { + logToConsole(`Local: āđ„āļ”āđ‰āļĢāļąāļš Item ID ${itemId} āļˆāļģāļ™āļ§āļ™ ${count.toLocaleString()}`, 'OK'); + showToast(`āđ€āļžāļīāđˆāļĄāđ„āļ­āđ€āļ—āļĄ ${itemId} x${count}`, 'success'); + } else { + logToConsole('āļĢāļđāļ›āđāļšāļšāļ„āļģāļŠāļąāđˆāļ‡āđ„āļĄāđˆāļ–āļđāļāļ•āđ‰āļ­āļ‡: /give [count]', 'ERR'); + } + } else { + logToConsole(`āļ„āļģāļŠāļąāđˆāļ‡āļ—āļĩāđˆāđ„āļĄāđˆāļĢāļđāđ‰āļˆāļąāļāļŦāļĢāļ·āļ­āđ„āļĄāđˆāļĢāļ­āļ‡āļĢāļąāļšāđƒāļ™āđ‚āļŦāļĄāļ”āļ­āļ­āļŸāđ„āļĨāļ™āđŒ: "${cmd}" (āļžāļīāļĄāļžāđŒ /help āđ€āļžāļ·āđˆāļ­āļ”āļđāļ„āļģāļŠāļąāđˆāļ‡)`, 'ERR'); + } +} + +function setupGMConsoleEvents() { + if (el.formConsoleCmd) { + el.formConsoleCmd.addEventListener('submit', (e) => { + e.preventDefault(); + if (!el.consoleCmdInput) return; + const cmd = el.consoleCmdInput.value.trim(); + if (!cmd) return; + el.consoleCmdInput.value = ''; + dispatchGMCommand(cmd); + }); + } + + // Key navigation in console input + if (el.consoleCmdInput) { + el.consoleCmdInput.addEventListener('keydown', (e) => { + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (consoleState.history.length === 0) return; + if (consoleState.historyIndex === -1) { + consoleState.historyIndex = consoleState.history.length - 1; + } else if (consoleState.historyIndex > 0) { + consoleState.historyIndex--; + } + el.consoleCmdInput.value = consoleState.history[consoleState.historyIndex] || ''; + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (consoleState.history.length === 0 || consoleState.historyIndex === -1) return; + if (consoleState.historyIndex < consoleState.history.length - 1) { + consoleState.historyIndex++; + el.consoleCmdInput.value = consoleState.history[consoleState.historyIndex]; + } else { + consoleState.historyIndex = -1; + el.consoleCmdInput.value = ''; + } + } + }); + } + + // Quick pill buttons + document.querySelectorAll('.console-pill-btn').forEach(btn => { + btn.addEventListener('click', () => { + const cmd = btn.getAttribute('data-cmd'); + if (cmd) { + if (el.consoleCmdInput) el.consoleCmdInput.value = cmd; + dispatchGMCommand(cmd); + } + }); + }); + + // Copy Log button + if (el.btnConsoleCopyLog) { + el.btnConsoleCopyLog.addEventListener('click', () => { + if (!el.consoleTerminalLog) return; + const text = el.consoleTerminalLog.innerText || el.consoleTerminalLog.textContent; + navigator.clipboard?.writeText(text).then(() => { + showToast('āļ„āļąāļ”āļĨāļ­āļ Log āļ—āļąāđ‰āļ‡āļŦāļĄāļ”āļĨāļ‡āđƒāļ™āļ„āļĨāļīāļ›āļšāļ­āļĢāđŒāļ”āđāļĨāđ‰āļ§', 'success'); + }).catch(() => { + showToast('āđ„āļĄāđˆāļŠāļēāļĄāļēāļĢāļ–āļ„āļąāļ”āļĨāļ­āļ Log āđ„āļ”āđ‰', 'error'); + }); + }); + } + + // Clear Log button + if (el.btnConsoleClearLog) { + el.btnConsoleClearLog.addEventListener('click', () => { + if (el.consoleTerminalLog) { + el.consoleTerminalLog.innerHTML = ''; + logToConsole('āļĨāđ‰āļēāļ‡āļŦāļ™āđ‰āļēāļˆāļ­ Terminal āđ€āļĢāļĩāļĒāļšāļĢāđ‰āļ­āļĒ', 'SYS'); + } + }); + } + + // Initial greeting + logToConsole('GaC Server Console āļžāļĢāđ‰āļ­āļĄāđƒāļŠāđ‰āļ‡āļēāļ™ (āļžāļīāļĄāļžāđŒ /help āđ€āļžāļ·āđˆāļ­āļ”āļđāļ„āļģāļŠāļąāđˆāļ‡āļ—āļąāđ‰āļ‡āļŦāļĄāļ”)', 'SYS'); } // Add extra slot in Creative Mode @@ -5908,6 +6337,7 @@ async function init() { setupModalSubtabs(); setupEvents(); setupGachabaseSyncEvents(); + setupGMConsoleEvents(); applyTranslations(); renderLineup(state.statusData?.lineups || DEFAULT_LINEUP); renderAvatarsGrid();