From 399e5fad93cacf0d5bdd07e0740c6d7837df4982 Mon Sep 17 00:00:00 2001 From: xziino Date: Wed, 20 May 2026 14:58:10 +0200 Subject: [PATCH] Add analysis features: HP bars, death highlight, mitigation via buffs field, Event Explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HP bar per player in AoE timeline (3-segment: remaining/damage/missing) - Death highlight: red border + background when hp === 0 - Mitigation tracking refactored to read buffs field directly from damage events instead of separate applybuff/removebuff window tracking (simpler, more accurate) - Mitigations are now per-target instead of per-event - Add Magick Barrier to tracked mitigation abilities - Player filter text input above AoE timeline - Player cards as toggle buttons; tanks hidden by default; sorted Healer→DPS→Tank - Fix Temperance double-display (dedup by name instead of abilityId) - Event Explorer in Report tab: DataType, raw event type, ability, player dropdowns, limit, time range; abilities and players loaded on fight select - Fight percentage display fix (was divided by 100 twice) Co-Authored-By: Claude Sonnet 4.6 --- api/abilities.php | 113 ++++++++++++++ api/analysis.php | 167 +++++---------------- api/debug-events.php | 127 ++++++++++++++++ assets/icons/mitigation/magick-barrier.png | Bin 0 -> 13252 bytes css/analysis.css | 81 ++++++++-- css/components.css | 24 +++ js/analysis.js | 88 +++++++++-- js/app.js | 95 ++++++++++-- templates/event-explorer.php | 68 +++++++++ templates/tab-analysis.php | 5 +- templates/tab-report.php | 1 + 11 files changed, 604 insertions(+), 165 deletions(-) create mode 100644 api/abilities.php create mode 100644 api/debug-events.php create mode 100644 assets/icons/mitigation/magick-barrier.png create mode 100644 templates/event-explorer.php diff --git a/api/abilities.php b/api/abilities.php new file mode 100644 index 0000000..eccba47 --- /dev/null +++ b/api/abilities.php @@ -0,0 +1,113 @@ + 'Method not allowed']); exit; } +if (empty($_SESSION['access_token'])) { echo json_encode(['reauth' => true]); exit; } +if (($_SESSION['token_expires'] ?? 0) <= time()) { echo json_encode(['reauth' => true]); exit; } + +$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? ''); +$fightId = (int)($_POST['fight_id'] ?? 0); +$startTime = (float)($_POST['start_time'] ?? 0); +$endTime = (float)($_POST['end_time'] ?? 0); + +if (!$reportCode || !$fightId || !$endTime) { http_response_code(400); echo json_encode(['error' => 'Missing params']); exit; } + +$token = $_SESSION['access_token']; + +function ab_gql(string $query): array { + global $token; + $ch = curl_init(GRAPHQL_URI); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(['query' => $query]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token], + CURLOPT_SSL_VERIFYPEER => !DEV_MODE, + ]); + $body = curl_exec($ch); + curl_close($ch); + return json_decode($body, true) ?? []; +} + +// Fetch ability names from masterData +$mdResult = ab_gql(<< 'tank', 'healers' => 'healer', 'dps' => 'dps']; +foreach ($roleMap as $group => $role) { + foreach ($pdGroups[$group] ?? [] as $p) { + $players[] = ['name' => $p['name'], 'role' => $role]; + } +} +usort($players, fn($a, $b) => strcmp($a['name'], $b['name'])); + +// Fetch DamageTaken events from NPC sources only (paginated) +$filterEsc = 'source.type = \\"NPC\\"'; +$seenIds = []; +$nextPage = $startTime; + +for ($page = 0; $page < 10; $page++) { + $evResult = ab_gql(<< 7) $seenIds[$abId] = true; + } + $nextPage = $ev['nextPageTimestamp'] ?? null; + if ($nextPage === null || $nextPage >= $endTime) break; +} + +// Build sorted ability list +$abilities = []; +foreach (array_keys($seenIds) as $id) { + $abilities[] = [ + 'id' => $id, + 'name' => $abilityNames[$id] ?? ('Ability #' . $id), + ]; +} +usort($abilities, fn($a, $b) => strcmp($a['name'], $b['name'])); + +echo json_encode(['abilities' => $abilities, 'players' => $players]); diff --git a/api/analysis.php b/api/analysis.php index 1ae7b3c..4d95bfe 100644 --- a/api/analysis.php +++ b/api/analysis.php @@ -75,11 +75,28 @@ const MITIGATION_ABILITIES = [ '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'], ]; +function resolveMitigations(string $buffStr, array $mitigIdMap): array { + if ($buffStr === '') return []; + $result = []; + foreach (explode('.', $buffStr) as $idStr) { + $id = (int)$idStr; + if (isset($mitigIdMap[$id])) { + $result[] = [ + 'name' => $mitigIdMap[$id]['name'], + 'dr' => $mitigIdMap[$id]['dr'], + 'buffType' => $mitigIdMap[$id]['buffType'], + ]; + } + } + return $result; +} + // ── 1. Player details + masterData (ability names) ───────────────────────── $pdResult = fflogs_gql(<< $role) { } } -// ── 2. Buff / debuff events for mitigation tracking ──────────────────────── -$mitigWindows = []; // [{name, dr, buffType, casterName, start, end}] - -if (!empty($mitigIdMap)) { - $idsStr = implode(', ', array_keys($mitigIdMap)); - $filterRaw = 'type in ("applybuff","removebuff","applydebuff","removedebuff","refreshbuff") and ability.id in (' . $idsStr . ')'; - $filterEsc = str_replace('"', '\\"', $filterRaw); - - $buffEvents = []; - $nextPage = $startTime; - - for ($page = 0; $page < 5; $page++) { - $bResult = fflogs_gql(<< true]); exit; } - if (isset($bResult['_curl_error'])) break; - - $bEv = $bResult['data']['reportData']['report']['events'] ?? []; - $buffEvents = array_merge($buffEvents, $bEv['data'] ?? []); - $nextPage = $bEv['nextPageTimestamp'] ?? null; - if ($nextPage === null || $nextPage >= $endTime) break; - } - - // Build time windows per (abilityId, sourceId). - // Party-wide buffs fire one applybuff per party member — we only open the - // window on the FIRST applybuff for a given (ability, caster) pair. - $openWindows = []; // "$abId_$srcId" => [start, abilityId, sourceId] - - foreach ($buffEvents as $ev) { - $type = $ev['type'] ?? ''; - $abId = (int)($ev['abilityGameID'] ?? 0); - $srcId = (int)($ev['sourceID'] ?? 0); - $ts = (float)($ev['timestamp'] ?? 0); - - if (!isset($mitigIdMap[$abId])) continue; - - $key = $abId . '_' . $srcId; - - if (in_array($type, ['applybuff', 'applydebuff'])) { - if (!isset($openWindows[$key])) { - $openWindows[$key] = ['start' => $ts, 'abilityId' => $abId, 'sourceId' => $srcId]; - } - } elseif ($type === 'refreshbuff') { - // Buff was refreshed — keep window open (don't reset start) - if (!isset($openWindows[$key])) { - $openWindows[$key] = ['start' => $ts, 'abilityId' => $abId, 'sourceId' => $srcId]; - } - } elseif (in_array($type, ['removebuff', 'removedebuff'])) { - if (isset($openWindows[$key])) { - $info = $mitigIdMap[$abId]; - $mitigWindows[] = [ - 'name' => $info['name'], - 'dr' => $info['dr'], - 'buffType' => $info['buffType'], - 'casterName' => $players[$srcId]['name'] ?? '?', - 'abilityId' => $abId, - 'start' => $openWindows[$key]['start'], - 'end' => $ts, - ]; - unset($openWindows[$key]); - } - } - } - - // Buffs still active at fight end - foreach ($openWindows as $win) { - $info = $mitigIdMap[$win['abilityId']]; - $mitigWindows[] = [ - 'name' => $info['name'], - 'dr' => $info['dr'], - 'buffType' => $info['buffType'], - 'casterName' => $players[$win['sourceId']]['name'] ?? '?', - 'abilityId' => $win['abilityId'], - 'start' => $win['start'], - 'end' => $endTime, - ]; - } -} - -// ── 3. Damage-taken events (paginated) ───────────────────────────────────── +// ── 2. Damage-taken events (paginated) ───────────────────────────────────── $allEvents = []; $nextPage = $startTime; @@ -239,6 +161,7 @@ for ($page = 0; $page < 10; $page++) { events( fightIDs: [$fightId], dataType: DamageTaken, + includeResources: true, startTime: $nextPage, endTime: $endTime ) { @@ -259,18 +182,18 @@ for ($page = 0; $page < 10; $page++) { if ($nextPage === null || $nextPage >= $endTime) break; } -// ── 4. AoE detection ─────────────────────────────────────────────────────── +// ── 3. AoE detection ─────────────────────────────────────────────────────── $buckets = []; foreach ($allEvents as $ev) { if (!empty($ev['tick'])) continue; - if (($ev['type'] ?? '') !== 'calculateddamage') continue; + if (($ev['type'] ?? '') !== 'damage') continue; $abId = (int)($ev['abilityGameID'] ?? 0); $tgtId = (int)($ev['targetID'] ?? 0); if (!$abId || !$tgtId || $abId <= 7) continue; $ts = (float)($ev['timestamp'] ?? 0); - $bucket = (int)floor($ts / 300) * 300; + $bucket = (int)floor($ts / 990) * 990; $key = $bucket . '_' . $abId; if (!isset($buckets[$key])) { @@ -283,7 +206,13 @@ foreach ($allEvents as $ev) { } if (!isset($buckets[$key]['targets'][$tgtId])) { - $buckets[$key]['targets'][$tgtId] = ['id' => $tgtId, 'amount' => 0]; + $buckets[$key]['targets'][$tgtId] = [ + 'id' => $tgtId, + 'amount' => 0, + 'hp' => (int)($ev['targetResources']['hitPoints'] ?? 0), + 'maxHp' => (int)($ev['targetResources']['maxHitPoints'] ?? 0), + 'buffs' => $ev['buffs'] ?? '', + ]; } $buckets[$key]['targets'][$tgtId]['amount'] += (int)($ev['amount'] ?? 0); } @@ -296,41 +225,25 @@ foreach ($buckets as $group) { foreach ($group['targets'] as $tgtId => $tgt) { $p = $players[$tgtId] ?? null; $targets[] = [ - 'id' => $tgtId, - 'name' => $p['name'] ?? '?', - 'type' => $p['type'] ?? '', - 'role' => $p['role'] ?? 'dps', - 'amount' => $tgt['amount'], + 'id' => $tgtId, + 'name' => $p['name'] ?? '?', + 'type' => $p['type'] ?? '', + 'role' => $p['role'] ?? 'dps', + 'amount' => $tgt['amount'], + 'hp' => $tgt['hp'], + 'maxHp' => $tgt['maxHp'], + 'mitigations' => resolveMitigations($tgt['buffs'], $mitigIdMap), ]; } - usort($targets, fn($a, $b) => $b['amount'] <=> $a['amount']); - - // Collect active mitigations at this timestamp - $evTs = $group['timestamp']; - $activeMitig = []; - $seen = []; - foreach ($mitigWindows as $win) { - if ($win['start'] <= $evTs && $win['end'] > $evTs) { - $dedupeKey = $win['abilityId'] . '_' . $win['casterName']; - if (!isset($seen[$dedupeKey])) { - $seen[$dedupeKey] = true; - $activeMitig[] = [ - 'name' => $win['name'], - 'dr' => $win['dr'], - 'buffType' => $win['buffType'], - 'casterName' => $win['casterName'], - ]; - } - } - } + $roleOrder = ['healer' => 0, 'dps' => 1, 'tank' => 2]; + usort($targets, fn($a, $b) => ($roleOrder[$a['role']] ?? 1) <=> ($roleOrder[$b['role']] ?? 1)); $aoeEvents[] = [ - 'timestamp' => $evTs, + 'timestamp' => $group['timestamp'], 'abilityId' => $group['abilityId'], 'abilityName' => $group['abilityName'], 'targets' => $targets, 'totalDamage' => array_sum(array_column($targets, 'amount')), - 'mitigations' => $activeMitig, ]; } usort($aoeEvents, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']); diff --git a/api/debug-events.php b/api/debug-events.php new file mode 100644 index 0000000..4ab54ab --- /dev/null +++ b/api/debug-events.php @@ -0,0 +1,127 @@ + 'Method not allowed']); exit; } +if (empty($_SESSION['access_token'])) { echo json_encode(['reauth' => true]); exit; } +if (($_SESSION['token_expires'] ?? 0) <= time()) { echo json_encode(['reauth' => true]); exit; } + +$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? ''); +$fightId = (int)($_POST['fight_id'] ?? 0); +$startTime = (float)($_POST['start_time'] ?? 0); +$endTime = (float)($_POST['end_time'] ?? 0); +$playerName = trim($_POST['player_name'] ?? ''); +$eventType = trim($_POST['event_type'] ?? ''); +$abilityId = (int)($_POST['ability_id'] ?? 0); +$limit = max(1, min(500, (int)($_POST['limit'] ?? 20))); +$startOffset = (float)($_POST['start_offset'] ?? 0) * 1000; // s → ms +$endOffset = isset($_POST['end_offset']) && $_POST['end_offset'] !== '' + ? (float)$_POST['end_offset'] * 1000 + : null; + +$allowedTypes = ['DamageTaken', 'DamageDone', 'Healing', 'Casts', 'Buffs', 'Deaths']; +$dataType = in_array($_POST['data_type'] ?? '', $allowedTypes) ? $_POST['data_type'] : 'DamageTaken'; + +if (!$reportCode || !$fightId) { http_response_code(400); echo json_encode(['error' => 'Missing params']); exit; } + +$queryStart = $startTime + $startOffset; +$queryEnd = $endOffset !== null ? $startTime + $endOffset : $endTime; +$queryEnd = min($queryEnd, $endTime); + +$token = $_SESSION['access_token']; + +function dbg_gql(string $query): array { + global $token; + $ch = curl_init(GRAPHQL_URI); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(['query' => $query]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token], + CURLOPT_SSL_VERIFYPEER => !DEV_MODE, + ]); + $body = curl_exec($ch); + curl_close($ch); + return json_decode($body, true) ?? []; +} + +// Resolve player name → actor IDs (source + target) +$playerIds = []; +if ($playerName !== '') { + $pd = dbg_gql(<< 0 && (int)($ev['abilityGameID'] ?? 0) !== $abilityId) continue; + if (!empty($playerIds)) { + $srcId = (int)($ev['sourceID'] ?? -1); + $tgtId = (int)($ev['targetID'] ?? -1); + if (!in_array($srcId, $playerIds) && !in_array($tgtId, $playerIds)) continue; + } + $filtered[] = $ev; + if (count($filtered) >= $limit) break; +} + +echo json_encode([ + 'data_type' => $dataType, + 'event_type' => $eventType ?: null, + 'ability_id' => $abilityId ?: null, + 'player_name' => $playerName ?: null, + 'player_ids' => $playerIds ?: null, + 'time_range' => [ + 'from_ms' => (int)$queryStart, + 'to_ms' => (int)$queryEnd, + ], + 'total_before_limit' => count($events), + 'count' => count($filtered), + 'events' => $filtered, +], JSON_PRETTY_PRINT); diff --git a/assets/icons/mitigation/magick-barrier.png b/assets/icons/mitigation/magick-barrier.png new file mode 100644 index 0000000000000000000000000000000000000000..4a6026e35665dbd67b85b1d72ec027ce9416e9f3 GIT binary patch literal 13252 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�}BfO;mFV z4az5Tb45tmiQ1@;@|4fPybO$bq8GY{BlPEE%Bk<;ffpsx$rQaZvZ*z#6k}o|<+eH) zY(_7LlN#IBZ&9m~F*HYdAqsBlKToyDqpL|2@AC)E$nNFnJ z?(laa|KhFhe(BpkR_s zWRjhpvN4vV4}D)c#(%(ytudJ7*@BVB#v&9T`vmNTzBsaf;^0+cm@6CeT#Qz7d1R=v z$Sz1Xm^m(32C_aFUCu%=9Q=e-8O38{sr+2DrU|`Cj+UVJIoEL@`f8AvJBNIjVgs;f zamY_QUQk4{qH|P}u3OtgwjOFsgc|wt!K9tcL@oR?f`T)2!d*pDQ|&QiT1$A@G(r~( zyC3ryWmHV=Rm{$J8%CRFVOeGkdhfm{#K52Z!P~~#*U&2HQQp$`U^zThy&<>sB4aLK7gw7pW_HA)?D&C zNM{Bv60QP`D7d#pO{giYlRzI7KsIVWi7@=smP3Z+!~V-X;AC=HN3x-H+n?y@AcZr; z+r(5TaMWHm5`p>hN~jUa9|Nk8l>}*|KxooQz84Fs-T`1Hhs7|O3GT&!os;%ohhc?x zg+9-rWF#0052mCruDQO$p0%1mp8t%*lS0lBraouN2>vosP#}Q zA5j27k1cGqFLeR|+imPB6tYW457JN_E7_Q%qpa$kATRXT$Q9 zT1BCZp%vcD1rEqSoh>}#@;UI+$b1R99hQa=yA^1`ZRR6}x=K6EUF(?=bfpVum_VRI zxRQ*)Rst$9R7@@)7fXTrLH9Ob&}++LEbPwMfLct&q&L8LCO;!nB`MeZ6@#yy3pw3d__96=*&{=RK}>X- zTR1pw3`Nh~fg)i_6-o28wHEYu`LH=3!sax3U2(nn zxnKe?dU2d+DQ$G18r_v_%<7|TfOw=$6xj^6GBnnxx>tAkMz%1Myz#D5`NNd3FAKli zmv2I|ibH`vL|W$%iyMeIUV`cQ6ml)9I7|_lzHR4w;hAEv62l>WF>!y7pA5VMn2fe} zn-8N_f`Ln>sT&z4aR7kHz8!*ⓈXpqMYEuao^C_LzwEsK4IB{&thH`a|h&6)KMwQ z2U`sHNh1`E(8WGjVzi>V{$l)S5;y7w+4BlvP zXhTT0Dbnv_T0(^Q*g=k$BLb1qK=AP{;5}lc9ixpnF|;KS@fdnSk6MFfHV5hphHDmw z>?5H3cO&<3uUNZQOsxxK`7W~%`kCL5t|VEgX0cE)N*_66AkVAffE~hLqyLbyTGATF zEiJF-2lDqI+SuS@wi_Lj8&u2RKFUY}Lbi9Cab1Z}YoPiY!JkD=o&+d*OpxKxNM9rr z#3LraKwPrG2x72sc-)ss<93F=2-WKQT2gb~@fcGP0GGKsW^eK+O_e5KVBDRGL&n=9 zNUa(}z7s+S=LztK`CZ|OWjnx;!hxYvgOYJ)9Jn<@ht;wB{$6Ah2BkpA4>XfP577Z- zNX~3wM%Ci>i5(_MiX0(!5Y|5`nZ7`Vv?6ETz)Q)guJ3Cn7$De1mYUR4?D7Y#e31G= zv~(>oLO2AZei3Yp4v?M5@2h=1S&dLAHU`S?6zA&j1_VjpjlPs+ zKu!#L)`e;Gfz`7^E5eQLF_|+&0gZeEMpd2$qR5}`hSiGBG?uyHID1+#BIRwMVRC^5 zPqi0!Nz!2$qvXWD%j(vvF_;y|Y?xwVurGAOc^!5Eh!@6j0k(WK$}Tx-gs^FFQH=EI lk}>iyNyNlZc473-!(E)t3&HVUQSi?{et7gJeLoUk`u`R2MOpv= literal 0 HcmV?d00001 diff --git a/css/analysis.css b/css/analysis.css index dd2382d..fbeb10c 100644 --- a/css/analysis.css +++ b/css/analysis.css @@ -1,28 +1,33 @@ /* ── Player Grid ─────────────────────────────────────────────────────────── */ .player-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 6px; } .player-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); - padding: 10px 12px; + padding: 7px 9px; display: flex; align-items: center; - gap: 10px; + gap: 7px; + cursor: pointer; + transition: opacity 0.15s, border-color 0.15s; + user-select: none; } +.player-card:hover { border-color: var(--borderem); } +.player-card.player-hidden { opacity: 0.35; border-style: dashed; } .player-job-icon { - width: 34px; - height: 34px; + width: 28px; + height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 10px; + font-size: 9px; font-weight: 600; font-family: var(--font-d); letter-spacing: 0.03em; @@ -33,18 +38,33 @@ .player-job-icon.role-dps { background: rgba(224,92,92,.15); color: var(--red); border: 1px solid rgba(224,92,92,.35); } .player-name { - font-size: 13px; + font-size: 12px; color: var(--t1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .player-type { - font-size: 11px; + font-size: 10px; color: var(--t2); margin-top: 1px; } +.card-title-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} +.card-title-row .card-title { margin-bottom: 0; flex-shrink: 0; } + +.filter-input { + width: 200px; + padding: 4px 9px; + font-size: 12px; + margin-left: auto; +} + /* ── AoE Timeline ────────────────────────────────────────────────────────── */ .aoe-event { display: grid; @@ -89,28 +109,67 @@ .aoe-target { display: flex; - align-items: center; + align-items: flex-start; gap: 5px; background: var(--bg3); border: 1px solid var(--border); border-radius: var(--r); - padding: 2px 8px 2px 6px; + padding: 3px 8px 4px 6px; font-size: 11px; } +.aoe-target--dead { + border-color: rgba(224, 92, 92, 0.6); + background: rgba(224, 92, 92, 0.08); +} + .aoe-target-job { font-family: var(--font-d); font-size: 10px; font-weight: 600; letter-spacing: 0.02em; + padding-top: 2px; + flex-shrink: 0; } .aoe-target-job.role-tank { color: var(--blue); } .aoe-target-job.role-healer { color: var(--green); } .aoe-target-job.role-dps { color: var(--red); } +.aoe-target-body { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-width: 0; +} + +.aoe-target-row { + display: flex; + align-items: center; + gap: 5px; +} + .aoe-target-name { color: var(--t1); } .aoe-target-dmg { color: var(--t2); margin-left: 2px; } +/* ── HP Bar ──────────────────────────────────────────────────────────────── */ +.aoe-hp-bar { + height: 6px; + background: var(--bg0); + border-radius: 2px; + overflow: hidden; + display: flex; +} + +.aoe-hp-remaining { + height: 100%; +} + +.aoe-hp-damage { + height: 100%; + background: rgba(224, 92, 92, 0.65); +} + .aoe-target-buffs { display: flex; flex-wrap: wrap; diff --git a/css/components.css b/css/components.css index 6ac21d6..8843bd6 100644 --- a/css/components.css +++ b/css/components.css @@ -173,6 +173,30 @@ select option { background: var(--bg2); } .mitig-opt .opt-name { font-size: 12px; flex: 1; } .mitig-opt .opt-dr { font-size: 11px; color: var(--t2); } +/* ── Event Explorer ─────────────────────────────────────────────────────────── */ +.explorer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 10px; + margin-bottom: 12px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.form-group label { + font-size: 11px; + color: var(--t2); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.form-group input, +.form-group select { width: 100%; } + /* ── Terminal output ────────────────────────────────────────────────────────── */ .terminal { background: var(--bg0); diff --git a/js/analysis.js b/js/analysis.js index 5e0e325..2fe7fab 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -16,6 +16,7 @@ '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', 'Reprisal': 'assets/icons/mitigation/reprisal.png', 'Feint': 'assets/icons/mitigation/feint.png', 'Addle': 'assets/icons/mitigation/addle.png', @@ -48,13 +49,20 @@ return String(n); } + let hiddenPlayers = new Set(); + let lastEvents = []; + let lastFightStart = 0; + let playerFilter = ''; + function renderPlayers(players) { - const grid = document.getElementById('player-grid'); - const order = { tank: 0, healer: 1, dps: 2 }; + const grid = document.getElementById('player-grid'); + const order = { healer: 0, dps: 1, tank: 2 }; players.sort((a, b) => (order[a.role] ?? 2) - (order[b.role] ?? 2)); + hiddenPlayers = new Set(players.filter(p => p.role === 'tank').map(p => p.id)); + grid.innerHTML = players.map(p => ` -
+
${abbr(p.type)}
${p.name}
@@ -64,7 +72,29 @@ `).join(''); } + document.getElementById('player-grid').addEventListener('click', e => { + const card = e.target.closest('.player-card'); + if (!card) return; + const id = parseInt(card.dataset.playerId, 10); + if (hiddenPlayers.has(id)) { + hiddenPlayers.delete(id); + card.classList.remove('player-hidden'); + } else { + hiddenPlayers.add(id); + card.classList.add('player-hidden'); + } + renderTimeline(lastEvents, lastFightStart); + }); + + document.getElementById('player-filter').addEventListener('input', e => { + playerFilter = e.target.value.trim().toLowerCase(); + renderTimeline(lastEvents, lastFightStart); + }); + function renderTimeline(events, fightStart) { + lastEvents = events; + lastFightStart = fightStart; + const el = document.getElementById('aoe-timeline'); if (!events.length) { @@ -72,24 +102,48 @@ return; } - el.innerHTML = events.map(ev => { - const mitigIcons = (ev.mitigations ?? []).map(m => { - const iconSrc = MITIG_ICONS[m.name]; - if (!iconSrc) return ''; - const dr = m.dr > 0 ? ` −${m.dr}%` : ''; - return `${m.name}`; - }).join(''); + const rows = events.map(ev => { + const visibleTargets = ev.targets.filter(t => + !hiddenPlayers.has(t.id) && + (!playerFilter || t.name.toLowerCase().includes(playerFilter)) + ); + if (!visibleTargets.length) return ''; - const targets = ev.targets.map(t => ` + const targets = visibleTargets.map(t => { + const hpBar = (t.maxHp > 0) ? (() => { + 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 `
+
+
+
`; + })() : ''; + + const mitigIcons = (t.mitigations ?? []).map(m => { + const iconSrc = MITIG_ICONS[m.name]; + if (!iconSrc) return ''; + const dr = m.dr > 0 ? ` −${m.dr}%` : ''; + return `${m.name}`; + }).join(''); + + const dead = t.hp === 0 && t.maxHp > 0; + + return `
-
+
${abbr(t.type)} - ${t.name} - ${fmtDmg(t.amount)} +
+
+ ${t.name} + ${fmtDmg(t.amount)} +
+ ${hpBar} +
${mitigIcons ? `
${mitigIcons}
` : ''} -
- `).join(''); +
`; + }).join(''); return `
@@ -104,6 +158,8 @@
`; }).join(''); + + el.innerHTML = rows || '

Keine sichtbaren Targets

'; } function setEmpty(msg) { diff --git a/js/app.js b/js/app.js index 405a580..e3446e9 100644 --- a/js/app.js +++ b/js/app.js @@ -1,12 +1,16 @@ document.addEventListener('DOMContentLoaded', () => { window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0 }; - const form = document.getElementById('report-form'); - const output = document.getElementById('output'); - const outputCard = document.getElementById('output-card'); - const initialHint = document.getElementById('initial-hint'); - const fightSelectCard = document.getElementById('fight-select-card'); - const fightSelect = document.getElementById('fight-select'); + const form = document.getElementById('report-form'); + const output = document.getElementById('output'); + const outputCard = document.getElementById('output-card'); + const initialHint = document.getElementById('initial-hint'); + const fightSelectCard = document.getElementById('fight-select-card'); + const fightSelect = document.getElementById('fight-select'); + const explorerCard = document.getElementById('event-explorer-card'); + const exLoadBtn = document.getElementById('ex-load-btn'); + const exAbilitySelect = document.getElementById('ex-ability'); + const exPlayerSelect = document.getElementById('ex-player-name'); let allFights = []; @@ -20,8 +24,7 @@ document.addEventListener('DOMContentLoaded', () => { if (fight.kill) return 'Kill'; const pct = fight.fightPercentage; if (pct == null) return '?'; - // fightPercentage is 0–10000 (e.g. 5000 = 50.00%) - return (pct / 100).toFixed(2) + '%'; + return pct.toFixed(2) + '%'; } function displayFight(fight) { @@ -40,7 +43,78 @@ document.addEventListener('DOMContentLoaded', () => { window.App.fightEnd = fight.endTime; displayFight(fight); + explorerCard.style.display = 'block'; window.analysisTab?.onFightSelected?.(); + loadAbilities(id, fight.startTime, fight.endTime); + }); + + async function loadAbilities(fightId, startTime, endTime) { + exAbilitySelect.innerHTML = ''; + try { + const res = await fetch('api/abilities.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + report_code: window.App.reportCode, + fight_id: fightId, + start_time: startTime, + end_time: endTime, + }), + }); + const json = await res.json(); + + exAbilitySelect.innerHTML = ''; + (json.abilities ?? []).forEach(ab => { + const opt = document.createElement('option'); + opt.value = ab.id; + opt.textContent = ab.name; + exAbilitySelect.appendChild(opt); + }); + + exPlayerSelect.innerHTML = ''; + (json.players ?? []).forEach(p => { + const opt = document.createElement('option'); + opt.value = p.name; + opt.textContent = p.name; + exPlayerSelect.appendChild(opt); + }); + } catch { + exAbilitySelect.innerHTML = ''; + } + } + + exLoadBtn.addEventListener('click', async () => { + if (!window.App.fightId) return; + + output.textContent = '// loading events...'; + outputCard.style.display = 'block'; + + const params = { + report_code: window.App.reportCode, + fight_id: window.App.fightId, + start_time: window.App.fightStart, + end_time: window.App.fightEnd, + data_type: document.getElementById('ex-data-type').value, + ability_id: exAbilitySelect.value, + event_type: document.getElementById('ex-event-type').value.trim(), + player_name: document.getElementById('ex-player-name').value.trim(), + limit: document.getElementById('ex-limit').value, + start_offset: document.getElementById('ex-start-offset').value, + end_offset: document.getElementById('ex-end-offset').value, + }; + + try { + const res = await fetch('api/debug-events.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(params), + }); + const json = await res.json(); + if (json.reauth) { window.location.href = 'auth/start.php'; return; } + output.textContent = JSON.stringify(json, null, 2); + } catch (err) { + output.textContent = '// Fehler: ' + err.message; + } }); form.addEventListener('submit', async (e) => { @@ -49,8 +123,9 @@ document.addEventListener('DOMContentLoaded', () => { initialHint.style.display = 'none'; outputCard.style.display = 'block'; output.textContent = '// fetching...'; - fightSelectCard.style.display = 'none'; - fightSelect.innerHTML = ''; + fightSelectCard.style.display = 'none'; + explorerCard.style.display = 'none'; + fightSelect.innerHTML = ''; allFights = []; const reportCode = form.elements['report_code'].value.trim(); diff --git a/templates/event-explorer.php b/templates/event-explorer.php new file mode 100644 index 0000000..9419f8d --- /dev/null +++ b/templates/event-explorer.php @@ -0,0 +1,68 @@ + diff --git a/templates/tab-analysis.php b/templates/tab-analysis.php index 657bdc9..0c70464 100644 --- a/templates/tab-analysis.php +++ b/templates/tab-analysis.php @@ -16,7 +16,10 @@
-
AoE Timeline
+
+
AoE Timeline
+ +
diff --git a/templates/tab-report.php b/templates/tab-report.php index 0c2a796..211db8f 100644 --- a/templates/tab-report.php +++ b/templates/tab-report.php @@ -1,3 +1,4 @@ +