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>
This commit is contained in:
xziino 2026-05-21 13:03:56 +02:00
parent 5345927b83
commit 7c0f070490
21 changed files with 139 additions and 44 deletions

View File

@ -56,29 +56,63 @@ function fflogs_gql(string $query): array {
return json_decode($body, true) ?? ['_parse_error' => true]; return json_decode($body, true) ?? ['_parse_error' => true];
} }
// ── Party-wide mitigation + boss debuffs to track ───────────────────────── // ── Party-wide mitigation, shields + boss debuffs to track ─────────────────
// Barriers (dr = 0) are shown without a percentage. // 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 = [ const MITIGATION_ABILITIES = [
'Passage of Arms' => ['dr' => 15, 'buffType' => 'buff'], // ── Damage reduction buffs ──────────────────────────────────────────────
'Divine Veil' => ['dr' => 0, 'buffType' => 'buff'], 'Passage of Arms' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001175],
'Shake It Off' => ['dr' => 0, 'buffType' => 'buff'], 'Dark Missionary' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001894],
'Dark Missionary' => ['dr' => 10, 'buffType' => 'buff'], 'Heart of Light' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001839],
'Heart of Light' => ['dr' => 10, 'buffType' => 'buff'], 'Temperance' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001873],
'Temperance' => ['dr' => 10, 'buffType' => 'buff'], 'Sacred Soil' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001944],
'Sacred Soil' => ['dr' => 10, 'buffType' => 'buff'], 'Expedient' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002711], // FFLogs: "Desperate Measures"
'Expedient' => ['dr' => 10, 'buffType' => 'buff'], 'Fey Illumination' => ['dr' => 5, 'buffType' => 'buff', 'statusId' => 1000317],
'Fey Illumination' => ['dr' => 5, 'buffType' => 'buff'], 'Collective Unconscious' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1000849],
'Collective Unconscious' => ['dr' => 10, 'buffType' => 'buff'], 'Holos' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1003003],
'Holos' => ['dr' => 10, 'buffType' => 'buff'], 'Kerachole' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002618],
'Kerachole' => ['dr' => 10, 'buffType' => 'buff'], 'Troubadour' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001934],
'Panhaima' => ['dr' => 0, 'buffType' => 'buff'], 'Tactician' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001951],
'Troubadour' => ['dr' => 15, 'buffType' => 'buff'], 'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826],
'Tactician' => ['dr' => 15, 'buffType' => 'buff'], 'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707],
'Shield Samba' => ['dr' => 15, 'buffType' => 'buff'], // ── Shields ─────────────────────────────────────────────────────────────
'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff'], // PLD
'Reprisal' => ['dr' => 10, 'buffType' => 'debuff'], 'Divine Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001362],
'Feint' => ['dr' => 10, 'buffType' => 'debuff'], 'Guardian' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003830], // FFLogs: "Guardian's Will"
'Addle' => ['dr' => 10, 'buffType' => 'debuff'], // 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 { function resolveMitigations(string $buffStr, array $mitigIdMap): array {
@ -127,13 +161,20 @@ foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ??
$abilityNames[(int)$ab['gameID']] = $ab['name']; $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 = []; $mitigIdMap = [];
foreach ($abilityNames as $gameId => $name) { foreach ($abilityNames as $gameId => $name) {
if (isset(MITIGATION_ABILITIES[$name])) { if (isset(MITIGATION_ABILITIES[$name])) {
$mitigIdMap[$gameId] = array_merge(['name' => $name], 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 // player actorID → player data
$pdRaw = $pdResult['data']['reportData']['report']['playerDetails'] ?? null; $pdRaw = $pdResult['data']['reportData']['report']['playerDetails'] ?? null;
@ -153,6 +194,7 @@ foreach ($roleMap as $group => $role) {
} }
} }
// ── 2. Damage-taken events (paginated) ───────────────────────────────────── // ── 2. Damage-taken events (paginated) ─────────────────────────────────────
$allEvents = []; $allEvents = [];
$nextPage = $startTime; $nextPage = $startTime;
@ -203,9 +245,11 @@ foreach ($allEvents as $ev) {
$byAbility[$abId][] = [ $byAbility[$abId][] = [
'ts' => (float)($ev['timestamp'] ?? 0), 'ts' => (float)($ev['timestamp'] ?? 0),
'tgtId' => $tgtId, 'tgtId' => $tgtId,
'amount' => (int)($ev['amount'] ?? 0), 'amount' => (int)($ev['amount'] ?? 0),
'absorbed' => (int)($ev['absorbed'] ?? 0), 'absorbed' => (int)($ev['absorbed'] ?? 0),
'overkill' => (int)($ev['overkill'] ?? 0), 'overkill' => (int)($ev['overkill'] ?? 0),
'unmitigatedAmount' => (int)($ev['unmitigatedAmount'] ?? 0),
'mitigated' => (int)($ev['mitigated'] ?? 0),
'hp' => (int)($ev['targetResources']['hitPoints'] ?? 0), 'hp' => (int)($ev['targetResources']['hitPoints'] ?? 0),
'maxHp' => (int)($ev['targetResources']['maxHitPoints'] ?? 0), 'maxHp' => (int)($ev['targetResources']['maxHitPoints'] ?? 0),
'buffs' => $ev['buffs'] ?? '', 'buffs' => $ev['buffs'] ?? '',
@ -237,16 +281,20 @@ foreach ($byAbility as $abId => $events) {
if (!isset($current['targets'][$tgtId])) { if (!isset($current['targets'][$tgtId])) {
$current['targets'][$tgtId] = [ $current['targets'][$tgtId] = [
'id' => $tgtId, 'id' => $tgtId,
'amount' => 0, 'amount' => 0,
'absorbed' => 0, 'absorbed' => 0,
'unmitigatedAmount' => 0,
'mitigated' => 0,
'overkill' => 0, 'overkill' => 0,
'hp' => $ev['hp'], 'hp' => $ev['hp'],
'maxHp' => $ev['maxHp'], 'maxHp' => $ev['maxHp'],
'buffs' => $ev['buffs'], 'buffs' => $ev['buffs'],
]; ];
} }
$current['targets'][$tgtId]['amount'] += $ev['amount']; $current['targets'][$tgtId]['amount'] += $ev['amount'];
$current['targets'][$tgtId]['absorbed'] += $ev['absorbed']; $current['targets'][$tgtId]['absorbed'] += $ev['absorbed'];
$current['targets'][$tgtId]['unmitigatedAmount'] += $ev['unmitigatedAmount'];
$current['targets'][$tgtId]['mitigated'] += $ev['mitigated'];
$current['targets'][$tgtId]['overkill'] += $ev['overkill']; $current['targets'][$tgtId]['overkill'] += $ev['overkill'];
} }
if ($current !== null) $clusters[] = $current; if ($current !== null) $clusters[] = $current;
@ -264,8 +312,10 @@ foreach ($clusters as $group) {
'name' => $p['name'] ?? '?', 'name' => $p['name'] ?? '?',
'type' => $p['type'] ?? '', 'type' => $p['type'] ?? '',
'role' => $p['role'] ?? 'dps', 'role' => $p['role'] ?? 'dps',
'amount' => $tgt['amount'], 'amount' => $tgt['amount'],
'absorbed' => $tgt['absorbed'], 'absorbed' => $tgt['absorbed'],
'unmitigatedAmount' => $tgt['unmitigatedAmount'],
'mitigated' => $tgt['mitigated'],
'overkill' => $tgt['overkill'], 'overkill' => $tgt['overkill'],
'hp' => $tgt['hp'], 'hp' => $tgt['hp'],
'maxHp' => $tgt['maxHp'], 'maxHp' => $tgt['maxHp'],

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,8 +1,7 @@
(function () { (function () {
const MITIG_ICONS = { const MITIG_ICONS = {
// DR buffs
'Passage of Arms': 'assets/icons/mitigation/passage-of-arms.png', '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', 'Dark Missionary': 'assets/icons/mitigation/dark-missionary.png',
'Heart of Light': 'assets/icons/mitigation/heart-of-light.png', 'Heart of Light': 'assets/icons/mitigation/heart-of-light.png',
'Temperance': 'assets/icons/mitigation/temperance.png', 'Temperance': 'assets/icons/mitigation/temperance.png',
@ -12,14 +11,37 @@
'Collective Unconscious': 'assets/icons/mitigation/collective-unconscious.png', 'Collective Unconscious': 'assets/icons/mitigation/collective-unconscious.png',
'Holos': 'assets/icons/mitigation/holos.png', 'Holos': 'assets/icons/mitigation/holos.png',
'Kerachole': 'assets/icons/mitigation/kerachole.png', 'Kerachole': 'assets/icons/mitigation/kerachole.png',
'Panhaima': 'assets/icons/mitigation/panhaima.png',
'Troubadour': 'assets/icons/mitigation/troubadour.png', 'Troubadour': 'assets/icons/mitigation/troubadour.png',
'Tactician': 'assets/icons/mitigation/tactician.png', 'Tactician': 'assets/icons/mitigation/tactician.png',
'Shield Samba': 'assets/icons/mitigation/shield-samba.png', 'Shield Samba': 'assets/icons/mitigation/shield-samba.png',
'Magick Barrier': 'assets/icons/mitigation/magick-barrier.png', 'Magick Barrier': 'assets/icons/mitigation/magick-barrier.png',
// Debuffs
'Reprisal': 'assets/icons/mitigation/reprisal.png', 'Reprisal': 'assets/icons/mitigation/reprisal.png',
'Feint': 'assets/icons/mitigation/feint.png', 'Feint': 'assets/icons/mitigation/feint.png',
'Addle': 'assets/icons/mitigation/addle.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 = { const JOB_ABBR = {
@ -339,10 +361,15 @@
// Current targets // Current targets
const targets = visibleTargets.map(t => { const targets = visibleTargets.map(t => {
const hpBar = (t.maxHp > 0) ? (() => { const hpBar = (t.maxHp > 0) ? (() => {
const afterPct = t.hp / t.maxHp * 100; const afterPct = t.hp / t.maxHp * 100;
const damagePct = t.amount / t.maxHp * 100; const damagePct = t.amount / t.maxHp * 100;
const hpColor = afterPct > 50 ? 'var(--green)' : afterPct > 25 ? '#e8a020' : 'var(--red)'; 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-remaining" style="width:${afterPct.toFixed(1)}%;background:${hpColor}"></div>
<div class="aoe-hp-damage" style="width:${damagePct.toFixed(1)}%"></div> <div class="aoe-hp-damage" style="width:${damagePct.toFixed(1)}%"></div>
</div>`; </div>`;
@ -351,10 +378,11 @@
const currentMitigNames = new Set((t.mitigations ?? []).map(m => m.name)); const currentMitigNames = new Set((t.mitigations ?? []).map(m => m.name));
const refTarget = refEv?.targets?.find(rt => rt.name === t.name); const refTarget = refEv?.targets?.find(rt => rt.name === t.name);
const missingMitigs = refTarget 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]; const iconSrc = MITIG_ICONS[m.name];
if (!iconSrc) return ''; if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : ''; 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)">`; 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(''); }).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; const dead = t.hp === 0 && t.maxHp > 0;
return ` return `
@ -379,7 +418,7 @@
<div class="aoe-target-body"> <div class="aoe-target-body">
<div class="aoe-target-row"> <div class="aoe-target-row">
<span class="aoe-target-name">${t.name}</span> <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> </div>
${hpBar} ${hpBar}
</div> </div>
@ -418,7 +457,7 @@
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>` ? `<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]; const iconSrc = MITIG_ICONS[m.name];
if (!iconSrc) return ''; if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : ''; const dr = m.dr > 0 ? ` ${m.dr}%` : '';

View File

@ -14,6 +14,12 @@ document.addEventListener('DOMContentLoaded', () => {
let allFights = []; 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) { function formatDuration(ms) {
const min = Math.floor(ms / 60000); const min = Math.floor(ms / 60000);
const sec = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0'); const sec = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');

View File

@ -7,7 +7,7 @@
<input <input
type="text" type="text"
name="report_code" name="report_code"
placeholder="z.B. aBcDeFgH1234" placeholder="Code oder FFLogs-Link"
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
required required