fix(combat): show offense + defense damage by element in grid

The element breakdown grid previously only showed damage RECEIVED
(defense) in the Mel/Msl and Magic columns, which was mostly empty
for characters who evade/resist everything. Now shows both:

- Given M/M + Given Mag: damage dealt by element (offense)
- Recv M/M + Recv Mag: damage taken by element (defense)

This makes the element breakdown immediately useful — you can see
that you're dealing Slash damage via melee, for example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 10:13:47 +02:00
parent c03b1c19f2
commit ee30ad2636

View file

@ -4810,85 +4810,61 @@ function renderCombatStatsContent(win, charName) {
const avgNormal = normalHits > 0 ? Math.round(offAll.totalNormalDamage / normalHits) : 0; const avgNormal = normalHits > 0 ? Math.round(offAll.totalNormalDamage / normalHits) : 0;
const avgCrit = crits > 0 ? Math.round(offAll.totalCritDamage / crits) : 0; const avgCrit = crits > 0 ? Math.round(offAll.totalCritDamage / crits) : 0;
// Damage received by element // Damage by element — offense (given) and defense (received)
const defDmgMM = {}, defDmgMag = {}; const offDmgMM = {}, offDmgMag = {}, defDmgMM = {}, defDmgMag = {};
let totalDefMM = 0, totalDefMag = 0; let totalOffMM = 0, totalOffMag = 0, totalDefMM = 0, totalDefMag = 0;
DAMAGE_ELEMENTS.forEach(el => { DAMAGE_ELEMENTS.forEach(el => {
offDmgMM[el] = getDefDmg(offense, 'MeleeMissile', el);
offDmgMag[el] = getDefDmg(offense, 'Magic', el);
defDmgMM[el] = getDefDmg(defense, 'MeleeMissile', el); defDmgMM[el] = getDefDmg(defense, 'MeleeMissile', el);
defDmgMag[el] = getDefDmg(defense, 'Magic', el); defDmgMag[el] = getDefDmg(defense, 'Magic', el);
totalOffMM += offDmgMM[el];
totalOffMag += offDmgMag[el];
totalDefMM += defDmgMM[el]; totalDefMM += defDmgMM[el];
totalDefMag += defDmgMag[el]; totalDefMag += defDmgMag[el];
}); });
const totalDmgGiven = offAll.totalNormalDamage + offAll.totalCritDamage; const totalDmgGiven = offAll.totalNormalDamage + offAll.totalCritDamage;
// Build the grid HTML — matches Mag-Tools CombatTrackerGUIInfo layout // Build the grid HTML
const fmtN = n => n === 0 ? '' : n.toLocaleString(); const fmtN = n => n === 0 ? '' : n.toLocaleString();
const rightCell = (text) => `<td style="text-align:right;padding:1px 4px;font-size:0.75rem;color:#ccc;">${text}</td>`; const rightCell = (text) => `<td style="text-align:right;padding:1px 4px;font-size:0.75rem;color:#ccc;">${text}</td>`;
const labelCell = (text) => `<td style="padding:1px 4px;font-size:0.75rem;color:#aaa;">${text}</td>`; const labelCell = (text) => `<td style="padding:1px 4px;font-size:0.75rem;color:#aaa;">${text}</td>`;
const headerCell = (text) => `<td style="text-align:right;padding:1px 4px;font-size:0.72rem;color:#888;font-weight:bold;">${text}</td>`; const headerCell = (text) => `<td style="text-align:right;padding:1px 4px;font-size:0.72rem;color:#888;font-weight:bold;">${text}</td>`;
const statLabel = (text) => `<td style="padding:1px 4px;font-size:0.72rem;color:#888;font-weight:bold;">${text}</td>`; const statLabel = (text) => `<td style="padding:1px 4px;font-size:0.72rem;color:#888;font-weight:bold;">${text}</td>`;
const sepCell = `<td style="width:4px;"></td>`;
let html = '<table style="width:100%;border-collapse:collapse;">'; let html = '<table style="width:100%;border-collapse:collapse;">';
// Header
html += `<tr><td></td>${headerCell('Mel/Msl')}${headerCell('Magic')}${statLabel('Attacks')}${rightCell(totalAttacks > 0 ? `${fmtN(totalAttacks)} (${hitRate}%)` : '')}</tr>`;
const statRows = [ // Column headers: Element | Dmg Given (M/M, Mag) | Dmg Recv (M/M, Mag) | Stats
['Typeless', 'Attacks', () => totalAttacks > 0 ? `${fmtN(totalAttacks)} (${hitRate}%)` : ''], html += `<tr><td></td>${headerCell('Given M/M')}${headerCell('Given Mag')}${sepCell}${headerCell('Recv M/M')}${headerCell('Recv Mag')}${sepCell}${statLabel('Attacks')}${rightCell(totalAttacks > 0 ? `${fmtN(totalAttacks)} (${hitRate}%)` : '')}</tr>`;
['Slash', 'Evades', () => totalMeleeDefends > 0 ? `${fmtN(totalMeleeDefends)} (${evadeRate}%)` : ''],
['Pierce', 'Resists', () => totalMagicDefends > 0 ? `${fmtN(totalMagicDefends)} (${resistRate}%)` : ''], // Stats to show on the right side of each element row
['Bludgeon', 'A.Surges', () => totalAethSurges > 0 ? `${fmtN(totalAethSurges)} (${aethRate}%)` : ''], const rightStats = [
['Fire', 'C.Surges', () => totalCloakSurges > 0 ? `${fmtN(totalCloakSurges)} (${cloakRate}%)` : ''], ['Evades', () => totalMeleeDefends > 0 ? `${fmtN(totalMeleeDefends)} (${evadeRate}%)` : ''],
['Cold', '', () => ''], ['Resists', () => totalMagicDefends > 0 ? `${fmtN(totalMagicDefends)} (${resistRate}%)` : ''],
['Acid', 'Av/Mx', () => avgNormal > 0 ? `${fmtN(avgNormal)} / ${fmtN(offAll.maxNormalDamage)}` : ''], ['A.Surges', () => totalAethSurges > 0 ? `${fmtN(totalAethSurges)} (${aethRate}%)` : ''],
['Electric', 'Crits', () => crits > 0 ? `${fmtN(crits)} (${critRate}%)` : ''], ['C.Surges', () => totalCloakSurges > 0 ? `${fmtN(totalCloakSurges)} (${cloakRate}%)` : ''],
['', () => ''],
['', () => ''],
['Av/Mx', () => avgNormal > 0 ? `${fmtN(avgNormal)} / ${fmtN(offAll.maxNormalDamage)}` : ''],
['Crits', () => crits > 0 ? `${fmtN(crits)} (${critRate}%)` : ''],
]; ];
// First row already printed above as header. Now element rows:
for (let i = 0; i < DAMAGE_ELEMENTS.length; i++) { for (let i = 0; i < DAMAGE_ELEMENTS.length; i++) {
const el = DAMAGE_ELEMENTS[i]; const el = DAMAGE_ELEMENTS[i];
const sr = statRows[i]; const rs = rightStats[i] || ['', () => ''];
let statLabelText = sr ? sr[1] : ''; html += `<tr>${labelCell(el)}${rightCell(fmtN(offDmgMM[el]))}${rightCell(fmtN(offDmgMag[el]))}${sepCell}${rightCell(fmtN(defDmgMM[el]))}${rightCell(fmtN(defDmgMag[el]))}${sepCell}${rs[0] ? statLabel(rs[0]) : '<td></td>'}${rightCell(rs[1]())}</tr>`;
let statValue = sr ? sr[2]() : '';
// Skip the duplicate "Attacks" row — it's already in the header
if (i === 0) {
statLabelText = 'Evades';
statValue = totalMeleeDefends > 0 ? `${fmtN(totalMeleeDefends)} (${evadeRate}%)` : '';
} else if (i === 1) {
statLabelText = 'Resists';
statValue = totalMagicDefends > 0 ? `${fmtN(totalMagicDefends)} (${resistRate}%)` : '';
} else if (i === 2) {
statLabelText = 'A.Surges';
statValue = totalAethSurges > 0 ? `${fmtN(totalAethSurges)} (${aethRate}%)` : '';
} else if (i === 3) {
statLabelText = 'C.Surges';
statValue = totalCloakSurges > 0 ? `${fmtN(totalCloakSurges)} (${cloakRate}%)` : '';
} else if (i === 4) {
statLabelText = '';
statValue = '';
} else if (i === 5) {
statLabelText = '';
statValue = '';
} else if (i === 6) {
statLabelText = 'Av/Mx';
statValue = avgNormal > 0 ? `${fmtN(avgNormal)} / ${fmtN(offAll.maxNormalDamage)}` : '';
} else if (i === 7) {
statLabelText = 'Crits';
statValue = crits > 0 ? `${fmtN(crits)} (${critRate}%)` : '';
}
html += `<tr>${labelCell(el)}${rightCell(fmtN(defDmgMM[el]))}${rightCell(fmtN(defDmgMag[el]))}${statLabelText ? statLabel(statLabelText) : '<td></td>'}${rightCell(statValue)}</tr>`;
} }
// Crit Avg/Max row // Crit Avg/Max row
html += `<tr><td></td><td></td><td></td>${statLabel('Av/Mx')}${rightCell(avgCrit > 0 ? `${fmtN(avgCrit)} / ${fmtN(offAll.maxCritDamage)}` : '')}</tr>`; html += `<tr><td></td><td></td><td></td>${sepCell}<td></td><td></td>${sepCell}${statLabel('Av/Mx')}${rightCell(avgCrit > 0 ? `${fmtN(avgCrit)} / ${fmtN(offAll.maxCritDamage)}` : '')}</tr>`;
// Blank row // Blank row
html += '<tr><td colspan="5" style="height:6px;"></td></tr>'; html += `<tr><td colspan="9" style="height:6px;"></td></tr>`;
// Total row // Total row
html += `<tr>${labelCell('Total')}${rightCell(fmtN(totalDefMM))}${rightCell(fmtN(totalDefMag))}${statLabel('Total')}${rightCell(fmtN(totalDmgGiven))}</tr>`; html += `<tr>${labelCell('Total')}${rightCell(fmtN(totalOffMM))}${rightCell(fmtN(totalOffMag))}${sepCell}${rightCell(fmtN(totalDefMM))}${rightCell(fmtN(totalDefMag))}${sepCell}${statLabel('Total')}${rightCell(fmtN(totalDmgGiven))}</tr>`;
html += '</table>'; html += '</table>';
grid.innerHTML = html; grid.innerHTML = html;