feat(dashboard): replace quick actions with team synergy analysis and interactive gm console dispatcher

This commit is contained in:
CAtlas0784 committed 2026-10-17 10:55:13 +07:00
1 parent 039609980e
commit bd367a11cd
3 files changed
+765 -94

No files matched your search

+188
View File
@@ -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;
}
+141 -88
View File
@@ -104,109 +104,162 @@
</div>
</div>
<div class="grid-2">
<!-- Server & Map Card -->
<div class="card">
<div class="card-title">
<span data-i18n="serverStatusTitle">📍 สถานะเซิร์ฟเวอร์ & พิกัดปัจจุบัน</span>
<span class="btn-sm btn-secondary" id="badge-version">API Live</span>
<div class="grid-2" style="align-items: start;">
<!-- Column 1: Server Status & Team Combat Synergy -->
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
<!-- 1. Server & Map Card -->
<div class="card">
<div class="card-title">
<span data-i18n="serverStatusTitle">📍 สถานะเซิร์ฟเวอร์ & พิกัดปัจจุบัน</span>
<span class="btn-sm btn-secondary" id="badge-version">API Live</span>
</div>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<div>
<span class="form-label" data-i18n="lblServerModeActive">โหมดเซิร์ฟเวอร์ที่ใช้งาน</span>
<strong id="lbl-active-mode" style="color: var(--accent-cyan);">GaC-hkrpg-go</strong>
</div>
<div>
<span class="form-label" data-i18n="lblApiEndpoint">API Gateway Endpoint</span>
<code id="lbl-api-url" style="color: var(--accent-cyan); font-size: 0.9rem;">-</code>
</div>
<div style="display: flex; gap: 1rem;">
<div>
<span class="form-label" data-i18n="lblPlaneId">Plane ID</span>
<span id="lbl-plane-id" style="font-weight: bold;">-</span>
</div>
<div>
<span class="form-label" data-i18n="lblFloorId">Floor ID</span>
<span id="lbl-floor-id" style="font-weight: bold;">-</span>
</div>
</div>
<div>
<span class="form-label" data-i18n="coords">พิกัดตัวละคร XYZ</span>
<span id="lbl-coordinates" style="color: var(--text-muted); font-size: 0.85rem;">-</span>
</div>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<div>
<span class="form-label" data-i18n="lblServerModeActive">โหมดเซิร์ฟเวอร์ที่ใช้งาน</span>
<strong id="lbl-active-mode" style="color: var(--accent-cyan);">GaC-hkrpg-go</strong>
<!-- 2. Team Combat Synergy & Weakness Break Analysis -->
<div class="card" id="card-team-synergy">
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<span style="display: flex; align-items: center; gap: 0.5rem;">
<span>⚔️</span>
<span>วิเคราะห์ Synergy ทีม & จุดอ่อน</span>
</span>
<span class="badge-super-tag" id="badge-synergy-coverage" style="background: rgba(56, 189, 248, 0.15); color: var(--accent-cyan); font-weight: 800;">ครอบคลุม 0/7 ธาตุ</span>
</div>
<div>
<span class="form-label" data-i18n="lblApiEndpoint">API Gateway Endpoint</span>
<code id="lbl-api-url" style="color: var(--accent-cyan); font-size: 0.9rem;">-</code>
</div>
<div style="display: flex; gap: 1rem;">
<div>
<span class="form-label" data-i18n="lblPlaneId">Plane ID</span>
<span id="lbl-plane-id" style="font-weight: bold;">-</span>
<p class="card-subtitle" style="margin-bottom: 0.85rem;">วิเคราะห์การครอบคลุม Weakness Break, ความสมดุลของบทบาท และความพร้อมของทีม</p>
<!-- A. Weakness Break Coverage 7 Elements -->
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.4rem;">
🎯 การทำลายจุดอ่อน (Weakness Break Coverage)
</div>
<div>
<span class="form-label" data-i18n="lblFloorId">Floor ID</span>
<span id="lbl-floor-id" style="font-weight: bold;">-</span>
<div class="synergy-elements-grid" id="synergy-elements-container">
<!-- Rendered dynamically by JS: Physical, Fire, Ice, Lightning, Wind, Quantum, Imaginary -->
</div>
</div>
<!-- B. Team Role Balance -->
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.4rem;">
🛡️ การกระจายบทบาทในทีม (Team Composition & Roles)
</div>
<div id="synergy-roles-container" style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem;">
<!-- Role tags: DPS, Sub-DPS, Buffer/Debuffer, Sustain -->
</div>
<div id="synergy-advice-box" style="padding: 0.6rem 0.8rem; border-radius: 6px; font-size: 0.8rem; line-height: 1.4; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08);">
<!-- Dynamic Advice -->
</div>
</div>
<!-- C. Equipment Readiness & Stats Overview -->
<div>
<span class="form-label" data-i18n="coords">พิกัดตัวละคร XYZ</span>
<span id="lbl-coordinates" style="color: var(--text-muted); font-size: 0.85rem;">-</span>
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.4rem;">
📊 ความพร้อมของอุปกรณ์และเลเวลเฉลี่ย
</div>
<div class="synergy-metrics-grid">
<div class="synergy-metric-box">
<div class="synergy-metric-val" id="synergy-avg-level">-</div>
<div class="synergy-metric-lbl">เลเวลเฉลี่ย</div>
</div>
<div class="synergy-metric-box">
<div class="synergy-metric-val" id="synergy-avg-eidolon">-</div>
<div class="synergy-metric-lbl">Eidolon เฉลี่ย</div>
</div>
<div class="synergy-metric-box">
<div class="synergy-metric-val" id="synergy-lc-ready">-</div>
<div class="synergy-metric-lbl">สวมกรวยแสง</div>
</div>
<div class="synergy-metric-box">
<div class="synergy-metric-val" id="synergy-relic-ready">-</div>
<div class="synergy-metric-lbl">รีลิกส์ครบชุด</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions Card -->
<div class="card">
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center;">
<span data-i18n="quickActionsTitle">⚡ ดำเนินการด่วน</span>
<span class="btn-sm btn-secondary" style="font-size: 0.75rem;" data-i18n="quickActionsBadge">คำสั่งด่วน</span>
<!-- Column 2: GM Command Dispatcher & Live Log Console -->
<div class="card" id="card-gm-console" style="display: flex; flex-direction: column; gap: 0.75rem;">
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<span style="display: flex; align-items: center; gap: 0.5rem;">
<span>💻</span>
<span>คอนโซลคำสั่ง GM & Log สด</span>
</span>
<div style="display: flex; gap: 0.35rem;">
<button type="button" class="btn btn-sm btn-secondary" id="btn-console-copy-log" title="คัดลอก Log ทั้งหมด" style="padding: 0.2rem 0.6rem; font-size: 0.75rem;">
📋 คัดลอก
</button>
<button type="button" class="btn btn-sm btn-secondary" id="btn-console-clear-log" title="ล้างหน้าจอ Terminal" style="padding: 0.2rem 0.6rem; font-size: 0.75rem;">
🗑️ ล้าง Log
</button>
</div>
</div>
<p class="card-subtitle" data-i18n="quickActionsSubtitle">เครื่องมือจัดการทีมและเซิร์ฟเวอร์แบบทันที</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 0.75rem;">
<!-- 1. Set Team Lineup to E0S1 (Standard) -->
<button type="button" class="btn btn-primary" id="btn-quick-lineup-e0s1" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem;">
<span style="font-size: 1.35rem; line-height: 1;">⚡</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem;" data-i18n="qaLineupE0S1Title">ตั้งค่าทีมเป็น E0S1</div>
<div style="font-size: 0.725rem; opacity: 0.9;" data-i18n="qaLineupE0S1Desc">ปรับตัวละครในทีมเป็น Lv.80, E0, S1 สกิลเต็ม</div>
</div>
</button>
<p class="card-subtitle" style="margin-bottom: 0.25rem;">ส่งคำสั่ง GM / Server commands แบบเรียลไทม์ พร้อมติดตามสถานะและ Packet Log</p>
<!-- 2. Equip Recommended Relics for Team -->
<button type="button" class="btn btn-secondary" id="btn-quick-lineup-relics" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem;">
<span style="font-size: 1.35rem; line-height: 1;">🛡️</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem;" data-i18n="qaLineupRelicsTitle">ติดตั้งรีลิกส์แนะนำทั้งทีม</div>
<div style="font-size: 0.725rem; color: var(--text-muted);" data-i18n="qaLineupRelicsDesc">สวมใส่รีลิกส์และเครื่องประดับให้ทุกคนในทีม</div>
</div>
</button>
<!-- Quick GM Command Pills -->
<div>
<div style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.35rem;">
⚡ คำสั่งด่วนที่ใช้บ่อย (Quick Commands)
</div>
<div class="console-quick-pills">
<button type="button" class="console-pill-btn" data-cmd="/heal" title="ฟื้นฟู HP/Energy ทั้งทีม">💖 /heal</button>
<button type="button" class="console-pill-btn" data-cmd="/give 102 100000" title="รับ 100,000 Stellar Jades">💎 +100k Jades</button>
<button type="button" class="console-pill-btn" data-cmd="/give 2 10000000" title="รับ 10,000,000 Credits">💰 +10M Credits</button>
<button type="button" class="console-pill-btn" data-cmd="/max" title="อัปเต็มเลเวล 80 E6 ทุกตัว">⚡ /max</button>
<button type="button" class="console-pill-btn" data-cmd="/teleport 20001 1" title="วาร์ปไปรถไฟ Astral Express">🚂 รถไฟ Express</button>
<button type="button" class="console-pill-btn" data-cmd="/clear lineup" title="ล้างตัวละครในทีม">🧹 เคลียร์ทีม</button>
<button type="button" class="console-pill-btn" data-cmd="/help" title="ดูรายการคำสั่งทั้งหมด">❓ /help</button>
</div>
</div>
<!-- 3. Unlock All Avatars -->
<button type="button" class="btn btn-gold" id="btn-quick-unlock-all-avatars" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem;">
<span style="font-size: 1.35rem; line-height: 1;">👥</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem;" data-i18n="qaUnlockAllTitle">ปลดล็อกตัวละครครบทุกตัว</div>
<div style="font-size: 0.725rem; opacity: 0.85;" data-i18n="qaUnlockAllDesc">เพิ่มครบ 70+ ตัว E6 เต็มคลัง</div>
</div>
</button>
<!-- Interactive Terminal Screen -->
<div class="console-terminal-wrapper">
<div class="console-terminal-header">
<span class="terminal-dot dot-red"></span>
<span class="terminal-dot dot-yellow"></span>
<span class="terminal-dot dot-green"></span>
<span class="terminal-title">GaC-hkrpg-go Console Session (Interactive GM Dispatcher)</span>
</div>
<div class="console-terminal-log" id="console-terminal-log">
<!-- Logs rendered here -->
</div>
</div>
<!-- 4. Add All 5-Star Light Cones -->
<button type="button" class="btn btn-secondary" id="btn-quick-add-all-lcs" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem;">
<span style="font-size: 1.35rem; line-height: 1;">🗡️</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem;" data-i18n="qaAddLcsTitle">เพิ่มกรวยแสง 5★ ทั้งหมด</div>
<div style="font-size: 0.725rem; color: var(--text-muted);" data-i18n="qaAddLcsDesc">กรวยแสงประจำตัว S5 เข้าคลัง</div>
</div>
</button>
<!-- 5. Teleport to Astral Express -->
<button type="button" class="btn btn-secondary" id="btn-quick-teleport-train" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem;">
<span style="font-size: 1.35rem; line-height: 1;">🚂</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem;" data-i18n="qaTeleportTitle">วาร์ปกลับรถไฟ Astral Express</div>
<div style="font-size: 0.725rem; color: var(--text-muted);" data-i18n="qaTeleportDesc">กลับสู่ Parlor Car จุดปลอดภัย</div>
</div>
</button>
<!-- 6. Clear Lineup Slots -->
<button type="button" class="btn btn-danger" id="btn-quick-clear-lineup" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem;">
<span style="font-size: 1.35rem; line-height: 1;">🧹</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem;" data-i18n="qaClearLineupTitle">ล้างตัวละครในทีม</div>
<div style="font-size: 0.725rem; opacity: 0.85;" data-i18n="qaClearLineupDesc">เคลียร์ทุกช่องเพื่อจัดทีมใหม่</div>
</div>
</button>
<!-- 7. Gachabase Game Data & Asset Sync -->
<button type="button" class="btn btn-secondary" id="btn-quick-gachabase-sync" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.6rem; text-align: left; padding: 0.75rem 0.9rem; border-color: rgba(245, 158, 11, 0.4);">
<span style="font-size: 1.35rem; line-height: 1;">🌐</span>
<div>
<div style="font-weight: 700; font-size: 0.875rem; color: #fbbf24;" data-i18n="qaGachabaseTitle">ซิงค์ข้อมูลเกม Gachabase</div>
<div style="font-size: 0.725rem; color: var(--text-muted);" data-i18n="qaGachabaseDesc">อัปเดตข้อมูล & รูปภาพเมื่อเกมมีแพตช์ใหม่</div>
</div>
<!-- Command Input Bar -->
<form id="form-console-cmd" style="display: flex; gap: 0.5rem; align-items: center; margin-top: 0.25rem;">
<div class="console-input-wrap" style="flex: 1; display: flex; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--border-color); border-radius: 6px; padding: 0 0.75rem;">
<span style="color: var(--accent-cyan); font-family: monospace; font-weight: 800; font-size: 1.1rem; margin-right: 0.5rem;">&gt;</span>
<input type="text" id="console-cmd-input" class="form-input" placeholder="พิมพ์คำสั่ง GM เช่น /heal, /max, /teleport 20411, /give 102 1000..." style="background: transparent; border: none; padding: 0.6rem 0; font-family: monospace; font-size: 0.85rem; box-shadow: none;" autocomplete="off">
</div>
<button type="submit" class="btn btn-primary" id="btn-console-cmd-send" style="padding: 0.6rem 1.25rem; font-weight: 700; white-space: nowrap;">
ส่งคำสั่ง
</button>
</form>
<div style="font-size: 0.725rem; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center;">
<span>💡 รองรับกด <kbd style="background: rgba(255,255,255,0.1); padding: 0.1rem 0.35rem; border-radius: 3px; font-family: monospace;">↑</kbd> <kbd style="background: rgba(255,255,255,0.1); padding: 0.1rem 0.35rem; border-radius: 3px; font-family: monospace;">↓</kbd> เพื่อเลือกประวัติคำสั่งก่อนหน้า</span>
<span id="lbl-console-conn-status" style="color: var(--accent-green);">● เซิร์ฟเวอร์ออนไลน์</span>
</div>
</div>
</div>
+436 -6
View File
@@ -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 `
<div class="synergy-element-chip ${isCovered ? 'active' : 'inactive'}" style="${isCovered ? `border-color: ${elem.color}66; box-shadow: 0 0 8px ${elem.color}22;` : ''}">
${getElementIconHtml(elem.key, 22)}
<span class="synergy-element-name" style="${isCovered ? `color: ${elem.color};` : ''}">${elem.nameTh}</span>
<span class="synergy-element-chars" title="${providerText}">${providerText}</span>
</div>
`;
}).join('');
// Render Role Tags
if (el.synergyRolesContainer) {
el.synergyRolesContainer.innerHTML = `
<div class="badge-super-tag" style="background: rgba(239, 68, 68, 0.15); color: #f87171; font-weight: 700;">
🗡️ DPS (${roles.dps.length}): ${roles.dps.join(', ') || '-'}
</div>
<div class="badge-super-tag" style="background: rgba(168, 85, 247, 0.15); color: #c084fc; font-weight: 700;">
⚡ ซัพพอร์ต/ดีบัฟ (${roles.support.length}): ${roles.support.join(', ') || '-'}
</div>
<div class="badge-super-tag" style="background: rgba(52, 211, 153, 0.15); color: #34d399; font-weight: 700;">
🛡️ Sustain ฮีล/ชิลด์ (${roles.sustain.length}): ${roles.sustain.join(', ') || '-'}
</div>
${roles.specialist.length ? `
<div class="badge-super-tag" style="background: rgba(56, 189, 248, 0.15); color: #38bdf8; font-weight: 700;">
🔮 พิเศษ/FUA (${roles.specialist.length}): ${roles.specialist.join(', ')}
</div>
` : ''}
`;
}
// Dynamic Advice Text
if (el.synergyAdviceBox) {
if (activeIds.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: var(--text-muted);">ยังไม่มีตัวละครในทีม กรุณาเลือกตัวละครในทีมด้านบน</span>`;
} else if (roles.sustain.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: #fbbf24;">⚠️ <strong>ข้อแนะนำ:</strong> ทีมยังขาดตัวละครสายป้องกันหรือรักษา (Preservation / Abundance) แนะนำใส่ 1 ตำแหน่งเพื่อความปลอดภัยในการเล่น</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)';
} else if (roles.dps.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: #38bdf8;">💡 <strong>ข้อแนะนำ:</strong> ทีมมีแต่ตัวซัพพอร์ต ยังไม่มีตัวทำดาเมจหลัก (Destruction / Hunt / Erudition)</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(56, 189, 248, 0.4)';
} else if (roles.support.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: #fbbf24;">💡 <strong>ข้อแนะนำ:</strong> ทีมมีตัวดาเมจแต่ยังไม่มีตัวบัฟหรือดีบัฟ (Harmony / Nihility) เสริมตัวซัพพอร์ตจะช่วยเร่งดาเมจได้มหาศาล</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)';
} else {
el.synergyAdviceBox.innerHTML = `<span style="color: #34d399;">✅ <strong>สมดุลยอดเยี่ยม:</strong> ทีมมีโครงสร้างครบเครื่อง ทั้งตัวทำดาเมจ ซัพพอร์ตบัฟ/ดีบัฟ และตัวฮีลเลอร์/ชิลเดอร์ พร้อมลุยทุกคอนเทนต์!</span>`;
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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 = `
<span class="log-time">${timeStr}</span>
<span class="log-tag ${tagClass}">[${escapeHtml(upperTag)}]</span>
<span class="log-msg">${escapeHtml(message)}</span>
`;
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 <plane> [floor] - วาร์ปไปยังฉาก/Plane ID (เช่น 20001 รถไฟ Express)', 'SYS');
logToConsole(' /scene <plane> [floor] - คำสั่งย่อของ /teleport', 'SYS');
logToConsole(' /clear lineup - ล้างตัวละครในทีมปัจจุบันทั้งหมด', 'SYS');
logToConsole(' /avatar <id> [lvl] [rank] - ปลดล็อกหรือตั้งค่าตัวละคร (เช่น /avatar 1308 80 6)', 'SYS');
logToConsole(' /give <id> [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 <id> [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 <id> [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();