Add static statusId map for all mitigation abilities, drop supplementary buff query
All MITIGATION_ABILITIES entries now carry a statusId field (FFLogs status ID = XIVAPI row_id + 1,000,000). The mitigIdMap is seeded from these static IDs as fallback for abilities absent from masterData (e.g. pre-pull shields). Removes the need for separate applybuff/removebuff API calls for name resolution. Also adds shield buffType with tooltip on absorbed value, expanded icon set, and unmitigatedAmount/mitigated passthrough. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ -56,29 +56,63 @@ function fflogs_gql(string $query): array {
|
||||
return json_decode($body, true) ?? ['_parse_error' => true];
|
||||
}
|
||||
|
||||
// ── Party-wide mitigation + boss debuffs to track ─────────────────────────
|
||||
// Barriers (dr = 0) are shown without a percentage.
|
||||
// ── Party-wide mitigation, shields + boss debuffs to track ─────────────────
|
||||
// buffType 'buff' = damage reduction, shown as icons per player
|
||||
// buffType 'shield' = barrier, shown only as tooltip on absorbed value
|
||||
// buffType 'debuff' = boss debuff, shown in event header
|
||||
const MITIGATION_ABILITIES = [
|
||||
'Passage of Arms' => ['dr' => 15, 'buffType' => 'buff'],
|
||||
'Divine Veil' => ['dr' => 0, 'buffType' => 'buff'],
|
||||
'Shake It Off' => ['dr' => 0, 'buffType' => 'buff'],
|
||||
'Dark Missionary' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Heart of Light' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Temperance' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Sacred Soil' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Expedient' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Fey Illumination' => ['dr' => 5, 'buffType' => 'buff'],
|
||||
'Collective Unconscious' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Holos' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Kerachole' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Panhaima' => ['dr' => 0, 'buffType' => 'buff'],
|
||||
'Troubadour' => ['dr' => 15, 'buffType' => 'buff'],
|
||||
'Tactician' => ['dr' => 15, 'buffType' => 'buff'],
|
||||
'Shield Samba' => ['dr' => 15, 'buffType' => 'buff'],
|
||||
'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff'],
|
||||
'Reprisal' => ['dr' => 10, 'buffType' => 'debuff'],
|
||||
'Feint' => ['dr' => 10, 'buffType' => 'debuff'],
|
||||
'Addle' => ['dr' => 10, 'buffType' => 'debuff'],
|
||||
// ── Damage reduction buffs ──────────────────────────────────────────────
|
||||
'Passage of Arms' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001175],
|
||||
'Dark Missionary' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001894],
|
||||
'Heart of Light' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001839],
|
||||
'Temperance' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001873],
|
||||
'Sacred Soil' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001944],
|
||||
'Expedient' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002711], // FFLogs: "Desperate Measures"
|
||||
'Fey Illumination' => ['dr' => 5, 'buffType' => 'buff', 'statusId' => 1000317],
|
||||
'Collective Unconscious' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1000849],
|
||||
'Holos' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1003003],
|
||||
'Kerachole' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002618],
|
||||
'Troubadour' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001934],
|
||||
'Tactician' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001951],
|
||||
'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826],
|
||||
'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707],
|
||||
// ── Shields ─────────────────────────────────────────────────────────────
|
||||
// PLD
|
||||
'Divine Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001362],
|
||||
'Guardian' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003830], // FFLogs: "Guardian's Will"
|
||||
// WAR
|
||||
'Shake It Off' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001457],
|
||||
'Bloodwhetting' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002678],
|
||||
// WHM
|
||||
'Divine Benison' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001218],
|
||||
'Divine Caress' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003903],
|
||||
// AST
|
||||
'Intersection' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001889],
|
||||
'Neutral Sect' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001921],
|
||||
'the Spire' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003892], // FFLogs: "The Spire"
|
||||
// SGE
|
||||
'Panhaima' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002613],
|
||||
'Holosakos' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003365],
|
||||
'Eukrasian Prognosis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002609],
|
||||
'Eukrasian Prognosis II' => ['dr' => 0, 'buffType' => 'shield'], // TODO
|
||||
'Eukrasian Diagnosis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002607],
|
||||
'Differential Diagnosis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002608],
|
||||
'Haima' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002612],
|
||||
// SCH
|
||||
'Galvanize' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1000297],
|
||||
'Seraphic Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001917],
|
||||
'Catalyze' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001918],
|
||||
// SMN
|
||||
'Radiant Aegis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002702],
|
||||
// PCT
|
||||
'Tempera Coat' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003686],
|
||||
'Tempera Grassa' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003687],
|
||||
// DNC
|
||||
'Improvised Finish' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002697],
|
||||
// ── Boss debuffs ────────────────────────────────────────────────────────
|
||||
'Reprisal' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001193],
|
||||
'Feint' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001195],
|
||||
'Addle' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001203],
|
||||
];
|
||||
|
||||
function resolveMitigations(string $buffStr, array $mitigIdMap): array {
|
||||
@ -127,13 +161,20 @@ foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ??
|
||||
$abilityNames[(int)$ab['gameID']] = $ab['name'];
|
||||
}
|
||||
|
||||
// gameID → mitigation meta (only for abilities present in this report)
|
||||
// gameID → mitigation meta: primary from masterData, statusId fallback for
|
||||
// abilities whose status ID isn't in masterData (add 'statusId' to the entry
|
||||
// in MITIGATION_ABILITIES when discovered via the Event Explorer).
|
||||
$mitigIdMap = [];
|
||||
foreach ($abilityNames as $gameId => $name) {
|
||||
if (isset(MITIGATION_ABILITIES[$name])) {
|
||||
$mitigIdMap[$gameId] = array_merge(['name' => $name], MITIGATION_ABILITIES[$name]);
|
||||
}
|
||||
}
|
||||
foreach (MITIGATION_ABILITIES as $name => $meta) {
|
||||
if (isset($meta['statusId']) && !isset($mitigIdMap[$meta['statusId']])) {
|
||||
$mitigIdMap[$meta['statusId']] = array_merge(['name' => $name], $meta);
|
||||
}
|
||||
}
|
||||
|
||||
// player actorID → player data
|
||||
$pdRaw = $pdResult['data']['reportData']['report']['playerDetails'] ?? null;
|
||||
@ -153,6 +194,7 @@ foreach ($roleMap as $group => $role) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── 2. Damage-taken events (paginated) ─────────────────────────────────────
|
||||
$allEvents = [];
|
||||
$nextPage = $startTime;
|
||||
@ -206,6 +248,8 @@ foreach ($allEvents as $ev) {
|
||||
'amount' => (int)($ev['amount'] ?? 0),
|
||||
'absorbed' => (int)($ev['absorbed'] ?? 0),
|
||||
'overkill' => (int)($ev['overkill'] ?? 0),
|
||||
'unmitigatedAmount' => (int)($ev['unmitigatedAmount'] ?? 0),
|
||||
'mitigated' => (int)($ev['mitigated'] ?? 0),
|
||||
'hp' => (int)($ev['targetResources']['hitPoints'] ?? 0),
|
||||
'maxHp' => (int)($ev['targetResources']['maxHitPoints'] ?? 0),
|
||||
'buffs' => $ev['buffs'] ?? '',
|
||||
@ -239,6 +283,8 @@ foreach ($byAbility as $abId => $events) {
|
||||
'id' => $tgtId,
|
||||
'amount' => 0,
|
||||
'absorbed' => 0,
|
||||
'unmitigatedAmount' => 0,
|
||||
'mitigated' => 0,
|
||||
'overkill' => 0,
|
||||
'hp' => $ev['hp'],
|
||||
'maxHp' => $ev['maxHp'],
|
||||
@ -247,6 +293,8 @@ foreach ($byAbility as $abId => $events) {
|
||||
}
|
||||
$current['targets'][$tgtId]['amount'] += $ev['amount'];
|
||||
$current['targets'][$tgtId]['absorbed'] += $ev['absorbed'];
|
||||
$current['targets'][$tgtId]['unmitigatedAmount'] += $ev['unmitigatedAmount'];
|
||||
$current['targets'][$tgtId]['mitigated'] += $ev['mitigated'];
|
||||
$current['targets'][$tgtId]['overkill'] += $ev['overkill'];
|
||||
}
|
||||
if ($current !== null) $clusters[] = $current;
|
||||
@ -266,6 +314,8 @@ foreach ($clusters as $group) {
|
||||
'role' => $p['role'] ?? 'dps',
|
||||
'amount' => $tgt['amount'],
|
||||
'absorbed' => $tgt['absorbed'],
|
||||
'unmitigatedAmount' => $tgt['unmitigatedAmount'],
|
||||
'mitigated' => $tgt['mitigated'],
|
||||
'overkill' => $tgt['overkill'],
|
||||
'hp' => $tgt['hp'],
|
||||
'maxHp' => $tgt['maxHp'],
|
||||
|
||||
BIN
assets/icons/mitigation/bloodwhetting.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/icons/mitigation/divine-benison.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/icons/mitigation/divine-caress.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/icons/mitigation/eukrasian-diagnosis.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/icons/mitigation/eukrasian-prognosis-ii.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/icons/mitigation/eukrasian-prognosis.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/icons/mitigation/galvanize.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/icons/mitigation/guardian.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/icons/mitigation/haima.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/icons/mitigation/improvised-finish.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/icons/mitigation/intersection.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/icons/mitigation/neutral-sect.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/icons/mitigation/radiant-aegis.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/icons/mitigation/seraphic-veil.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
assets/icons/mitigation/tempera-coat.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/icons/mitigation/tempera-grassa.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/icons/mitigation/the-spire.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
@ -1,8 +1,7 @@
|
||||
(function () {
|
||||
const MITIG_ICONS = {
|
||||
// DR buffs
|
||||
'Passage of Arms': 'assets/icons/mitigation/passage-of-arms.png',
|
||||
'Divine Veil': 'assets/icons/mitigation/divine-veil.png',
|
||||
'Shake It Off': 'assets/icons/mitigation/shake-it-off.png',
|
||||
'Dark Missionary': 'assets/icons/mitigation/dark-missionary.png',
|
||||
'Heart of Light': 'assets/icons/mitigation/heart-of-light.png',
|
||||
'Temperance': 'assets/icons/mitigation/temperance.png',
|
||||
@ -12,14 +11,37 @@
|
||||
'Collective Unconscious': 'assets/icons/mitigation/collective-unconscious.png',
|
||||
'Holos': 'assets/icons/mitigation/holos.png',
|
||||
'Kerachole': 'assets/icons/mitigation/kerachole.png',
|
||||
'Panhaima': 'assets/icons/mitigation/panhaima.png',
|
||||
'Troubadour': 'assets/icons/mitigation/troubadour.png',
|
||||
'Tactician': 'assets/icons/mitigation/tactician.png',
|
||||
'Shield Samba': 'assets/icons/mitigation/shield-samba.png',
|
||||
'Magick Barrier': 'assets/icons/mitigation/magick-barrier.png',
|
||||
// Debuffs
|
||||
'Reprisal': 'assets/icons/mitigation/reprisal.png',
|
||||
'Feint': 'assets/icons/mitigation/feint.png',
|
||||
'Addle': 'assets/icons/mitigation/addle.png',
|
||||
// Shields
|
||||
'Divine Veil': 'assets/icons/mitigation/divine-veil.png',
|
||||
'Guardian': 'assets/icons/mitigation/guardian.png',
|
||||
'Shake It Off': 'assets/icons/mitigation/shake-it-off.png',
|
||||
'Bloodwhetting': 'assets/icons/mitigation/bloodwhetting.png',
|
||||
'Divine Benison': 'assets/icons/mitigation/divine-benison.png',
|
||||
'Divine Caress': 'assets/icons/mitigation/divine-caress.png',
|
||||
'Intersection': 'assets/icons/mitigation/intersection.png',
|
||||
'Neutral Sect': 'assets/icons/mitigation/neutral-sect.png',
|
||||
'the Spire': 'assets/icons/mitigation/the-spire.png',
|
||||
'Panhaima': 'assets/icons/mitigation/panhaima.png',
|
||||
'Holosakos': 'assets/icons/mitigation/holos.png',
|
||||
'Eukrasian Prognosis': 'assets/icons/mitigation/eukrasian-prognosis.png',
|
||||
'Eukrasian Prognosis II': 'assets/icons/mitigation/eukrasian-prognosis-ii.png',
|
||||
'Eukrasian Diagnosis': 'assets/icons/mitigation/eukrasian-diagnosis.png',
|
||||
'Differential Diagnosis': 'assets/icons/mitigation/eukrasian-diagnosis.png',
|
||||
'Haima': 'assets/icons/mitigation/haima.png',
|
||||
'Galvanize': 'assets/icons/mitigation/galvanize.png',
|
||||
'Seraphic Veil': 'assets/icons/mitigation/seraphic-veil.png',
|
||||
'Radiant Aegis': 'assets/icons/mitigation/radiant-aegis.png',
|
||||
'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png',
|
||||
'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png',
|
||||
'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png',
|
||||
};
|
||||
|
||||
const JOB_ABBR = {
|
||||
@ -342,7 +364,12 @@
|
||||
const afterPct = t.hp / t.maxHp * 100;
|
||||
const damagePct = t.amount / t.maxHp * 100;
|
||||
const hpColor = afterPct > 50 ? 'var(--green)' : afterPct > 25 ? '#e8a020' : 'var(--red)';
|
||||
return `<div class="aoe-hp-bar">
|
||||
const missingBefore = Math.max(0, t.maxHp - t.hp - t.amount);
|
||||
const fmt = n => n.toLocaleString();
|
||||
const hpPct = (t.hp / t.maxHp * 100).toFixed(1);
|
||||
const missingPct = (missingBefore / t.maxHp * 100).toFixed(1);
|
||||
const tooltip = `MaxHP: ${fmt(t.maxHp)}\nCurrentHP: ${fmt(t.hp)}\nHP-%: ${hpPct}%\nMissingBefore: ${fmt(missingBefore)}\nMissing-%: ${missingPct}%`;
|
||||
return `<div class="aoe-hp-bar" title="${tooltip}">
|
||||
<div class="aoe-hp-remaining" style="width:${afterPct.toFixed(1)}%;background:${hpColor}"></div>
|
||||
<div class="aoe-hp-damage" style="width:${damagePct.toFixed(1)}%"></div>
|
||||
</div>`;
|
||||
@ -351,10 +378,11 @@
|
||||
const currentMitigNames = new Set((t.mitigations ?? []).map(m => m.name));
|
||||
const refTarget = refEv?.targets?.find(rt => rt.name === t.name);
|
||||
const missingMitigs = refTarget
|
||||
? (refTarget.mitigations ?? []).filter(m => m.buffType !== 'debuff' && !currentMitigNames.has(m.name))
|
||||
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'buff' && !currentMitigNames.has(m.name))
|
||||
: [];
|
||||
|
||||
const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType !== 'debuff').map(m => {
|
||||
// DR buff icons (shown below player box)
|
||||
const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
|
||||
const iconSrc = MITIG_ICONS[m.name];
|
||||
if (!iconSrc) return '';
|
||||
const dr = m.dr > 0 ? ` −${m.dr}%` : '';
|
||||
@ -367,6 +395,17 @@
|
||||
return `<img class="aoe-target-buff-icon aoe-buff-missing" src="${iconSrc}" alt="${m.name}" title="${m.name} fehlt (war im Referenz-Pull aktiv)">`;
|
||||
}).join('');
|
||||
|
||||
// Shield tooltip on absorbed value
|
||||
const activeShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
|
||||
const missingShields = refTarget
|
||||
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'shield' && !currentMitigNames.has(m.name))
|
||||
: [];
|
||||
const shieldLines = [
|
||||
...activeShields.map(s => s.name),
|
||||
...missingShields.map(s => `[fehlt: ${s.name}]`),
|
||||
];
|
||||
const shieldTitle = shieldLines.length ? shieldLines.join('\n') : null;
|
||||
|
||||
const dead = t.hp === 0 && t.maxHp > 0;
|
||||
|
||||
return `
|
||||
@ -379,7 +418,7 @@
|
||||
<div class="aoe-target-body">
|
||||
<div class="aoe-target-row">
|
||||
<span class="aoe-target-name">${t.name}</span>
|
||||
<span class="aoe-target-dmg">${fmtDmg(t.amount)}${t.absorbed > 0 ? ` <span class="aoe-target-absorbed">+${fmtDmg(t.absorbed)}</span>` : ''}</span>
|
||||
<span class="aoe-target-dmg"><span title="Unmitigated: ${(t.unmitigatedAmount||0).toLocaleString()}\nMitigated: ${(t.mitigated||0).toLocaleString()}\nShielded: ${(t.absorbed||0).toLocaleString()}\nHP damage: ${(t.amount||0).toLocaleString()}">${fmtDmg(t.amount)}</span>${t.absorbed > 0 ? ` <span class="aoe-target-absorbed" title="${shieldTitle ?? 'Keine erkannten Schilde'}">+${fmtDmg(t.absorbed)}</span>` : ''}</span>
|
||||
</div>
|
||||
${hpBar}
|
||||
</div>
|
||||
@ -418,7 +457,7 @@
|
||||
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
|
||||
: '';
|
||||
|
||||
const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType !== 'debuff').map(m => {
|
||||
const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
|
||||
const iconSrc = MITIG_ICONS[m.name];
|
||||
if (!iconSrc) return '';
|
||||
const dr = m.dr > 0 ? ` −${m.dr}%` : '';
|
||||
|
||||
@ -14,6 +14,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
let allFights = [];
|
||||
|
||||
const codeInput = form.elements['report_code'];
|
||||
codeInput.addEventListener('input', () => {
|
||||
const match = codeInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/);
|
||||
if (match) codeInput.value = match[1];
|
||||
});
|
||||
|
||||
function formatDuration(ms) {
|
||||
const min = Math.floor(ms / 60000);
|
||||
const sec = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="report_code"
|
||||
placeholder="z.B. aBcDeFgH1234"
|
||||
placeholder="Code oder FFLogs-Link"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
|
||||