'Method not allowed']); 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); $language = strtolower(trim($_POST['language'] ?? 'en')); $language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en'; $translate = 'true'; if (!$reportCode || !$fightId || !$endTime) { http_response_code(400); echo json_encode(['error' => 'Missing required parameters']); exit; } $cacheParts = [$fightId, (int)$startTime, (int)$endTime]; $cached = read_cached_log('analysis', $reportCode, $language, $cacheParts); if ($cached !== null) { echo $cached; exit; } if (empty($_SESSION['access_token'])) { echo json_encode(['reauth' => true]); exit; } if (($_SESSION['token_expires'] ?? 0) <= time()) { echo json_encode(['reauth' => true]); exit; } $token = $_SESSION['access_token']; function localized_graphql_uri(string $language): string { $host = [ 'de' => 'de.fflogs.com', 'fr' => 'fr.fflogs.com', 'jp' => 'ja.fflogs.com', ][$language] ?? 'www.fflogs.com'; return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI); } function fflogs_gql(string $query): array { global $token, $language; $acceptLanguage = $language === 'jp' ? 'ja' : $language; $ch = curl_init(localized_graphql_uri($language)); 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, 'Accept-Language: ' . $acceptLanguage, ], 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, 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 = [ // ── Damage reduction buffs ────────────────────────────────────────────── 'Passage of Arms' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001175, 'extraAbilityGameID' => 7385], 'Dark Missionary' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001894, 'extraAbilityGameID' => 16471], 'Heart of Light' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001839, 'extraAbilityGameID' => 16160], 'Temperance' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001873, 'extraAbilityGameID' => 16536], 'Sacred Soil' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001944, 'extraAbilityGameID' => 188], 'Expedient' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002711, 'extraAbilityGameID' => 25868], // FFLogs: "Desperate Measures" 'Fey Illumination' => ['dr' => 5, 'buffType' => 'buff', 'statusId' => 1000317, 'extraAbilityGameID' => 16538], 'Collective Unconscious' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1000849, 'extraAbilityGameID' => 3613], 'Holos' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1003003, 'extraAbilityGameID' => 24310], 'Kerachole' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002618, 'extraAbilityGameID' => 24298], 'Troubadour' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001934, 'extraAbilityGameID' => 7405], 'Tactician' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001951, 'extraAbilityGameID' => 16889], 'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826, 'extraAbilityGameID' => 16012], 'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707, 'extraAbilityGameID' => 25857], // ── Personal / targeted mitigation ───────────────────────────────────── 'Rampart' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7531], // PLD 'Hallowed Ground' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 30], 'Sentinel' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 17], 'Bulwark' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 22], 'Holy Sheltron' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25746], 'Intervention' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7382], // WAR 'Holmgang' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 43], 'Vengeance' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 44], 'Damnation' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36923], 'Thrill of Battle' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 40], 'Raw Intuition' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3551], // DRK 'Living Dead' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3638], 'Shadow Wall' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3636], 'Shadowed Vigil' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36927], 'Dark Mind' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3634], 'The Blackest Night' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 7393], 'Oblation' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25754], // GNB 'Superbolide' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16152], 'Nebula' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16148], 'Great Nebula' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36935], 'Camouflage' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16140], 'Heart of Stone' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16161], 'Heart of Corundum' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25758], // DPS 'Riddle of Earth' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7394], 'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241], 'Third Eye' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7498], 'Arcane Crest' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 24404], 'Manaward' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 157], // ── Shields ───────────────────────────────────────────────────────────── // PLD 'Divine Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001362, 'extraAbilityGameID' => 3540], 'Guardian' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003830, 'extraAbilityGameID' => 36920], // FFLogs: "Guardian's Will" // WAR 'Shake It Off' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001457, 'extraAbilityGameID' => 7388], 'Bloodwhetting' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002678, 'extraAbilityGameID' => 25751], // WHM 'Divine Benison' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001218, 'extraAbilityGameID' => 7432], 'Divine Caress' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003903, 'extraAbilityGameID' => 37011], // AST 'Intersection' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001889, 'extraAbilityGameID' => 16556], 'Neutral Sect' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001921, 'extraAbilityGameID' => 16559], 'the Spire' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003892, 'extraAbilityGameID' => 37025], // FFLogs: "The Spire" // SGE 'Panhaima' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002613, 'extraAbilityGameID' => 24311], 'Holosakos' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003365, 'extraAbilityGameID' => 24310], 'Eukrasian Prognosis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002609, 'extraAbilityGameID' => 24292], 'Eukrasian Prognosis II' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 37034], 'Eukrasian Diagnosis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002607, 'extraAbilityGameID' => 24291], 'Differential Diagnosis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002608, 'extraAbilityGameID' => 24291], 'Haima' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002612, 'extraAbilityGameID' => 24305], // SCH 'Galvanize' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1000297, 'extraAbilityGameID' => 185], 'Seraphic Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001917, 'extraAbilityGameID' => 16548], 'Catalyze' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001918, 'extraAbilityGameID' => 185], // SMN 'Radiant Aegis' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002702, 'extraAbilityGameID' => 25799], // PCT 'Tempera Coat' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003686, 'extraAbilityGameID' => 34685], 'Tempera Grassa' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003687, 'extraAbilityGameID' => 34686], // DNC 'Improvised Finish' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002697, 'extraAbilityGameID' => 25789], // ── Boss debuffs ──────────────────────────────────────────────────────── 'Reprisal' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001193, 'extraAbilityGameID' => 7535], 'Feint' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001195, 'extraAbilityGameID' => 7549], 'Addle' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001203, 'extraAbilityGameID' => 7560], ]; function resolveMitigations(string $buffStr, array $mitigIdMap): array { if ($buffStr === '') return []; $result = []; $seen = []; foreach (explode('.', $buffStr) as $idStr) { $id = (int)$idStr; if (isset($mitigIdMap[$id])) { $name = $mitigIdMap[$id]['name']; if (isset($seen[$name])) continue; $seen[$name] = true; $result[] = [ 'key' => $mitigIdMap[$id]['key'] ?? $name, 'name' => $name, 'dr' => $mitigIdMap[$id]['dr'], 'buffType' => $mitigIdMap[$id]['buffType'], 'extraAbilityGameID' => $mitigIdMap[$id]['extraAbilityGameID'] ?? null, ]; } } return $result; } // Fallback for shields consumed by a hit: the damage event's buffs field no // longer contains the shield ID (already removed), but the applybuff/removebuff // timeline shows it was active just before the hit. function shieldsActiveAt(array $shieldTimeline, int $targetId, float $ts, array $mitigIdMap): array { $result = []; foreach ($shieldTimeline[$targetId] ?? [] as $statusId => $intervals) { foreach ($intervals as $iv) { // applied before hit, removed at or after hit (200ms buffer for event ordering) if ($iv['apply'] <= $ts && ($iv['remove'] === null || $iv['remove'] >= $ts - 200)) { if (isset($mitigIdMap[$statusId])) { $m = $mitigIdMap[$statusId]; $result[] = [ 'key' => $m['key'] ?? $m['name'], 'name' => $m['name'], 'dr' => $m['dr'], 'buffType' => $m['buffType'], 'extraAbilityGameID' => $m['extraAbilityGameID'] ?? null, ]; } break; } } } return $result; } // ── 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/statusID → display name $abilityNames = []; foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) { $abilityNames[(int)$ab['gameID']] = $ab['name']; } // 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(['key' => $name, 'name' => $name], MITIGATION_ABILITIES[$name]); } } foreach (MITIGATION_ABILITIES as $name => $meta) { if (isset($meta['statusId']) && !isset($mitigIdMap[$meta['statusId']])) { $displayName = $abilityNames[(int)$meta['statusId']] ?? $name; $mitigIdMap[$meta['statusId']] = array_merge(['key' => $name, 'name' => $displayName], $meta); } } // statusId set for tracked mitigations — used to resolve localized buff names // from Buffs events and to build the shield fallback timeline. $trackedStatusIds = []; foreach (MITIGATION_ABILITIES as $meta) { if (isset($meta['statusId'])) { $trackedStatusIds[$meta['statusId']] = true; } } // 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. 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; } // ── 2b. Shield buff/debuff timeline ──────────────────────────────────────── // Builds applybuff/removebuff intervals per target so we can detect shields // that were consumed by a hit (absent from the damage event's buffs snapshot). $shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...] $statusNames = []; // statusId → localized display name from Buffs events $statusActionIds = []; // statusId → applybuff extraAbilityGameID from FFLogs if (!empty($trackedStatusIds)) { $nextPage = $startTime; for ($page = 0; $page < 10; $page++) { $bfResult = fflogs_gql(<< true]); exit; } if (isset($bfResult['_curl_error'])) break; $bfEv = $bfResult['data']['reportData']['report']['events'] ?? []; foreach ($bfEv['data'] ?? [] as $ev) { $abId = (int)($ev['abilityGameID'] ?? 0); if (!isset($trackedStatusIds[$abId])) continue; $evName = $ev['ability']['name'] ?? null; if (is_string($evName) && $evName !== '') { $statusNames[$abId] = $evName; } $extraAbilityGameID = (int)($ev['extraAbilityGameID'] ?? 0); if ($extraAbilityGameID > 0) { $statusActionIds[$abId] = $extraAbilityGameID; } $tgtId = (int)($ev['targetID'] ?? 0); $ts = (float)($ev['timestamp'] ?? 0); $type = $ev['type'] ?? ''; $meta = $mitigIdMap[$abId] ?? null; if (($meta['buffType'] ?? null) !== 'shield') continue; if ($type === 'applybuff') { $shieldTimeline[$tgtId][$abId][] = ['apply' => $ts, 'remove' => null]; } elseif ($type === 'removebuff') { if (isset($shieldTimeline[$tgtId][$abId])) { for ($i = count($shieldTimeline[$tgtId][$abId]) - 1; $i >= 0; $i--) { if ($shieldTimeline[$tgtId][$abId][$i]['remove'] === null) { $shieldTimeline[$tgtId][$abId][$i]['remove'] = $ts; break; } } } } } $nextPage = $bfEv['nextPageTimestamp'] ?? null; if ($nextPage === null || $nextPage >= $endTime) break; } } foreach ($statusNames as $statusId => $displayName) { if (isset($mitigIdMap[$statusId])) { $mitigIdMap[$statusId]['name'] = $displayName; } } foreach ($statusActionIds as $statusId => $extraAbilityGameID) { if (isset($mitigIdMap[$statusId])) { $mitigIdMap[$statusId]['extraAbilityGameID'] = $extraAbilityGameID; } } $mitigationNames = []; foreach ($mitigIdMap as $meta) { $key = $meta['key'] ?? null; if ($key) { $mitigationNames[$key] = $meta['name'] ?? $key; } } // ── 3. AoE detection — proximity clustering ──────────────────────────────── // Group events by abilityId, then cluster by time proximity (≤ 1000ms from // the first event in the cluster) to avoid fixed-window boundary splits. const CLUSTER_WINDOW_MS = 1000; const HEAVY_TANKBUSTER_MIN_HP_RATIO = 0.33; $byAbility = []; // abilityId → [{ts, tgtId, amount, hp, maxHp, buffs, name}] foreach ($allEvents as $ev) { if (!empty($ev['tick'])) continue; if (($ev['type'] ?? '') !== 'damage') continue; $abId = (int)($ev['abilityGameID'] ?? 0); $tgtId = (int)($ev['targetID'] ?? 0); if (!$abId || !$tgtId || $abId <= 7) continue; $srcId = (int)($ev['sourceID'] ?? 0); if ($srcId > 0 && isset($players[$srcId])) continue; $byAbility[$abId][] = [ 'ts' => (float)($ev['timestamp'] ?? 0), 'tgtId' => $tgtId, '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'] ?? '', 'name' => $abilityNames[$abId] ?? $ev['ability']['name'] ?? ('Ability #' . $abId), ]; } $clusters = []; foreach ($byAbility as $abId => $events) { // Events from FFLogs are already time-sorted, but sort per ability to be safe usort($events, fn($a, $b) => $a['ts'] <=> $b['ts']); $clusterStart = null; $current = null; foreach ($events as $ev) { if ($current === null || ($ev['ts'] - $clusterStart) > CLUSTER_WINDOW_MS) { if ($current !== null) $clusters[] = $current; $clusterStart = $ev['ts']; $current = [ 'timestamp' => (int)$ev['ts'], 'abilityId' => $abId, 'abilityName' => $ev['name'], 'targets' => [], ]; } $tgtId = $ev['tgtId']; if (!isset($current['targets'][$tgtId])) { $current['targets'][$tgtId] = [ 'id' => $tgtId, 'ts' => $ev['ts'], 'amount' => 0, 'absorbed' => 0, 'unmitigatedAmount' => 0, 'mitigated' => 0, 'overkill' => 0, 'hp' => $ev['hp'], 'maxHp' => $ev['maxHp'], 'buffs' => $ev['buffs'], ]; } $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; } $bossEvents = []; $aoeEvents = []; foreach ($clusters as $group) { $targetCount = count($group['targets']); $isHeavyTankbuster = false; if ($targetCount < 3) { foreach ($group['targets'] as $tgtId => $tgt) { $p = $players[$tgtId] ?? null; if (($p['role'] ?? null) !== 'tank') continue; $tankMaxHp = (int)($tgt['maxHp'] ?? 0); $rawDamage = max( (int)($tgt['unmitigatedAmount'] ?? 0), (int)($tgt['amount'] ?? 0) + (int)($tgt['absorbed'] ?? 0) + (int)($tgt['mitigated'] ?? 0) ); if ($tankMaxHp > 0 && $rawDamage >= $tankMaxHp * HEAVY_TANKBUSTER_MIN_HP_RATIO) { $isHeavyTankbuster = true; break; } } } $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'], 'absorbed' => $tgt['absorbed'], 'unmitigatedAmount' => $tgt['unmitigatedAmount'], 'mitigated' => $tgt['mitigated'], 'overkill' => $tgt['overkill'], 'hp' => $tgt['hp'], 'maxHp' => $tgt['maxHp'], 'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline) { $mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap); if ($tgt['absorbed'] > 0 && !empty($shieldTimeline)) { $existing = []; foreach ($mitigations as $m) { if ($m['buffType'] === 'shield') $existing[$m['name']] = true; } foreach (shieldsActiveAt($shieldTimeline, $tgt['id'], $tgt['ts'], $mitigIdMap) as $s) { if (!isset($existing[$s['name']])) $mitigations[] = $s; } } return $mitigations; })(), ]; } $roleOrder = ['healer' => 0, 'dps' => 1, 'tank' => 2]; usort($targets, function($a, $b) use ($roleOrder) { $roleCmp = ($roleOrder[$a['role']] ?? 1) <=> ($roleOrder[$b['role']] ?? 1); return $roleCmp !== 0 ? $roleCmp : strcmp($a['name'], $b['name']); }); $bossEvent = [ 'timestamp' => $group['timestamp'], 'abilityId' => $group['abilityId'], 'abilityName' => $group['abilityName'], 'targets' => $targets, 'totalDamage' => array_sum(array_column($targets, 'amount')), 'isHeavyTankbuster' => $isHeavyTankbuster, ]; $bossEvents[] = $bossEvent; if ($targetCount < 3 && !$isHeavyTankbuster) continue; $aoeEvents[] = $bossEvent; } usort($bossEvents, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']); usort($aoeEvents, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']); $response = json_encode([ 'players' => array_values($players), 'boss_events' => $bossEvents, 'aoe_events' => $aoeEvents, 'fight_start' => (int)$startTime, 'mitigation_names' => $mitigationNames, ]); if ($response === false) { http_response_code(500); echo json_encode(['error' => 'Could not encode analysis response']); exit; } write_cached_log('analysis', $reportCode, $language, $cacheParts, $response); echo $response;