feat: implement in-game Damage Analyst & remove role distribution
This commit is contained in:
1 parent
f8c39142eb
commit
b5c4dbec9e
3 files changed
+886
-58
No files matched your search
+110
-15
@@ -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;">
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user