'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 required parameters']); exit; } $token = $_SESSION['access_token']; function fflogs_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); $err = curl_error($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($err) return ['_curl_error' => $err]; if ($code === 401) return ['_reauth' => true]; return json_decode($body, true) ?? ['_parse_error' => true]; } // ── Party-wide mitigation + boss debuffs to track ───────────────────────── // Barriers (dr = 0) are shown without a percentage. 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'], 'Reprisal' => ['dr' => 10, 'buffType' => 'debuff'], 'Feint' => ['dr' => 10, 'buffType' => 'debuff'], 'Addle' => ['dr' => 10, 'buffType' => 'debuff'], ]; // ── 1. Player details + masterData (ability names) ───────────────────────── $pdResult = fflogs_gql(<< true]); exit; } if (isset($pdResult['_curl_error'])) { http_response_code(502); echo json_encode(['error' => $pdResult['_curl_error']]); exit; } // abilityGameID → display name $abilityNames = []; foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) { $abilityNames[(int)$ab['gameID']] = $ab['name']; } // gameID → mitigation meta (only for abilities present in this report) $mitigIdMap = []; foreach ($abilityNames as $gameId => $name) { if (isset(MITIGATION_ABILITIES[$name])) { $mitigIdMap[$gameId] = array_merge(['name' => $name], MITIGATION_ABILITIES[$name]); } } // player actorID → player data $pdRaw = $pdResult['data']['reportData']['report']['playerDetails'] ?? null; $pdParsed = is_string($pdRaw) ? json_decode($pdRaw, true) : $pdRaw; $pdGroups = $pdParsed['data']['playerDetails'] ?? []; $players = []; $roleMap = ['tanks' => 'tank', 'healers' => 'healer', 'dps' => 'dps']; foreach ($roleMap as $group => $role) { foreach ($pdGroups[$group] ?? [] as $p) { $players[(int)$p['id']] = [ 'id' => (int)$p['id'], 'name' => $p['name'], 'type' => $p['type'] ?? '', 'role' => $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) ───────────────────────────────────── $allEvents = []; $nextPage = $startTime; for ($page = 0; $page < 10; $page++) { $evResult = fflogs_gql(<< true]); exit; } if (isset($evResult['_curl_error'])) break; $ev = $evResult['data']['reportData']['report']['events'] ?? []; $allEvents = array_merge($allEvents, $ev['data'] ?? []); $nextPage = $ev['nextPageTimestamp'] ?? null; if ($nextPage === null || $nextPage >= $endTime) break; } // ── 4. AoE detection ─────────────────────────────────────────────────────── $buckets = []; foreach ($allEvents as $ev) { if (!empty($ev['tick'])) continue; if (($ev['type'] ?? '') !== 'calculateddamage') 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; $key = $bucket . '_' . $abId; if (!isset($buckets[$key])) { $buckets[$key] = [ 'timestamp' => (int)$ts, 'abilityId' => $abId, 'abilityName' => $abilityNames[$abId] ?? $ev['ability']['name'] ?? ('Ability #' . $abId), 'targets' => [], ]; } if (!isset($buckets[$key]['targets'][$tgtId])) { $buckets[$key]['targets'][$tgtId] = ['id' => $tgtId, 'amount' => 0]; } $buckets[$key]['targets'][$tgtId]['amount'] += (int)($ev['amount'] ?? 0); } $aoeEvents = []; foreach ($buckets as $group) { if (count($group['targets']) < 3) continue; $targets = []; 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'], ]; } 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'], ]; } } } $aoeEvents[] = [ 'timestamp' => $evTs, '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']); echo json_encode([ 'players' => array_values($players), 'aoe_events' => $aoeEvents, 'fight_start' => (int)$startTime, ]);