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",