From 636a65965a511c2d98287f59c09b606800a93b0a Mon Sep 17 00:00:00 2001 From: xziino Date: Sun, 24 May 2026 15:22:55 +0200 Subject: [PATCH] Analyse+Planer: sourceID fuer Reprisal/Feint/Addle via Cast-Events - Neuer Abschnitt 2c in analysis.php: Cast-Events fuer Reprisal/Feint/Addle per 1 GQL-Request (3 Aliase) abfragen, da dataType:Buffs nur Friendly- Targets liefert (Boss-Debuffs fehlen dort) - buffSourceTimeline mit Cast-Timestamps + 10s Dauer befuellt - findBuffSourcePlayer(): sucht aktiven Caster zum Schadens-Zeitpunkt - resolveMitigations(): gibt sourcePlayerType aus buffSourceTimeline zurueck - guessJob() in planner.js: sourcePlayerType als erste Prioritaet vor job-basiertem Fallback -> DRK Reprisal, MNK Feint etc. korrekt zugeordnet - analysis.js: Debuff-Icons im Header zeigen Job im Tooltip (z.B. DRK - Reprisal) - Cache v5 -> v6 Co-Authored-By: Claude Sonnet 4.6 --- api/analysis.php | 89 ++++++++++++++++++++++++++++++++++++++++++++---- api/cache.php | 2 +- js/analysis.js | 8 +++-- js/planner.js | 8 +++-- 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/api/analysis.php b/api/analysis.php index 805415f..088e3a2 100644 --- a/api/analysis.php +++ b/api/analysis.php @@ -172,7 +172,7 @@ const MITIGATION_ABILITIES = [ 'Addle' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001203, 'extraAbilityGameID' => 7560], ]; -function resolveMitigations(string $buffStr, array $mitigIdMap): array { +function resolveMitigations(string $buffStr, array $mitigIdMap, array $buffSourceTimeline = [], array $players = [], float $ts = 0): array { if ($buffStr === '') return []; $result = []; $seen = []; @@ -182,18 +182,33 @@ function resolveMitigations(string $buffStr, array $mitigIdMap): array { $name = $mitigIdMap[$id]['name']; if (isset($seen[$name])) continue; $seen[$name] = true; - $result[] = [ + $entry = [ 'key' => $mitigIdMap[$id]['key'] ?? $name, 'name' => $name, 'dr' => $mitigIdMap[$id]['dr'], 'buffType' => $mitigIdMap[$id]['buffType'], 'extraAbilityGameID' => $mitigIdMap[$id]['extraAbilityGameID'] ?? null, ]; + $source = findBuffSourcePlayer($buffSourceTimeline, $id, $ts, $players); + if ($source) $entry['sourcePlayerType'] = $source['type']; + $result[] = $entry; } } return $result; } +// Findet den Spieler der einen Buff zum Zeitpunkt $ts gecastet hat (anhand der applybuff-Timeline). +function findBuffSourcePlayer(array $sourceTimeline, int $statusId, float $ts, array $players): ?array { + $best = null; + foreach ($sourceTimeline[$statusId] ?? [] as $entry) { + if ($entry['apply'] > $ts + 200) continue; // noch nicht aktiv + if ($entry['remove'] !== null && $entry['remove'] < $ts - 200) continue; // schon abgelaufen + if ($best === null || $entry['apply'] > $best['apply']) $best = $entry; + } + if ($best === null || empty($best['sourceId'])) return null; + return $players[$best['sourceId']] ?? null; +} + // 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. @@ -326,9 +341,10 @@ for ($page = 0; $page < 10; $page++) { // ── 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 +$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...] +$buffSourceTimeline = []; // statusId → [[apply, remove|null, sourceId], ...] — wer hat den Buff gecastet? +$statusNames = []; // statusId → localized display name from Buffs events +$statusActionIds = []; // statusId → applybuff extraAbilityGameID from FFLogs if (!empty($trackedStatusIds)) { $nextPage = $startTime; @@ -373,6 +389,21 @@ if (!empty($trackedStatusIds)) { $type = $ev['type'] ?? ''; $meta = $mitigIdMap[$abId] ?? null; + // Source-Tracking für alle getrackten Abilities (unabhängig von buffType) + $srcId = (int)($ev['sourceID'] ?? 0); + if ($srcId > 0 && isset($players[$srcId])) { + if ($type === 'applybuff') { + $buffSourceTimeline[$abId][] = ['apply' => $ts, 'remove' => null, 'sourceId' => $srcId]; + } elseif ($type === 'removebuff') { + for ($i = count($buffSourceTimeline[$abId] ?? []) - 1; $i >= 0; $i--) { + if ($buffSourceTimeline[$abId][$i]['remove'] === null) { + $buffSourceTimeline[$abId][$i]['remove'] = $ts; + break; + } + } + } + } + if (($meta['buffType'] ?? null) !== 'shield') continue; if ($type === 'applybuff') { @@ -394,6 +425,50 @@ if (!empty($trackedStatusIds)) { } } +// ── 2c. Boss-Debuff-Source via Casts ─────────────────────────────────────── +// dataType: Buffs liefert nur Events auf Spieler (Friendly). Reprisal/Feint/Addle +// werden auf den Boss (Hostile) angewendet und tauchen dort nicht auf. +// Lösung: Cast-Events der drei Abilities direkt abfragen — 1 GQL-Request, 3 Aliase. +$dbReprisalActionId = (int)(MITIGATION_ABILITIES['Reprisal']['extraAbilityGameID'] ?? 0); +$dbFeintActionId = (int)(MITIGATION_ABILITIES['Feint']['extraAbilityGameID'] ?? 0); +$dbAddleActionId = (int)(MITIGATION_ABILITIES['Addle']['extraAbilityGameID'] ?? 0); +$dbReprisalStatusId = (int)(MITIGATION_ABILITIES['Reprisal']['statusId'] ?? 0); +$dbFeintStatusId = (int)(MITIGATION_ABILITIES['Feint']['statusId'] ?? 0); +$dbAddleStatusId = (int)(MITIGATION_ABILITIES['Addle']['statusId'] ?? 0); + +if ($dbReprisalActionId && $dbFeintActionId && $dbAddleActionId) { + $dbResult = fflogs_gql(<< true]); exit; } + + foreach ([ + 'reprisal' => ['statusId' => $dbReprisalStatusId, 'durationMs' => 10000], + 'feint' => ['statusId' => $dbFeintStatusId, 'durationMs' => 10000], + 'addle' => ['statusId' => $dbAddleStatusId, 'durationMs' => 10000], + ] as $alias => $info) { + foreach ($dbResult['data']['reportData']['report'][$alias]['data'] ?? [] as $ev) { + if (($ev['type'] ?? '') !== 'cast') continue; + $srcId = (int)($ev['sourceID'] ?? 0); + if ($srcId <= 0 || !isset($players[$srcId]) || !$info['statusId']) continue; + $ts = (float)($ev['timestamp'] ?? 0); + $buffSourceTimeline[$info['statusId']][] = [ + 'apply' => $ts, + 'remove' => $ts + $info['durationMs'], + 'sourceId' => $srcId, + ]; + } + } +} + foreach ($statusNames as $statusId => $displayName) { if (isset($mitigIdMap[$statusId])) { $mitigIdMap[$statusId]['name'] = $displayName; @@ -527,8 +602,8 @@ foreach ($clusters as $group) { 'overkill' => $tgt['overkill'], 'hp' => $tgt['hp'], 'maxHp' => $tgt['maxHp'], - 'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline) { - $mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap); + 'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline, $buffSourceTimeline, $players) { + $mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap, $buffSourceTimeline, $players, $tgt['ts']); if ($tgt['absorbed'] > 0 && !empty($shieldTimeline)) { $existing = []; foreach ($mitigations as $m) { diff --git a/api/cache.php b/api/cache.php index 1ad39d3..cd8c630 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 = 'v4'; +const CACHED_LOG_VERSION = 'v6'; function cache_language(string $language): string { $language = strtolower(trim($language)); diff --git a/js/analysis.js b/js/analysis.js index b1868d7..87fcc93 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -605,10 +605,12 @@ ].map(m => { const iconSrc = mitigationIcon(m); if (!iconSrc) return ''; - const dr = m.dr > 0 ? ` −${m.dr}%` : ''; + const dr = m.dr > 0 ? ` −${m.dr}%` : ''; + const jobAbbr = m.sourcePlayerType ? (JOB_ABBR[m.sourcePlayerType] ?? '') : ''; + const label = jobAbbr ? `${jobAbbr} · ${m.name}` : m.name; return m.missing - ? `${m.name}` - : `${m.name}`; + ? `${m.name}` + : `${m.name}`; }).join(''); // Current targets diff --git a/js/planner.js b/js/planner.js index 2d8e58b..1988e6e 100644 --- a/js/planner.js +++ b/js/planner.js @@ -2238,7 +2238,11 @@ function sortedAssignments(assignments) { ); } -function guessJob(abilityName, players) { +function guessJob(abilityName, players, mitigation = null) { + // Direkte Zuordnung über sourcePlayerType aus dem applybuff-Event (zuverlässigste Quelle) + if (mitigation?.sourcePlayerType) { + return JOB_FROM_TYPE[mitigation.sourcePlayerType] ?? ''; + } if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName]; const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? ''); if (abilityName === 'Reprisal') { @@ -2293,7 +2297,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga ability: key, abilityName: mitigationDisplayName(m) || mitigationNames[key], actionId: m.extraAbilityGameID ?? null, - job: guessJob(key, players), + job: guessJob(key, players, m), buffType: m.buffType ?? '', timestamp: Math.max(0, relTs + IMPORT_OFFSET_MS), });