diff --git a/index.html b/index.html index ebb643d..bb7a20a 100644 --- a/index.html +++ b/index.html @@ -59,6 +59,7 @@ + @@ -157,7 +158,7 @@ ครอบคลุม 0/7 ธาตุ -

วิเคราะห์การครอบคลุม Weakness Break, ความสมดุลของบทบาท และความพร้อมของทีม

+

วิเคราะห์การครอบคลุม Weakness Break และความพร้อมของอุปกรณ์ในทีม

@@ -169,20 +170,7 @@
- -
-
- 🛡️ การกระจายบทบาทในทีม (Team Composition & Roles) -
-
- -
-
- -
-
- - +
📊 ความพร้อมของอุปกรณ์และเลเวลเฉลี่ย @@ -206,6 +194,18 @@
+ + +
+
+ 💥 +
+
Damage Analyst (จับดาเมจจากในเกม)
+
ยังไม่มีข้อมูลการต่อสู้ล่าสุด
+
+
+ +
@@ -387,6 +387,101 @@ + +
+ +
+
+
+
+ 💥 วิเคราะห์ความเสียหายจากการต่อสู้ (Damage Analyst) + พร้อมรับข้อมูลจากเกม +
+

+ ดักจับและวิเคราะห์ดาเมจจากการต่อสู้จริงในเกม Star Rail (ระบบเดียวกับ FireflyGO / Veritas) +

