diff --git a/api/analysis.php b/api/analysis.php index 67d7616..9aa9802 100644 --- a/api/analysis.php +++ b/api/analysis.php @@ -455,6 +455,7 @@ foreach ($byAbility as $abId => $events) { if ($current !== null) $clusters[] = $current; } +$bossEvents = []; $aoeEvents = []; foreach ($clusters as $group) { $targetCount = count($group['targets']); @@ -476,8 +477,6 @@ foreach ($clusters as $group) { } } - if ($targetCount < 3 && !$isHeavyTankbuster) continue; - $targets = []; foreach ($group['targets'] as $tgtId => $tgt) { $p = $players[$tgtId] ?? null; @@ -514,7 +513,7 @@ foreach ($clusters as $group) { return $roleCmp !== 0 ? $roleCmp : strcmp($a['name'], $b['name']); }); - $aoeEvents[] = [ + $bossEvent = [ 'timestamp' => $group['timestamp'], 'abilityId' => $group['abilityId'], 'abilityName' => $group['abilityName'], @@ -522,11 +521,18 @@ foreach ($clusters as $group) { '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, diff --git a/api/cache.php b/api/cache.php index 8aeade9..e77fa93 100644 --- a/api/cache.php +++ b/api/cache.php @@ -2,7 +2,7 @@ declare(strict_types=1); const CACHED_LOG_DIR = __DIR__ . '/../cached_logs'; -const CACHED_LOG_VERSION = 'v2'; +const CACHED_LOG_VERSION = 'v3'; function cache_language(string $language): string { $language = strtolower(trim($language)); diff --git a/css/planner.css b/css/planner.css index 1dcb3ec..c590c8b 100644 --- a/css/planner.css +++ b/css/planner.css @@ -707,6 +707,39 @@ .timeline-hint { font-size: 12px; color: var(--t3); + margin-bottom: 8px; +} + +.timeline-controls { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.timeline-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border: 1px solid var(--border); + border-radius: var(--r); + background: rgba(255,255,255,.025); + color: var(--t2); + font-size: 12px; + cursor: pointer; + user-select: none; +} + +.timeline-toggle:hover { + border-color: var(--borderem); + color: var(--t1); +} + +.timeline-toggle input { + width: auto !important; + min-width: 0 !important; + margin: 0; } .timeline-empty, @@ -772,6 +805,20 @@ z-index: 9; } +.timeline-boss-label { + border-top: none; + border-left: none; + border-radius: 0; + cursor: pointer; + font: inherit; + text-align: left; + width: 100%; +} + +.timeline-boss-label:hover { + background: rgba(200,168,75,.08); +} + .timeline-track, .timeline-axis-track { position: relative; @@ -1092,6 +1139,8 @@ z-index: 300; min-width: 190px; max-width: 280px; + max-height: min(520px, calc(100vh - 24px)); + overflow-y: auto; padding: 5px; border: 1px solid var(--borderem); border-radius: var(--r); @@ -1125,6 +1174,23 @@ cursor: not-allowed; } +.timeline-menu-header:disabled { + opacity: 1; + padding-top: 9px; + padding-bottom: 4px; + color: var(--gold); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + cursor: default; +} + +.timeline-menu-header:hover { + background: transparent; + color: var(--gold); +} + .timeline-menu-item img { width: 18px; height: 18px; diff --git a/js/planner.js b/js/planner.js index 5e0f560..73e3734 100644 --- a/js/planner.js +++ b/js/planner.js @@ -41,6 +41,7 @@ function createPlan(name) { updatedAt: Date.now(), source: null, mitigationNames: {}, + timelineOptions: { includeShields: false, includePersonal: false }, folderId: null, jobComposition: Array(8).fill(''), mechanics: [] @@ -177,7 +178,8 @@ async function ensureActionMetaLoaded() { function sameMechanic(existing, incoming, source) { const fightStart = source?.fightStart ?? 0; - const incomingRel = incoming.timestamp - fightStart; + const incomingTs = Number(incoming.timestamp); + const incomingRel = incomingTs >= fightStart ? incomingTs - fightStart : incomingTs; if (existing.abilityId && incoming.abilityId && existing.abilityId === incoming.abilityId) { return Math.abs(existing.timestamp - incomingRel) < 1500; } @@ -391,8 +393,18 @@ function renderPlanDetail(plan) {