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 <noreply@anthropic.com>
This commit is contained in:
parent
a9b3cc8666
commit
636a65965a
@ -172,7 +172,7 @@ const MITIGATION_ABILITIES = [
|
|||||||
'Addle' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001203, 'extraAbilityGameID' => 7560],
|
'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 [];
|
if ($buffStr === '') return [];
|
||||||
$result = [];
|
$result = [];
|
||||||
$seen = [];
|
$seen = [];
|
||||||
@ -182,18 +182,33 @@ function resolveMitigations(string $buffStr, array $mitigIdMap): array {
|
|||||||
$name = $mitigIdMap[$id]['name'];
|
$name = $mitigIdMap[$id]['name'];
|
||||||
if (isset($seen[$name])) continue;
|
if (isset($seen[$name])) continue;
|
||||||
$seen[$name] = true;
|
$seen[$name] = true;
|
||||||
$result[] = [
|
$entry = [
|
||||||
'key' => $mitigIdMap[$id]['key'] ?? $name,
|
'key' => $mitigIdMap[$id]['key'] ?? $name,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'dr' => $mitigIdMap[$id]['dr'],
|
'dr' => $mitigIdMap[$id]['dr'],
|
||||||
'buffType' => $mitigIdMap[$id]['buffType'],
|
'buffType' => $mitigIdMap[$id]['buffType'],
|
||||||
'extraAbilityGameID' => $mitigIdMap[$id]['extraAbilityGameID'] ?? null,
|
'extraAbilityGameID' => $mitigIdMap[$id]['extraAbilityGameID'] ?? null,
|
||||||
];
|
];
|
||||||
|
$source = findBuffSourcePlayer($buffSourceTimeline, $id, $ts, $players);
|
||||||
|
if ($source) $entry['sourcePlayerType'] = $source['type'];
|
||||||
|
$result[] = $entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $result;
|
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
|
// 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
|
// longer contains the shield ID (already removed), but the applybuff/removebuff
|
||||||
// timeline shows it was active just before the hit.
|
// timeline shows it was active just before the hit.
|
||||||
@ -326,9 +341,10 @@ for ($page = 0; $page < 10; $page++) {
|
|||||||
// ── 2b. Shield buff/debuff timeline ────────────────────────────────────────
|
// ── 2b. Shield buff/debuff timeline ────────────────────────────────────────
|
||||||
// Builds applybuff/removebuff intervals per target so we can detect shields
|
// 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).
|
// that were consumed by a hit (absent from the damage event's buffs snapshot).
|
||||||
$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...]
|
$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...]
|
||||||
$statusNames = []; // statusId → localized display name from Buffs events
|
$buffSourceTimeline = []; // statusId → [[apply, remove|null, sourceId], ...] — wer hat den Buff gecastet?
|
||||||
$statusActionIds = []; // statusId → applybuff extraAbilityGameID from FFLogs
|
$statusNames = []; // statusId → localized display name from Buffs events
|
||||||
|
$statusActionIds = []; // statusId → applybuff extraAbilityGameID from FFLogs
|
||||||
|
|
||||||
if (!empty($trackedStatusIds)) {
|
if (!empty($trackedStatusIds)) {
|
||||||
$nextPage = $startTime;
|
$nextPage = $startTime;
|
||||||
@ -373,6 +389,21 @@ if (!empty($trackedStatusIds)) {
|
|||||||
$type = $ev['type'] ?? '';
|
$type = $ev['type'] ?? '';
|
||||||
$meta = $mitigIdMap[$abId] ?? null;
|
$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 (($meta['buffType'] ?? null) !== 'shield') continue;
|
||||||
|
|
||||||
if ($type === 'applybuff') {
|
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(<<<GQL
|
||||||
|
{
|
||||||
|
reportData {
|
||||||
|
report(code: "$reportCode") {
|
||||||
|
reprisal: events(fightIDs: [$fightId], dataType: Casts, abilityID: $dbReprisalActionId, startTime: $startTime, endTime: $endTime) { data }
|
||||||
|
feint: events(fightIDs: [$fightId], dataType: Casts, abilityID: $dbFeintActionId, startTime: $startTime, endTime: $endTime) { data }
|
||||||
|
addle: events(fightIDs: [$fightId], dataType: Casts, abilityID: $dbAddleActionId, startTime: $startTime, endTime: $endTime) { data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GQL);
|
||||||
|
if (isset($dbResult['_reauth'])) { echo json_encode(['reauth' => 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) {
|
foreach ($statusNames as $statusId => $displayName) {
|
||||||
if (isset($mitigIdMap[$statusId])) {
|
if (isset($mitigIdMap[$statusId])) {
|
||||||
$mitigIdMap[$statusId]['name'] = $displayName;
|
$mitigIdMap[$statusId]['name'] = $displayName;
|
||||||
@ -527,8 +602,8 @@ foreach ($clusters as $group) {
|
|||||||
'overkill' => $tgt['overkill'],
|
'overkill' => $tgt['overkill'],
|
||||||
'hp' => $tgt['hp'],
|
'hp' => $tgt['hp'],
|
||||||
'maxHp' => $tgt['maxHp'],
|
'maxHp' => $tgt['maxHp'],
|
||||||
'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline) {
|
'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline, $buffSourceTimeline, $players) {
|
||||||
$mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap);
|
$mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap, $buffSourceTimeline, $players, $tgt['ts']);
|
||||||
if ($tgt['absorbed'] > 0 && !empty($shieldTimeline)) {
|
if ($tgt['absorbed'] > 0 && !empty($shieldTimeline)) {
|
||||||
$existing = [];
|
$existing = [];
|
||||||
foreach ($mitigations as $m) {
|
foreach ($mitigations as $m) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
const CACHED_LOG_DIR = __DIR__ . '/../cached_logs';
|
const CACHED_LOG_DIR = __DIR__ . '/../cached_logs';
|
||||||
const CACHED_LOG_VERSION = 'v4';
|
const CACHED_LOG_VERSION = 'v6';
|
||||||
|
|
||||||
function cache_language(string $language): string {
|
function cache_language(string $language): string {
|
||||||
$language = strtolower(trim($language));
|
$language = strtolower(trim($language));
|
||||||
|
|||||||
@ -605,10 +605,12 @@
|
|||||||
].map(m => {
|
].map(m => {
|
||||||
const iconSrc = mitigationIcon(m);
|
const iconSrc = mitigationIcon(m);
|
||||||
if (!iconSrc) return '';
|
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
|
return m.missing
|
||||||
? `<img class="aoe-target-buff-icon aoe-buff-missing" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr} fehlt (war im Referenz-Pull aktiv)">`
|
? `<img class="aoe-target-buff-icon aoe-buff-missing" src="${iconSrc}" alt="${m.name}" title="${label}${dr} fehlt (war im Referenz-Pull aktiv)">`
|
||||||
: `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
|
: `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${label}${dr}">`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Current targets
|
// Current targets
|
||||||
|
|||||||
@ -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];
|
if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName];
|
||||||
const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? '');
|
const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? '');
|
||||||
if (abilityName === 'Reprisal') {
|
if (abilityName === 'Reprisal') {
|
||||||
@ -2293,7 +2297,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
|||||||
ability: key,
|
ability: key,
|
||||||
abilityName: mitigationDisplayName(m) || mitigationNames[key],
|
abilityName: mitigationDisplayName(m) || mitigationNames[key],
|
||||||
actionId: m.extraAbilityGameID ?? null,
|
actionId: m.extraAbilityGameID ?? null,
|
||||||
job: guessJob(key, players),
|
job: guessJob(key, players, m),
|
||||||
buffType: m.buffType ?? '',
|
buffType: m.buffType ?? '',
|
||||||
timestamp: Math.max(0, relTs + IMPORT_OFFSET_MS),
|
timestamp: Math.max(0, relTs + IMPORT_OFFSET_MS),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user