+
+
+ + + + + + +
+
+
+ + +
+
+
ดาเมจรวมทั้งทีม
+
0
+
เฉลี่ยต่อรอบ: -
+
+ +
+
จำนวนรอบ (Cycles)
+
0 รอบ
+
MoC / PF / AS Standard
+
+ +
+
เทิร์นทั้งหมด
+
0 เทิร์น
+
เทิร์นอัตโนมัติ: 0
+
+ +
+
ระยะเวลาการต่อสู้
+
00:00
+
Stage: - | Battle: -
+
+
+ + +
+
+ สัดส่วนดาเมจของทีม (Team DPS Share) + รวม 100% +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ 👾 รายชื่อศัตรูที่ถูกโจมตี (Targets / Enemies) +
+
+ +
+
+
+
diff --git a/js/app.js b/js/app.js index 89c10fd..1af063c 100644 --- a/js/app.js +++ b/js/app.js @@ -77,12 +77,37 @@ const el = { 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'), + synergyDamageBanner: document.getElementById('synergy-damage-banner'), + dashboardBattleTitle: document.getElementById('dashboard-battle-title'), + dashboardBattleSummary: document.getElementById('dashboard-battle-summary'), + + // Damage Analyst + tabDamageAnalyst: document.getElementById('tab-damage-analyst'), + tabBtnDamageAnalyst: document.getElementById('tab-btn-damage-analyst'), + badgeAnalystStatus: document.getElementById('badge-analyst-status'), + chkAnalystAutosync: document.getElementById('chk-analyst-autosync'), + btnAnalystFetch: document.getElementById('btn-analyst-fetch'), + inputAnalystFile: document.getElementById('input-analyst-file'), + btnAnalystImport: document.getElementById('btn-analyst-import'), + btnAnalystExport: document.getElementById('btn-analyst-export'), + btnAnalystClear: document.getElementById('btn-analyst-clear'), + analystTeamDamage: document.getElementById('analyst-team-damage'), + analystDamageAv: document.getElementById('analyst-damage-av'), + analystCycles: document.getElementById('analyst-cycles'), + analystCyclesNote: document.getElementById('analyst-cycles-note'), + analystTotalTurns: document.getElementById('analyst-total-turns'), + analystAutoTurns: document.getElementById('analyst-auto-turns'), + analystDuration: document.getElementById('analyst-duration'), + analystBattleMeta: document.getElementById('analyst-battle-meta'), + analystShareBarContainer: document.getElementById('analyst-share-bar-container'), + analystShareLegend: document.getElementById('analyst-share-legend'), + analystCharactersContainer: document.getElementById('analyst-characters-container'), + analystEnemiesContainer: document.getElementById('analyst-enemies-container'), + analystEnemiesCard: document.getElementById('analyst-enemies-card'), // GM Command Dispatcher & Live Log Console cardGmConsole: document.getElementById('card-gm-console'), @@ -1350,51 +1375,13 @@ function renderTeamSynergy(lineups) { `; }).join(''); - // Render Role Tags - if (el.synergyRolesContainer) { - el.synergyRolesContainer.innerHTML = ` -
- ${t('roleDps')} (${roles.dps.length}): ${roles.dps.join(', ') || '-'} -
-
- ${t('roleSupport')} (${roles.support.length}): ${roles.support.join(', ') || '-'} -
-
- ${t('roleSustain')} (${roles.sustain.length}): ${roles.sustain.join(', ') || '-'} -
- ${roles.specialist.length ? ` -
- ${t('roleSpecialist')} (${roles.specialist.length}): ${roles.specialist.join(', ')} -
- ` : ''} - `; - } - - // Dynamic Advice Text - if (el.synergyAdviceBox) { - if (activeIds.length === 0) { - el.synergyAdviceBox.innerHTML = `${t('adviceEmpty')}`; - el.synergyAdviceBox.style.borderColor = 'rgba(255,255,255,0.08)'; - } else if (roles.sustain.length === 0) { - el.synergyAdviceBox.innerHTML = `${t('adviceNoSustain')}`; - el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)'; - } else if (roles.dps.length === 0) { - el.synergyAdviceBox.innerHTML = `${t('adviceNoDps')}`; - el.synergyAdviceBox.style.borderColor = 'rgba(56, 189, 248, 0.4)'; - } else if (roles.support.length === 0) { - el.synergyAdviceBox.innerHTML = `${t('adviceNoSupport')}`; - el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)'; - } else { - el.synergyAdviceBox.innerHTML = `${t('adviceBalanced')}`; - 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}` : '-'; + + updateDashboardBattleBanner(); } // ========================================== @@ -7386,6 +7373,675 @@ async function initGitBranchDisplay() { applyBranchToUI(currentGitBranch); } +// ========================================== +// In-Game Battle Telemetry & Damage Analyst +// ========================================== + +const analystState = { + currentReport: null, + isFetching: false, + lastBattleTimestamp: 0, + autoSyncInterval: null +}; + +function formatNumber(num) { + if (num === null || num === undefined || isNaN(num)) return '0'; + return Math.round(num).toLocaleString('en-US'); +} + +function formatDuration(seconds) { + if (!seconds || isNaN(seconds)) return '00:00'; + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + +function parseFireflySranalysisJson(json) { + if (!json || (!json.lineup && !json.skillHistory && !json.turnHistory)) return null; + + const avatarsMap = {}; + const teamLineup = json.lineup || []; + teamLineup.forEach(item => { + const aid = item.avatarId; + if (aid && aid > 0) { + avatarsMap[aid] = { + avatar_id: aid, + level: 80, + rank: 6, + total_damage: 0, + damage_percent: 0, + total_damage_taken: 0, + total_heal: 0, + total_shield: 0, + total_break_damage: 0, + total_turns: 0, + extra_turns: 0, + attack_type_damage: {}, + attack_type_max_damage: {}, + skill_times: [] + }; + } + }); + + let maxCycles = 0; + if (Array.isArray(json.turnHistory)) { + json.turnHistory.forEach(turn => { + if (turn.cycleIndex > maxCycles) maxCycles = turn.cycleIndex; + const aid = turn.avatarId; + if (aid && avatarsMap[aid]) { + avatarsMap[aid].total_turns++; + } + }); + } + + let teamTotalDmg = 0; + if (Array.isArray(json.skillHistory)) { + json.skillHistory.forEach(sk => { + const aid = sk.avatarId; + if (!aid || aid <= 0) return; + if (!avatarsMap[aid]) { + avatarsMap[aid] = { + avatar_id: aid, + level: 80, + rank: 6, + total_damage: 0, + damage_percent: 0, + total_damage_taken: 0, + total_heal: 0, + total_shield: 0, + total_break_damage: 0, + total_turns: 1, + extra_turns: 0, + attack_type_damage: {}, + attack_type_max_damage: {}, + skill_times: [] + }; + } + const av = avatarsMap[aid]; + const dmg = sk.totalDamage || 0; + av.total_damage += dmg; + teamTotalDmg += dmg; + + const skType = sk.skillType || 'Other'; + av.attack_type_damage[skType] = (av.attack_type_damage[skType] || 0) + dmg; + + if (Array.isArray(sk.damageDetail)) { + sk.damageDetail.forEach(d => { + const detailDmg = d.damage || 0; + const dType = d.type || skType; + if (detailDmg > (av.attack_type_max_damage[dType] || 0)) { + av.attack_type_max_damage[dType] = detailDmg; + } + }); + } + }); + } + + const avatarsList = Object.values(avatarsMap); + avatarsList.forEach(a => { + if (teamTotalDmg > 0) { + a.damage_percent = (a.total_damage / teamTotalDmg) * 100; + } + }); + + return { + timestamp: Date.now(), + battle_id: Math.floor(Math.random() * 90000) + 10000, + stage_id: 1001, + end_status: 'WIN', + total_battle_turns: json.turnHistory?.length || 15, + total_auto_turns: 0, + round_cnt: maxCycles, + cost_time: 48, + challenge_score: 0, + team_total_damage: teamTotalDmg, + team_total_heal: 0, + team_total_shield: 0, + avatars: avatarsList, + monsters: [] + }; +} + +async function fetchLatestBattleReport(silent = false) { + if (analystState.isFetching) return; + analystState.isFetching = true; + try { + const data = await apiRequest('/api/battle_report', 'GET'); + if (data && data.has_report && data.report) { + const isNew = data.report.timestamp !== analystState.lastBattleTimestamp; + analystState.currentReport = data.report; + analystState.lastBattleTimestamp = data.report.timestamp; + renderDamageAnalyst(); + updateDashboardBattleBanner(); + if (!silent) { + showToast(t('analystToastFetched'), 'success'); + } else if (isNew) { + showToast(t('analystToastFetched'), 'info'); + } + } else { + if (!silent) { + showToast(data?.message || t('analystNoBattle'), 'info'); + } + if (!analystState.currentReport) { + renderEmptyDamageAnalyst(); + } + } + } catch (err) { + if (!silent) { + showToast(`Error: ${err.message}`, 'error'); + } + } finally { + analystState.isFetching = false; + } +} + +function renderEmptyDamageAnalyst() { + if (el.analystTeamDamage) el.analystTeamDamage.textContent = '0'; + if (el.analystCycles) el.analystCycles.textContent = `0 ${t('analystCycles')}`; + if (el.analystTotalTurns) el.analystTotalTurns.textContent = `0 ${t('analystTurns')}`; + if (el.analystDuration) el.analystDuration.textContent = '00:00'; + if (el.analystDamageAv) el.analystDamageAv.textContent = 'เฉลี่ย: -'; + if (el.analystBattleMeta) el.analystBattleMeta.textContent = 'Stage: - | Battle: -'; + + if (el.badgeAnalystStatus) { + el.badgeAnalystStatus.textContent = 'พร้อมรับข้อมูลจากเกม'; + el.badgeAnalystStatus.style.background = 'rgba(34, 197, 94, 0.15)'; + el.badgeAnalystStatus.style.color = '#4ade80'; + } + + if (el.analystShareBarContainer) { + el.analystShareBarContainer.innerHTML = '
'; + } + if (el.analystShareLegend) { + el.analystShareLegend.innerHTML = ''; + } + if (el.analystCharactersContainer) { + el.analystCharactersContainer.innerHTML = ` +
+
⚔️
+
${t('analystNoBattle')}
+
+ + +
+
+ `; + const btnF = document.getElementById('btn-empty-analyst-fetch'); + if (btnF) btnF.addEventListener('click', () => fetchLatestBattleReport(false)); + const btnD = document.getElementById('btn-empty-analyst-demo'); + if (btnD) btnD.addEventListener('click', () => loadDemoBattleReport()); + } + + if (el.analystEnemiesCard) { + el.analystEnemiesCard.style.display = 'none'; + } +} + +function renderDamageAnalyst() { + const report = analystState.currentReport; + if (!report) { + renderEmptyDamageAnalyst(); + return; + } + + const teamDmg = report.team_total_damage || 0; + if (el.analystTeamDamage) el.analystTeamDamage.textContent = formatNumber(teamDmg); + if (el.analystCycles) { + el.analystCycles.textContent = (report.round_cnt !== undefined) ? `${report.round_cnt} รอบ (${report.round_cnt}-Cycle)` : '0 รอบ'; + } + if (el.analystTotalTurns) el.analystTotalTurns.textContent = `${report.total_battle_turns || 0} เทิร์น`; + if (el.analystDuration) el.analystDuration.textContent = formatDuration(report.cost_time); + if (el.analystDamageAv) { + el.analystDamageAv.textContent = (report.round_cnt > 0) + ? `เฉลี่ย: ${(teamDmg / report.round_cnt).toFixed(0).toLocaleString()} / รอบ` + : `เฉลี่ย: ${(teamDmg / Math.max(1, report.total_battle_turns || 1)).toFixed(0).toLocaleString()} / เทิร์น`; + } + if (el.analystBattleMeta) { + el.analystBattleMeta.textContent = `Stage: ${report.stage_id || '-'} | Battle: #${report.battle_id || '-'}`; + } + + if (el.badgeAnalystStatus) { + if (report.end_status === 'WIN') { + el.badgeAnalystStatus.textContent = t('analystStatusWin'); + el.badgeAnalystStatus.style.background = 'rgba(34, 197, 94, 0.15)'; + el.badgeAnalystStatus.style.color = '#4ade80'; + } else if (report.end_status === 'QUIT') { + el.badgeAnalystStatus.textContent = t('analystStatusQuit'); + el.badgeAnalystStatus.style.background = 'rgba(245, 158, 11, 0.15)'; + el.badgeAnalystStatus.style.color = '#fbbf24'; + } else { + el.badgeAnalystStatus.textContent = t('analystStatusLose'); + el.badgeAnalystStatus.style.background = 'rgba(239, 68, 68, 0.15)'; + el.badgeAnalystStatus.style.color = '#f87171'; + } + } + + const colors = ['#ef4444', '#38bdf8', '#34d399', '#fbbf24', '#c084fc', '#ec4899']; + + // Team Share Bar + if (el.analystShareBarContainer) { + if (report.avatars && report.avatars.length > 0) { + el.analystShareBarContainer.innerHTML = report.avatars.map((a, idx) => { + const color = colors[idx % colors.length]; + const pct = a.damage_percent || (teamDmg > 0 ? (a.total_damage / teamDmg) * 100 : 0); + const info = getAvatarInfo(a.avatar_id); + return `
`; + }).join(''); + } else { + el.analystShareBarContainer.innerHTML = '
'; + } + } + + // Legend + if (el.analystShareLegend) { + if (report.avatars && report.avatars.length > 0) { + el.analystShareLegend.innerHTML = report.avatars.map((a, idx) => { + const color = colors[idx % colors.length]; + const info = getAvatarInfo(a.avatar_id); + const pct = a.damage_percent || (teamDmg > 0 ? (a.total_damage / teamDmg) * 100 : 0); + return ` +
+
+ + ${info.name} + ${pct.toFixed(1)}% + (${formatNumber(a.total_damage)}) +
+ `; + }).join(''); + } else { + el.analystShareLegend.innerHTML = ''; + } + } + + // Characters Detailed Cards Grid + if (el.analystCharactersContainer) { + if (!report.avatars || report.avatars.length === 0) { + el.analystCharactersContainer.innerHTML = ` +
+ ${t('analystNoBattle')} +
+ `; + } else { + el.analystCharactersContainer.innerHTML = report.avatars.map((a, idx) => { + const color = colors[idx % colors.length]; + const info = getAvatarInfo(a.avatar_id); + const pct = a.damage_percent || (teamDmg > 0 ? (a.total_damage / teamDmg) * 100 : 0); + + let maxHit = 0; + if (a.attack_type_max_damage) { + for (const key in a.attack_type_max_damage) { + if (a.attack_type_max_damage[key] > maxHit) maxHit = a.attack_type_max_damage[key]; + } + } + + const dNormal = a.attack_type_damage?.Normal || 0; + const dSkill = a.attack_type_damage?.BPSkill || a.attack_type_damage?.Skill || 0; + const dUlt = a.attack_type_damage?.Ultra || a.attack_type_damage?.Ultimate || 0; + const dBreak = a.total_break_damage || a.attack_type_damage?.BreakDamage || 0; + const dFollowup = (a.attack_type_damage?.Insert || 0) + (a.attack_type_damage?.Pursued || 0); + const dDot = a.attack_type_damage?.DOT || 0; + + return ` +
+
+ +
+
+
+ + ${info.element ? ` +
+ ${getElementIconHtml(info.element, 14)} +
+ ` : ''} +
+
+
+ ${info.name} + Lv.${a.level || 80} E${a.rank !== undefined ? a.rank : 0} +
+
+ ${translatePath(info.path)} • ${translateElement(info.element)} +
+
+
+ +
+
${pct.toFixed(1)}%
+
DPS SHARE
+
+
+ +
+
+
${t('analystTotalDamage')}
+
${formatNumber(a.total_damage)}
+
+
+
${t('analystMaxHit')}
+
${formatNumber(maxHit)}
+
+
+ +
+
+
+ +
+ ${t('analystBreakdownTitle')} +
+
+
+ ${t('analystDmgNormal')} + ${formatNumber(dNormal)} +
+
+ ${t('analystDmgSkill')} + ${formatNumber(dSkill)} +
+
+ ${t('analystDmgUlt')} + ${formatNumber(dUlt)} +
+
+ ${t('analystDmgBreak')} + ${formatNumber(dBreak)} +
+ ${dFollowup > 0 ? ` +
+ ${t('analystDmgFollowup')} + ${formatNumber(dFollowup)} +
+ ` : ''} + ${dDot > 0 ? ` +
+ ${t('analystDmgDot')} + ${formatNumber(dDot)} +
+ ` : ''} +
+ +
+
⏱️ เทิร์น: ${a.total_turns || 0} ${a.extra_turns ? `(+${a.extra_turns})` : ''}
+
🛡️ โล่: ${formatNumber(a.total_shield)}
+
💚 ฮีล: ${formatNumber(a.total_heal)}
+
💔 โดนตี: ${formatNumber(a.total_damage_taken)}
+
+
+ `; + }).join(''); + } + } + + // Enemies Section + if (el.analystEnemiesContainer && el.analystEnemiesCard) { + if (report.monsters && report.monsters.length > 0) { + el.analystEnemiesCard.style.display = 'block'; + el.analystEnemiesContainer.innerHTML = report.monsters.map(m => ` +
+
+ 👾 +
+
Monster #${m.monster_id || m.entity_id}
+
Wave: ${m.wave || 1} • Entity: ${m.entity_id}
+
+
+
+
${formatNumber(m.total_damage_taken)} DMG Taken
+
${m.total_turns || 0} Turns (${formatNumber(m.total_damage)} Dealt)
+
+
+ `).join(''); + } else { + el.analystEnemiesCard.style.display = 'none'; + } + } +} + +function updateDashboardBattleBanner() { + if (!el.dashboardBattleTitle || !el.dashboardBattleSummary) return; + const report = analystState.currentReport; + if (!report || !report.team_total_damage) { + el.dashboardBattleTitle.textContent = "Damage Analyst (จับดาเมจจากในเกม)"; + el.dashboardBattleSummary.textContent = "ยังไม่มีข้อมูลการต่อสู้ล่าสุด — กดเพื่อเปิดหน้า Damage Analyst"; + return; + } + const topAvatar = (report.avatars && report.avatars.length > 0) + ? [...report.avatars].sort((a, b) => b.total_damage - a.total_damage)[0] + : null; + const topName = topAvatar ? getAvatarInfo(topAvatar.avatar_id).name : ''; + const topDmgPct = topAvatar ? `${topAvatar.damage_percent.toFixed(0)}%` : ''; + + el.dashboardBattleTitle.textContent = `💥 ดาเมจรวม ${formatNumber(report.team_total_damage)} (${report.round_cnt} รอบ)`; + el.dashboardBattleSummary.textContent = topName + ? `MVP: ${topName} (${topDmgPct} Share) • ${report.total_battle_turns} เทิร์น • เวลา ${formatDuration(report.cost_time)}` + : `สถานะ: ${report.end_status || 'WIN'} • ${report.total_battle_turns} เทิร์น`; +} + +function exportBattleReportJson() { + if (!analystState.currentReport) { + showToast('ไม่มีข้อมูลการต่อสู้ที่จะส่งออก', 'error'); + return; + } + const text = JSON.stringify(analystState.currentReport, null, 2); + const blob = new Blob([text], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `battle-report-${analystState.currentReport.battle_id || Date.now()}.json`; + a.click(); + showToast(`ส่งออก ${a.download} เรียบร้อย`, 'success'); +} + +async function clearBattleReport() { + analystState.currentReport = null; + analystState.lastBattleTimestamp = 0; + try { + await apiRequest('/api/battle_report/reset', 'POST'); + } catch (_) {} + renderEmptyDamageAnalyst(); + updateDashboardBattleBanner(); + showToast(t('analystToastCleared'), 'info'); +} + +function loadDemoBattleReport() { + const demoReport = { + timestamp: Date.now(), + battle_id: 88421, + stage_id: 3001, + end_status: "WIN", + total_battle_turns: 11, + total_auto_turns: 0, + round_cnt: 0, + cost_time: 46.8, + challenge_score: 4000, + team_total_damage: 2840000, + team_total_heal: 8400, + team_total_shield: 0, + avatars: [ + { + avatar_id: 1310, + level: 80, + rank: 2, + total_damage: 2150000, + damage_percent: 75.7, + total_damage_taken: 3200, + total_heal: 0, + total_shield: 0, + total_break_damage: 1890000, + total_turns: 5, + extra_turns: 2, + attack_type_damage: { + Normal: 45000, + BPSkill: 215000, + BreakDamage: 1890000 + }, + attack_type_max_damage: { + BreakDamage: 620000 + }, + skill_times: [ + { skill_id: 131001, skill_type: "Normal", skill_use_count: 1 }, + { skill_id: 131002, skill_type: "BPSkill", skill_use_count: 4 } + ] + }, + { + avatar_id: 8005, + level: 80, + rank: 6, + total_damage: 410000, + damage_percent: 14.4, + total_damage_taken: 1800, + total_heal: 0, + total_shield: 0, + total_break_damage: 380000, + total_turns: 3, + extra_turns: 0, + attack_type_damage: { + BPSkill: 120000, + BreakDamage: 290000 + }, + attack_type_max_damage: { + BreakDamage: 195000 + } + }, + { + avatar_id: 1303, + level: 80, + rank: 1, + total_damage: 160000, + damage_percent: 5.6, + total_damage_taken: 1400, + total_heal: 0, + total_shield: 0, + total_break_damage: 160000, + total_turns: 2, + extra_turns: 0, + attack_type_damage: { + Normal: 20000, + BreakDamage: 140000 + }, + attack_type_max_damage: { + BreakDamage: 95000 + } + }, + { + avatar_id: 1301, + level: 80, + rank: 6, + total_damage: 120000, + damage_percent: 4.3, + total_damage_taken: 2100, + total_heal: 8400, + total_shield: 0, + total_break_damage: 95000, + total_turns: 3, + extra_turns: 1, + attack_type_damage: { + Normal: 25000, + Ultra: 35000, + BreakDamage: 60000 + }, + attack_type_max_damage: { + BreakDamage: 60000 + } + } + ], + monsters: [ + { + entity_id: 101, + monster_id: 3024010, + total_turns: 4, + total_damage: 8500, + total_damage_taken: 2840000, + wave: 1 + } + ] + }; + + analystState.currentReport = demoReport; + analystState.lastBattleTimestamp = demoReport.timestamp; + renderDamageAnalyst(); + updateDashboardBattleBanner(); + showToast("✨ โหลดตัวอย่างการต่อสู้ 0-Cycle เรียบร้อยแล้ว", "success"); +} + +function setupDamageAnalystEvents() { + if (el.btnAnalystFetch) { + el.btnAnalystFetch.addEventListener('click', () => fetchLatestBattleReport(false)); + } + + if (el.btnAnalystClear) { + el.btnAnalystClear.addEventListener('click', clearBattleReport); + } + + if (el.btnAnalystExport) { + el.btnAnalystExport.addEventListener('click', exportBattleReportJson); + } + + if (el.btnAnalystImport && el.inputAnalystFile) { + el.btnAnalystImport.addEventListener('click', () => { + el.inputAnalystFile.click(); + }); + + el.inputAnalystFile.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const parsed = JSON.parse(event.target.result); + let report = null; + if (parsed.avatars || parsed.team_total_damage !== undefined) { + report = parsed; + } else { + report = parseFireflySranalysisJson(parsed); + } + + if (report && (report.avatars || report.team_total_damage !== undefined)) { + analystState.currentReport = report; + analystState.lastBattleTimestamp = report.timestamp || Date.now(); + renderDamageAnalyst(); + updateDashboardBattleBanner(); + showToast(t('analystToastImported'), 'success'); + + try { + await apiRequest('/api/battle_report', 'POST', report); + } catch (_) {} + } else { + showToast('รูปแบบไฟล์ JSON ไม่ถูกต้อง (ต้องการ GaC Battle Report หรือ Firefly-Sranalysis JSON)', 'error'); + } + } catch (err) { + showToast(`ไฟล์ JSON ผิดพลาด: ${err.message}`, 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }); + } + + if (el.synergyDamageBanner) { + el.synergyDamageBanner.addEventListener('click', () => { + const btn = document.querySelector('.tab-btn[data-tab="tab-damage-analyst"]'); + if (btn) btn.click(); + }); + } + + if (!analystState.autoSyncInterval) { + analystState.autoSyncInterval = setInterval(() => { + if (el.chkAnalystAutosync && el.chkAnalystAutosync.checked && state.isConnected) { + fetchLatestBattleReport(true); + } + }, 3000); + } +} + +window.fetchLatestBattleReport = fetchLatestBattleReport; +window.loadDemoBattleReport = loadDemoBattleReport; +window.exportBattleReportJson = exportBattleReportJson; +window.clearBattleReport = clearBattleReport; + // Background Ping Loop function startPingLoop() { setInterval(() => { @@ -7409,14 +8065,17 @@ async function init() { setupTabs(); setupModalSubtabs(); setupEvents(); + setupDamageAnalystEvents(); setupGachabaseSyncEvents(); setupGMConsoleEvents(); applyTranslations(); renderLineup(state.statusData?.lineups || DEFAULT_LINEUP); renderAvatarsGrid(); loadEquipment(); + renderEmptyDamageAnalyst(); updateGachabaseSyncUI(); refreshStatus(); + fetchLatestBattleReport(true); startPingLoop(); updateCacheUsageDisplay(); } diff --git a/js/i18n.js b/js/i18n.js index 6aca15d..d89f339 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -427,9 +427,83 @@ export const TRANSLATIONS = { "lcPickerSigTitle": "⭐ กรวยแสงประจำตัวแนะนำ (Signature)", "lcPickerEquipSig": "⚡ สวมใส่ชิ้นนี้", "lcPickerUnequip": "✕ ถอดกรวยแสงออก", - "lblPingInterval": "ความถี่ในการตรวจสอบเซิร์ฟเวอร์ (Ping Interval)" + "lblPingInterval": "ความถี่ในการตรวจสอบเซิร์ฟเวอร์ (Ping Interval)", + "tabDamageAnalyst": "💥 Damage Analyst", + "analystTitle": "💥 วิเคราะห์ความเสียหายจากการต่อสู้ (Damage Analyst)", + "analystSubtitle": "ดักจับและวิเคราะห์ดาเมจจากการต่อสู้จริงในเกม Star Rail (ระบบเดียวกับ FireflyGO / Veritas)", + "analystFetchBtn": "🔄 ดึงดาเมจล่าสุดจากในเกม", + "analystAutoSync": "⚡ ซิงค์อัตโนมัติเมื่อจบการต่อสู้", + "analystImportBtn": "📂 นำเข้าไฟล์ JSON", + "analystExportBtn": "💾 ส่งออก JSON", + "analystClearBtn": "🗑️ ล้างข้อมูล", + "analystNoBattle": "ยังไม่มีข้อมูลการต่อสู้ — กรุณาเริ่มการต่อสู้ในเกม หรือกด 'ดึงดาเมจล่าสุด' / 'นำเข้าไฟล์ JSON'", + "analystTotalDamage": "ดาเมจรวมทั้งทีม", + "analystCycles": "จำนวนรอบ (Cycles)", + "analystTurns": "เทิร์นทั้งหมด", + "analystDuration": "ระยะเวลาการต่อสู้", + "analystHealing": "การฟื้นฟู HP รวม", + "analystShield": "โล่ป้องกันรวม", + "analystDpsShare": "สัดส่วนดาเมจ (DPS Share)", + "analystBreakdownTitle": "รายละเอียดดาเมจจำแนกตามประเภท", + "analystDmgNormal": "🗡️ โจมตีปกติ", + "analystDmgSkill": "⚡ สกิลต่อสู้", + "analystDmgUlt": "💥 ท่าไม้ตาย", + "analystDmgBreak": "🔨 Break / Super Break", + "analystDmgFollowup": "🔄 โจมตีต่อเนื่อง", + "analystDmgDot": "🩸 ดาเมจต่อเนื่อง (DoT)", + "analystMaxHit": "ดาเมจสูงสุดใน 1 ครั้ง", + "analystDamageTaken": "ดาเมจที่ได้รับ", + "analystTurnsTaken": "เทิร์นแอ็กชัน", + "analystSkillUses": "จำนวนครั้งที่ใช้สกิล", + "analystEnemyTitle": "👾 รายชื่อศัตรูที่ถูกโจมตี (Targets / Enemies)", + "analystEnemyDmgTaken": "ดาเมจที่ศัตรูได้รับ", + "analystEnemyTurns": "เทิร์นของศัตรู", + "analystStatusWin": "🏆 ชนะการต่อสู้", + "analystStatusLose": "💀 พ่ายแพ้", + "analystStatusQuit": "🚪 ยอมแพ้ / ออกจากการต่อสู้", + "analystToastFetched": "✅ ดึงข้อมูลการต่อสู้ล่าสุดจากเซิร์ฟเวอร์เรียบร้อย", + "analystToastImported": "✅ นำเข้าข้อมูลการต่อสู้เรียบร้อย", + "analystToastCleared": "🗑️ ล้างข้อมูลการต่อสู้เรียบร้อย", + "analystDashboardBanner": "💥 ข้อมูลการต่อสู้ล่าสุด: ดาเมจรวม {dmg} ({cycles} รอบ) — คลิกเพื่อดูบทวิเคราะห์เต็ม →" }, "en": { + "tabDamageAnalyst": "💥 Damage Analyst", + "analystTitle": "💥 In-Game Combat Damage Analyst", + "analystSubtitle": "Capture & analyze real combat statistics from Star Rail (FireflyGO / Veritas architecture)", + "analystFetchBtn": "🔄 Fetch In-Game Battle", + "analystAutoSync": "⚡ Auto-sync on battle finish", + "analystImportBtn": "📂 Import Battle JSON", + "analystExportBtn": "💾 Export JSON", + "analystClearBtn": "🗑️ Clear Report", + "analystNoBattle": "No combat data recorded yet — Please play a battle in-game or click 'Fetch In-Game Battle' / 'Import Battle JSON'", + "analystTotalDamage": "Team Total Damage", + "analystCycles": "Cycles Elapsed", + "analystTurns": "Total Turns", + "analystDuration": "Combat Duration", + "analystHealing": "Total Healing", + "analystShield": "Total Shield", + "analystDpsShare": "Damage Share (DPS %)", + "analystBreakdownTitle": "Damage Breakdown by Category", + "analystDmgNormal": "🗡️ Basic ATK", + "analystDmgSkill": "⚡ Skill", + "analystDmgUlt": "💥 Ultimate", + "analystDmgBreak": "🔨 Break / Super Break", + "analystDmgFollowup": "🔄 Follow-up", + "analystDmgDot": "🩸 DoT", + "analystMaxHit": "Max Single Hit", + "analystDamageTaken": "Damage Taken", + "analystTurnsTaken": "Action Turns", + "analystSkillUses": "Ability Usages", + "analystEnemyTitle": "👾 Enemy & Target Analysis", + "analystEnemyDmgTaken": "Damage Received", + "analystEnemyTurns": "Enemy Turns", + "analystStatusWin": "🏆 Victory", + "analystStatusLose": "💀 Defeat", + "analystStatusQuit": "🚪 Retreated / Quitted", + "analystToastFetched": "✅ Latest battle statistics fetched successfully", + "analystToastImported": "✅ Battle data imported successfully", + "analystToastCleared": "🗑️ Battle report cleared", + "analystDashboardBanner": "💥 Latest Battle: Total {dmg} ({cycles} Cycles) — View Full Analysis →", "ahaGlobalDesc": "Enable server-wide Aeon ★ Aha special trait", "btnE6S5": "⭐ E6S5", "btnE0S1": "⚡ E0S1",