From e03e5b1f14e4b5d9bdd6196433fa6b0327a7b8c8 Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Mon, 19 Oct 2026 19:42:42 +0700 Subject: [PATCH] fix: preserve battle slot order and display native damage categories --- README.md | 2 ++ js/app.js | 59 +++++++++++++++++++++---------------- tests/damage-order.test.cjs | 23 +++++++++++++++ 3 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 tests/damage-order.test.cjs diff --git a/README.md b/README.md index a6a2187..ffe2977 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ GaC-Srtool สื่อสารกับเซิร์ฟเวอร์ GaC-h ### บันทึกตัวละครและ Traces +Damage Analyst เรียงการ์ดตาม `report.avatars[].slot` จาก Battle จริง ไม่เปลี่ยนตามทีมหน้าโลก รายงานเก่าที่ไม่มี slot คงลำดับเดิม และแสดงประเภทดาเมจเพิ่มเติมที่ API ส่งมา เช่น Elation Damage / True Damage ทดสอบลำดับด้วย `node --test tests/damage-order.test.cjs` ข้อมูลสดต้องใช้ SDK และ `damage_analyst_live.lua` ฉบับที่อ่าน StatisticsComponent ของ Battle ปัจจุบัน + การบันทึกจากหน้ารายละเอียดส่งข้อมูลตัวละครทั้งชุดผ่าน `/api/freesr` ในคำขอเดียว รวมถึง `data.skills` และ `data.skills_by_anchor_type` สำหรับ Traces เพื่อไม่ให้ server sync ข้อมูลระดับ/Eidolon ระหว่างทางก่อนข้อมูล Trace จะถูกบันทึก `saveAvatarDetails(closeModal = true)` ยังรองรับการเรียกจากปุ่ม build ที่ไม่ต้องปิดหน้าต่าง ถ้า server ตอบผิดพลาดจะแสดงข้อความบันทึกไม่สำเร็จแทนการแจ้งว่าสำเร็จ ### ข้อมูล Build และแหล่งที่มา diff --git a/js/app.js b/js/app.js index 842baa5..1169cb6 100644 --- a/js/app.js +++ b/js/app.js @@ -1521,6 +1521,17 @@ function hasAvatarBattleStatsData(avatars) { }); } +function hasAvatarDamageBreakdownData(avatar) { + const damageTypes = avatar?.attack_type_damage; + return Number(avatar?.total_break_damage) > 0 + || (damageTypes && Object.keys(damageTypes).length > 0); +} + +function hasAvatarMaxHitData(avatar) { + const maxDamage = avatar?.attack_type_max_damage; + return Boolean(maxDamage && Object.values(maxDamage).some(value => Number(value) > 0)); +} + function getAvatarDamagePercent(avatar, teamDamage, available) { if (!available) return null; const reportedPercent = Number(avatar?.damage_percent); @@ -8368,28 +8379,16 @@ function renderEmptyDamageAnalyst() { function orderBattleReportAvatars(avatars) { if (!Array.isArray(avatars) || avatars.length < 2) return avatars || []; - const lineup = state.statusData?.lineups; - if (!lineup) return avatars; - - const lineupSlots = Array.isArray(lineup) - ? lineup.map((avatarId, slot) => [slot, avatarId]) - : Object.entries(lineup).sort(([left], [right]) => Number(left) - Number(right)); - const lineupIds = lineupSlots - .map(([, avatarId]) => Number(avatarId)) - .filter(avatarId => Number.isSafeInteger(avatarId) && avatarId > 0); - const reportById = new Map(); - - for (const avatar of avatars) { - const avatarId = Number(avatar?.avatar_id); - if (!Number.isSafeInteger(avatarId) || avatarId <= 0 || reportById.has(avatarId)) { - return avatars; - } - reportById.set(avatarId, avatar); + // Battle slots remain stable when the overworld team changes or the native + // statistics dictionary enumerates characters in a different order. + const slots = avatars.map(avatar => avatar?.slot); + if (slots.every(slot => Number.isSafeInteger(slot) && slot >= 0) + && new Set(slots).size === avatars.length) { + return [...avatars].sort((left, right) => left.slot - right.slot); } - const orderedIds = [...new Set(lineupIds)].filter(avatarId => reportById.has(avatarId)); - if (orderedIds.length !== avatars.length) return avatars; - return orderedIds.map(avatarId => reportById.get(avatarId)); + // A historical report without battle slots must keep its recorded order. + return avatars; } function renderDamageAnalyst() { @@ -8497,6 +8496,8 @@ function renderDamageAnalyst() { const color = colors[idx % colors.length]; const info = getAvatarInfo(a.avatar_id); const pct = getAvatarDamagePercent(a, teamDmg, hasAvatarDamage); + const hasDamageBreakdown = hasAvatarDamageBreakdownData(a); + const hasMaxHit = hasAvatarMaxHitData(a); let maxHit = 0; if (a.attack_type_max_damage) { @@ -8552,7 +8553,7 @@ function renderDamageAnalyst() {
${t('analystMaxHit')}
-
${hasAvatarDamage ? formatNumber(maxHit) : '—'}
+
${hasMaxHit ? formatNumber(maxHit) : '—'}
@@ -8566,19 +8567,19 @@ function renderDamageAnalyst() {
${t('analystDmgNormal')} - ${hasAvatarDamage ? formatNumber(dNormal) : '—'} + ${hasDamageBreakdown ? formatNumber(dNormal) : '—'}
${t('analystDmgSkill')} - ${hasAvatarDamage ? formatNumber(dSkill) : '—'} + ${hasDamageBreakdown ? formatNumber(dSkill) : '—'}
${t('analystDmgUlt')} - ${hasAvatarDamage ? formatNumber(dUlt) : '—'} + ${hasDamageBreakdown ? formatNumber(dUlt) : '—'}
${t('analystDmgBreak')} - ${hasAvatarDamage ? formatNumber(dBreak) : '—'} + ${hasDamageBreakdown ? formatNumber(dBreak) : '—'}
${dFollowup > 0 ? `
@@ -8592,6 +8593,14 @@ function renderDamageAnalyst() { ${formatNumber(dDot)}
` : ''} + ${Object.entries(a.attack_type_damage || {}) + .filter(([type, damage]) => Number(damage) > 0 && !['Normal', 'BPSkill', 'Skill', 'Ultra', 'Ultimate', 'BreakDamage', 'Insert', 'Pursued', 'DOT'].includes(type)) + .map(([type, damage]) => ` +
+ ${escapeHtml(type.replace(/([a-z])([A-Z])/g, '$1 $2'))} + ${formatNumber(Number(damage))} +
+ `).join('')}
diff --git a/tests/damage-order.test.cjs b/tests/damage-order.test.cjs new file mode 100644 index 0000000..338865b --- /dev/null +++ b/tests/damage-order.test.cjs @@ -0,0 +1,23 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const path = require('node:path'); +const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8'); +const start = source.indexOf('function orderBattleReportAvatars('); +const end = source.indexOf('\nfunction renderDamageAnalyst(', start); +const context = { state: { statusData: { lineups: { 0: 30, 1: 20, 2: 10 } } } }; +vm.createContext(context); +vm.runInContext(source.slice(start, end), context); + +test('battle slots override a different overworld lineup without mutating the report', () => { + const avatars = [{ avatar_id: 20, slot: 2 }, { avatar_id: 30, slot: 0 }, { avatar_id: 10, slot: 1 }]; + const result = context.orderBattleReportAvatars(avatars); + assert.deepEqual(Array.from(result, a => a.avatar_id), [30, 10, 20]); + assert.deepEqual(avatars.map(a => a.avatar_id), [20, 30, 10]); +}); + +test('historical reports keep recorded order when battle slots are absent', () => { + const result = context.orderBattleReportAvatars([{ avatar_id: 10 }, { avatar_id: 30 }, { avatar_id: 20 }]); + assert.deepEqual(Array.from(result, a => a.avatar_id), [10, 30, 20]); +});