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 0000000..4a6026e Binary files /dev/null and b/assets/icons/mitigation/magick-barrier.png differ 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 @@ +