feat: implement in-game Damage Analyst & remove role distribution

This commit is contained in:
CAtlas0784 committed 2026-10-17 13:07:32 +07:00
1 parent f8c39142eb
commit b5c4dbec9e
3 files changed
+886 -58

No files matched your search

+110 -15
View File
@@ -59,6 +59,7 @@
<button class="tab-btn active" data-tab="tab-dashboard" data-i18n="tabDashboard">📊 แดชบอร์ด & จัดทีม</button>
<button class="tab-btn" data-tab="tab-avatars" data-i18n="tabAvatars">👥 ตัวละคร</button>
<button class="tab-btn" data-tab="tab-equipment" data-i18n="tabEquipment">🗡️ กรวยแสง & รีลิกส์</button>
<button class="tab-btn" data-tab="tab-damage-analyst" id="tab-btn-damage-analyst" data-i18n="tabDamageAnalyst">💥 Damage Analyst</button>
<button class="tab-btn" data-tab="tab-persistent" id="tab-btn-persistent" data-i18n="tabPersistent">💾 จัดการเซฟ GaC</button>
<button class="tab-btn" data-tab="tab-exrank" id="tab-btn-exrank" data-i18n="tabExrank">⚡ สกิลพิเศษ Ex-Rank</button>
<button class="tab-btn" data-tab="tab-json" data-i18n="tabJson">📝 จัดการไฟล์ JSON</button>
@@ -157,7 +158,7 @@
</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>
<p class="card-subtitle" style="margin-bottom: 0.85rem;" data-i18n="synergySubtitle">วิเคราะห์การครอบคลุม Weakness Break, ความสมดุลของบทบาท และความพร้อมของทีม</p>
<p class="card-subtitle" style="margin-bottom: 0.85rem;" data-i18n="synergySubtitle">วิเคราะห์การครอบคลุม Weakness Break และความพร้อมของอุปกรณ์ในทีม</p>
<!-- A. Weakness Break Coverage 7 Elements -->
<div style="margin-bottom: 1rem;">
@@ -169,20 +170,7 @@
</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;" data-i18n="synergyRoleBalance">
🛡️ การกระจายบทบาทในทีม (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 -->
<!-- B. Equipment Readiness & Stats Overview -->
<div>
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.4rem;" data-i18n="synergyEquipmentTitle">
📊 ความพร้อมของอุปกรณ์และเลเวลเฉลี่ย
@@ -206,6 +194,18 @@
</div>
</div>
</div>
<!-- C. Quick Damage Analyst Banner -->
<div id="synergy-damage-banner" style="margin-top: 1rem; padding: 0.75rem 1rem; border-radius: 8px; background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(245, 158, 11, 0.08) 100%); border: 1px solid rgba(239, 68, 68, 0.2); display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.2s ease;" title="คลิกเพื่อเปิดหน้า Damage Analyst">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span style="font-size: 1.4rem;">💥</span>
<div>
<div style="font-size: 0.85rem; font-weight: 700; color: #f87171;" id="dashboard-battle-title">Damage Analyst (จับดาเมจจากในเกม)</div>
<div style="font-size: 0.75rem; color: var(--text-muted);" id="dashboard-battle-summary">ยังไม่มีข้อมูลการต่อสู้ล่าสุด</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.6rem; pointer-events: none;">ดูบทวิเคราะห์ →</button>
</div>
</div>
</div>
@@ -387,6 +387,101 @@
</div>
</section>
<!-- 3.5. Damage Analyst (In-Game Battle Telemetry & Damage Analysis) -->
<section id="tab-damage-analyst" class="tab-content">
<!-- Toolbar & Control Card -->
<div class="card" style="margin-bottom: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div>
<div class="card-title" style="margin-bottom: 0.25rem;">
<span data-i18n="analystTitle">💥 วิเคราะห์ความเสียหายจากการต่อสู้ (Damage Analyst)</span>
<span class="badge-super-tag" id="badge-analyst-status" style="background: rgba(34, 197, 94, 0.15); color: #4ade80;">พร้อมรับข้อมูลจากเกม</span>
</div>
<p class="card-subtitle" style="margin-bottom: 0;" data-i18n="analystSubtitle">
ดักจับและวิเคราะห์ดาเมจจากการต่อสู้จริงในเกม Star Rail (ระบบเดียวกับ FireflyGO / Veritas)
</p>
</div>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--text-muted); cursor: pointer; user-select: none; margin-right: 0.5rem;">
<input type="checkbox" id="chk-analyst-autosync" checked style="accent-color: var(--accent-cyan); cursor: pointer;">
<span data-i18n="analystAutoSync">ซิงค์อัตโนมัติ</span>
</label>
<button type="button" class="btn btn-primary" id="btn-analyst-fetch" style="background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);">
<span data-i18n="analystFetchBtn">🔄 ดึงดาเมจล่าสุดจากในเกม</span>
</button>
<input type="file" id="input-analyst-file" accept=".json" style="display: none;">
<button type="button" class="btn btn-secondary" id="btn-analyst-import" data-i18n="analystImportBtn">
📂 นำเข้า JSON
</button>
<button type="button" class="btn btn-secondary" id="btn-analyst-export" data-i18n="analystExportBtn">
💾 ส่งออก JSON
</button>
<button type="button" class="btn btn-secondary" id="btn-analyst-clear" style="color: #f87171;" data-i18n="analystClearBtn">
🗑️ ล้างข้อมูล
</button>
</div>
</div>
</div>
<!-- Overview 4 Metrics Cards -->
<div class="grid-4" style="margin-bottom: 1.25rem;">
<div class="card" style="padding: 1.2rem; border-left: 4px solid #ef4444; background: linear-gradient(135deg, rgba(239,68,68,0.06) 0%, rgba(20,20,28,0.7) 100%);">
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;" data-i18n="analystTotalDamage">ดาเมจรวมทั้งทีม</div>
<div style="font-size: 1.85rem; font-weight: 900; color: #f87171; letter-spacing: -0.5px; margin: 0.25rem 0;" id="analyst-team-damage">0</div>
<div style="font-size: 0.75rem; color: var(--text-muted);" id="analyst-damage-av">เฉลี่ยต่อรอบ: -</div>
</div>
<div class="card" style="padding: 1.2rem; border-left: 4px solid #f59e0b; background: linear-gradient(135deg, rgba(245,158,11,0.06) 0%, rgba(20,20,28,0.7) 100%);">
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;" data-i18n="analystCycles">จำนวนรอบ (Cycles)</div>
<div style="font-size: 1.85rem; font-weight: 900; color: #fbbf24; letter-spacing: -0.5px; margin: 0.25rem 0;" id="analyst-cycles">0 รอบ</div>
<div style="font-size: 0.75rem; color: var(--text-muted);" id="analyst-cycles-note">MoC / PF / AS Standard</div>
</div>
<div class="card" style="padding: 1.2rem; border-left: 4px solid #38bdf8; background: linear-gradient(135deg, rgba(56,189,248,0.06) 0%, rgba(20,20,28,0.7) 100%);">
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;" data-i18n="analystTurns">เทิร์นทั้งหมด</div>
<div style="font-size: 1.85rem; font-weight: 900; color: #38bdf8; letter-spacing: -0.5px; margin: 0.25rem 0;" id="analyst-total-turns">0 เทิร์น</div>
<div style="font-size: 0.75rem; color: var(--text-muted);" id="analyst-auto-turns">เทิร์นอัตโนมัติ: 0</div>
</div>
<div class="card" style="padding: 1.2rem; border-left: 4px solid #34d399; background: linear-gradient(135deg, rgba(52,211,153,0.06) 0%, rgba(20,20,28,0.7) 100%);">
<div style="font-size: 0.775rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;" data-i18n="analystDuration">ระยะเวลาการต่อสู้</div>
<div style="font-size: 1.85rem; font-weight: 900; color: #34d399; letter-spacing: -0.5px; margin: 0.25rem 0;" id="analyst-duration">00:00</div>
<div style="font-size: 0.75rem; color: var(--text-muted);" id="analyst-battle-meta">Stage: - | Battle: -</div>
</div>
</div>
<!-- Team Damage Share Bar -->
<div class="card" style="margin-bottom: 1.25rem;">
<div class="card-title" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<span data-i18n="analystDpsShare">สัดส่วนดาเมจของทีม (Team DPS Share)</span>
<span style="font-size: 0.8rem; color: var(--text-muted);" id="analyst-total-share-label">รวม 100%</span>
</div>
<!-- Progress Bar Container -->
<div id="analyst-share-bar-container" style="height: 18px; width: 100%; border-radius: 9px; background: rgba(255,255,255,0.05); overflow: hidden; display: flex; margin-bottom: 0.85rem; border: 1px solid rgba(255,255,255,0.1);">
<!-- Dynamic segments filled by JS -->
</div>
<!-- Legend Container -->
<div id="analyst-share-legend" style="display: flex; gap: 1.5rem; flex-wrap: wrap;">
<!-- Dynamic legend filled by JS -->
</div>
</div>
<!-- Character Breakdown Grid -->
<div id="analyst-characters-container" class="grid-2" style="gap: 1.25rem; margin-bottom: 1.5rem;">
<!-- Populated dynamically by JS for each avatar in battle -->
</div>
<!-- Target Enemies Encountered -->
<div class="card" id="analyst-enemies-card" style="margin-bottom: 1.5rem;">
<div class="card-title" style="margin-bottom: 0.75rem;">
<span data-i18n="analystEnemyTitle">👾 รายชื่อศัตรูที่ถูกโจมตี (Targets / Enemies)</span>
</div>
<div id="analyst-enemies-container" style="display: flex; flex-direction: column; gap: 0.6rem;">
<!-- Populated by JS -->
</div>
</div>
</section>
<!-- 4. Persistent Save Settings (Exclusive to GaC-hkrpg-go) -->
<section id="tab-persistent" class="tab-content">
<div class="card" style="margin-bottom: 1.5rem;">
+701 -42
View File
@@ -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 = `
<div class="badge-super-tag" style="background: rgba(239, 68, 68, 0.15); color: #f87171; font-weight: 700;">
${t('roleDps')} (${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;">
${t('roleSupport')} (${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;">
${t('roleSustain')} (${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;">
${t('roleSpecialist')} (${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);">${t('adviceEmpty')}</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(255,255,255,0.08)';
} else if (roles.sustain.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: #fbbf24;">${t('adviceNoSustain')}</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)';
} else if (roles.dps.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: #38bdf8;">${t('adviceNoDps')}</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(56, 189, 248, 0.4)';
} else if (roles.support.length === 0) {
el.synergyAdviceBox.innerHTML = `<span style="color: #fbbf24;">${t('adviceNoSupport')}</span>`;
el.synergyAdviceBox.style.borderColor = 'rgba(245, 158, 11, 0.4)';
} else {
el.synergyAdviceBox.innerHTML = `<span style="color: #34d399;">${t('adviceBalanced')}</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}` : '-';
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 = '<div style="width: 100%; background: rgba(255,255,255,0.05); height: 100%;"></div>';
}
if (el.analystShareLegend) {
el.analystShareLegend.innerHTML = '';
}
if (el.analystCharactersContainer) {
el.analystCharactersContainer.innerHTML = `
<div style="grid-column: 1/-1; text-align: center; padding: 3rem 1rem; color: var(--text-muted);">
<div style="font-size: 3rem; margin-bottom: 0.75rem;">⚔️</div>
<div style="font-size: 1.1rem; font-weight: 700; color: var(--text-main); margin-bottom: 0.5rem;">${t('analystNoBattle')}</div>
<div style="display: flex; gap: 0.5rem; justify-content: center; margin-top: 1rem; flex-wrap: wrap;">
<button type="button" class="btn btn-primary" id="btn-empty-analyst-fetch" style="background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);">${t('analystFetchBtn')}</button>
<button type="button" class="btn btn-secondary" id="btn-empty-analyst-demo">✨ โหลดตัวอย่างการต่อสู้ (Demo)</button>
</div>
</div>
`;
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 `<div style="width: ${pct}%; background: ${color}; height: 100%; transition: width 0.4s ease;" title="${info.name}: ${pct.toFixed(1)}% (${formatNumber(a.total_damage)})"></div>`;
}).join('');
} else {
el.analystShareBarContainer.innerHTML = '<div style="width: 100%; background: rgba(255,255,255,0.05); height: 100%;"></div>';
}
}
// 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 `
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.825rem;">
<div style="width: 10px; height: 10px; border-radius: 50%; background: ${color}; box-shadow: 0 0 6px ${color}88;"></div>
<img src="${getAvatarIconSrc(a.avatar_id)}" onerror="handleAvatarImgError(this, ${a.avatar_id})" style="width: 22px; height: 22px; border-radius: 50%; object-fit: cover; border: 1px solid ${color};">
<span style="font-weight: 700; color: var(--text-highlight);">${info.name}</span>
<span style="font-weight: 800; color: ${color};">${pct.toFixed(1)}%</span>
<span style="color: var(--text-muted); font-size: 0.75rem;">(${formatNumber(a.total_damage)})</span>
</div>
`;
}).join('');
} else {
el.analystShareLegend.innerHTML = '';
}
}
// Characters Detailed Cards Grid
if (el.analystCharactersContainer) {
if (!report.avatars || report.avatars.length === 0) {
el.analystCharactersContainer.innerHTML = `
<div style="grid-column: 1/-1; text-align: center; padding: 2.5rem; color: var(--text-muted);">
${t('analystNoBattle')}
</div>
`;
} 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 `
<div class="card" style="padding: 1.25rem; border: 1px solid rgba(255,255,255,0.08); background: linear-gradient(135deg, rgba(255,255,255,0.02) 0%, rgba(20,20,30,0.8) 100%); position: relative; overflow: hidden;">
<div style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: ${color};"></div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="position: relative; width: 48px; height: 48px; flex-shrink: 0;">
<img src="${getAvatarIconSrc(a.avatar_id)}"
onerror="handleAvatarImgError(this, ${a.avatar_id})"
style="width: 48px; height: 48px; border-radius: 50%; object-fit: cover; border: 2px solid ${color};">
${info.element ? `
<div style="position: absolute; bottom: -2px; right: -2px; width: 18px; height: 18px; border-radius: 50%; background: #0f172a; display: flex; align-items: center; justify-content: center; border: 1px solid rgba(255,255,255,0.2);">
${getElementIconHtml(info.element, 14)}
</div>
` : ''}
</div>
<div>
<div style="font-weight: 800; font-size: 1.05rem; color: var(--text-highlight); display: flex; align-items: center; gap: 0.45rem;">
<span>${info.name}</span>
<span style="font-size: 0.75rem; padding: 0.1rem 0.4rem; border-radius: 4px; background: rgba(255,255,255,0.08); color: var(--text-muted); font-weight: 700;">Lv.${a.level || 80} E${a.rank !== undefined ? a.rank : 0}</span>
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.15rem;">
${translatePath(info.path)} • ${translateElement(info.element)}
</div>
</div>
</div>
<div style="text-align: right;">
<div style="font-size: 1.4rem; font-weight: 900; color: ${color}; letter-spacing: -0.5px;">${pct.toFixed(1)}%</div>
<div style="font-size: 0.7rem; color: var(--text-muted); font-weight: 700;">DPS SHARE</div>
</div>
</div>
<div style="padding: 0.75rem 1rem; border-radius: 8px; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.05); margin-bottom: 0.85rem; display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.725rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;">${t('analystTotalDamage')}</div>
<div style="font-size: 1.5rem; font-weight: 900; color: #fff;">${formatNumber(a.total_damage)}</div>
</div>
<div style="text-align: right;">
<div style="font-size: 0.725rem; color: var(--text-muted);">${t('analystMaxHit')}</div>
<div style="font-size: 0.95rem; font-weight: 800; color: #f87171;">${formatNumber(maxHit)}</div>
</div>
</div>
<div style="height: 6px; border-radius: 3px; background: rgba(255,255,255,0.06); overflow: hidden; margin-bottom: 1rem;">
<div style="height: 100%; width: ${pct}%; background: ${color}; border-radius: 3px;"></div>
</div>
<div style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); margin-bottom: 0.4rem;">
${t('analystBreakdownTitle')}
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; margin-bottom: 1rem;">
<div style="padding: 0.4rem 0.6rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); display: flex; justify-content: space-between; font-size: 0.775rem;">
<span style="color: var(--text-muted);">${t('analystDmgNormal')}</span>
<span style="font-weight: 700; color: var(--text-main);">${formatNumber(dNormal)}</span>
</div>
<div style="padding: 0.4rem 0.6rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); display: flex; justify-content: space-between; font-size: 0.775rem;">
<span style="color: var(--text-muted);">${t('analystDmgSkill')}</span>
<span style="font-weight: 700; color: #38bdf8;">${formatNumber(dSkill)}</span>
</div>
<div style="padding: 0.4rem 0.6rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); display: flex; justify-content: space-between; font-size: 0.775rem;">
<span style="color: var(--text-muted);">${t('analystDmgUlt')}</span>
<span style="font-weight: 700; color: #c084fc;">${formatNumber(dUlt)}</span>
</div>
<div style="padding: 0.4rem 0.6rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); display: flex; justify-content: space-between; font-size: 0.775rem;">
<span style="color: var(--text-muted);">${t('analystDmgBreak')}</span>
<span style="font-weight: 700; color: #fbbf24;">${formatNumber(dBreak)}</span>
</div>
${dFollowup > 0 ? `
<div style="padding: 0.4rem 0.6rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); display: flex; justify-content: space-between; font-size: 0.775rem;">
<span style="color: var(--text-muted);">${t('analystDmgFollowup')}</span>
<span style="font-weight: 700; color: #f472b6;">${formatNumber(dFollowup)}</span>
</div>
` : ''}
${dDot > 0 ? `
<div style="padding: 0.4rem 0.6rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.04); display: flex; justify-content: space-between; font-size: 0.775rem;">
<span style="color: var(--text-muted);">${t('analystDmgDot')}</span>
<span style="font-weight: 700; color: #a78bfa;">${formatNumber(dDot)}</span>
</div>
` : ''}
</div>
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap; padding-top: 0.65rem; border-top: 1px solid rgba(255,255,255,0.06); font-size: 0.75rem; color: var(--text-muted);">
<div>⏱️ เทิร์น: <strong style="color: #fff;">${a.total_turns || 0}</strong> ${a.extra_turns ? `(+${a.extra_turns})` : ''}</div>
<div>🛡️ โล่: <strong style="color: #38bdf8;">${formatNumber(a.total_shield)}</strong></div>
<div>💚 ฮีล: <strong style="color: #34d399;">${formatNumber(a.total_heal)}</strong></div>
<div>💔 โดนตี: <strong style="color: #f87171;">${formatNumber(a.total_damage_taken)}</strong></div>
</div>
</div>
`;
}).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 => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0.85rem; border-radius: 6px; background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.05); font-size: 0.825rem;">
<div style="display: flex; align-items: center; gap: 0.6rem;">
<span style="font-size: 1.2rem;">👾</span>
<div>
<div style="font-weight: 700; color: var(--text-highlight);">Monster #${m.monster_id || m.entity_id}</div>
<div style="font-size: 0.725rem; color: var(--text-muted);">Wave: ${m.wave || 1} • Entity: ${m.entity_id}</div>
</div>
</div>
<div style="text-align: right;">
<div style="font-weight: 800; color: #f87171;">${formatNumber(m.total_damage_taken)} DMG Taken</div>
<div style="font-size: 0.725rem; color: var(--text-muted);">${m.total_turns || 0} Turns (${formatNumber(m.total_damage)} Dealt)</div>
</div>
</div>
`).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();
}
+75 -1
View File
@@ -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",