Compare commits
No commits in common. "fce55c5bb656de1d814b8ea249b10a8c988b63c0" and "7eeeb5ef560c05846a885a25bb9233f2d3507293" have entirely different histories.
fce55c5bb6
...
7eeeb5ef56
100
api/analysis.php
@ -88,12 +88,10 @@ const MITIGATION_ABILITIES = [
|
||||
'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],
|
||||
'Aquaveil' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1002708, 'extraAbilityGameID' => 25861], // Personal, WHM auf Ziel
|
||||
'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],
|
||||
'Exaltation' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002717, 'extraAbilityGameID' => 25873], // Personal, AST auf Ziel
|
||||
'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],
|
||||
@ -108,15 +106,12 @@ const MITIGATION_ABILITIES = [
|
||||
'Bulwark' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 22],
|
||||
'Holy Sheltron' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 25746],
|
||||
'Intervention' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 7382],
|
||||
'Knight\'s Resolve' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002675, 'extraAbilityGameID' => 7382], // Proc von Intervention
|
||||
// WAR
|
||||
'Holmgang' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 43],
|
||||
'Vengeance' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 44],
|
||||
'Damnation' => ['dr' => 40, 'buffType' => 'buff', 'extraAbilityGameID' => 36923],
|
||||
'Thrill of Battle' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 40],
|
||||
'Raw Intuition' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 3551],
|
||||
'Nascent Glint' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1001858, 'extraAbilityGameID' => 16464], // Proc von Nascent Flash auf Ziel
|
||||
'Stem the Flow' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002679, 'extraAbilityGameID' => 25751], // Proc von Bloodwhetting / Nascent Flash
|
||||
// DRK
|
||||
'Living Dead' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3638],
|
||||
'Shadow Wall' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 3636],
|
||||
@ -127,11 +122,10 @@ const MITIGATION_ABILITIES = [
|
||||
// GNB
|
||||
'Superbolide' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 16152],
|
||||
'Nebula' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 16148],
|
||||
'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'statusId' => 1003838, 'extraAbilityGameID' => 36935],
|
||||
'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'extraAbilityGameID' => 36935],
|
||||
'Camouflage' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 16140],
|
||||
'Heart of Stone' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 16161],
|
||||
'Heart of Corundum' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1002683, 'extraAbilityGameID' => 25758],
|
||||
'Clarity of Corundum' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1002684, 'extraAbilityGameID' => 25758], // Proc von Heart of Corundum, kann beliebiges Partymitglied treffen
|
||||
'Heart of Corundum' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 25758],
|
||||
// DPS
|
||||
'Riddle of Earth' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7394],
|
||||
'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241],
|
||||
@ -145,7 +139,6 @@ const MITIGATION_ABILITIES = [
|
||||
// WAR
|
||||
'Shake It Off' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001457, 'extraAbilityGameID' => 7388],
|
||||
'Bloodwhetting' => ['dr' => 10, 'buffType' => 'shield', 'statusId' => 1002678, 'extraAbilityGameID' => 25751],
|
||||
'Stem the Tide' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002680, 'extraAbilityGameID' => 25751], // Proc von Bloodwhetting / Nascent Flash
|
||||
// WHM
|
||||
'Divine Benison' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001218, 'extraAbilityGameID' => 7432],
|
||||
'Divine Caress' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003903, 'extraAbilityGameID' => 37011],
|
||||
@ -178,7 +171,7 @@ const MITIGATION_ABILITIES = [
|
||||
'Addle' => ['dr' => 10, 'buffType' => 'debuff', 'statusId' => 1001203, 'extraAbilityGameID' => 7560],
|
||||
];
|
||||
|
||||
function resolveMitigations(string $buffStr, array $mitigIdMap, array $buffSourceTimeline = [], array $players = [], float $ts = 0): array {
|
||||
function resolveMitigations(string $buffStr, array $mitigIdMap): array {
|
||||
if ($buffStr === '') return [];
|
||||
$result = [];
|
||||
$seen = [];
|
||||
@ -188,33 +181,18 @@ function resolveMitigations(string $buffStr, array $mitigIdMap, array $buffSourc
|
||||
$name = $mitigIdMap[$id]['name'];
|
||||
if (isset($seen[$name])) continue;
|
||||
$seen[$name] = true;
|
||||
$entry = [
|
||||
$result[] = [
|
||||
'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.
|
||||
@ -347,10 +325,9 @@ 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], ...]
|
||||
$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
|
||||
$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;
|
||||
@ -395,21 +372,6 @@ 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') {
|
||||
@ -431,50 +393,6 @@ 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) {
|
||||
if (isset($mitigIdMap[$statusId])) {
|
||||
$mitigIdMap[$statusId]['name'] = $displayName;
|
||||
@ -608,8 +526,8 @@ foreach ($clusters as $group) {
|
||||
'overkill' => $tgt['overkill'],
|
||||
'hp' => $tgt['hp'],
|
||||
'maxHp' => $tgt['maxHp'],
|
||||
'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline, $buffSourceTimeline, $players) {
|
||||
$mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap, $buffSourceTimeline, $players, $tgt['ts']);
|
||||
'mitigations' => (function() use ($tgt, $mitigIdMap, $shieldTimeline) {
|
||||
$mitigations = resolveMitigations($tgt['buffs'], $mitigIdMap);
|
||||
if ($tgt['absorbed'] > 0 && !empty($shieldTimeline)) {
|
||||
$existing = [];
|
||||
foreach ($mitigations as $m) {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
const CACHED_LOG_DIR = __DIR__ . '/../cached_logs';
|
||||
const CACHED_LOG_VERSION = 'v8';
|
||||
const CACHED_LOG_VERSION = 'v3';
|
||||
|
||||
function cache_language(string $language): string {
|
||||
$language = strtolower(trim($language));
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
@ -778,41 +778,5 @@
|
||||
},
|
||||
"icon": "https://xivapi.com/i/003000/003689_hr1.png",
|
||||
"shield": "360% of HP restored"
|
||||
},
|
||||
"16464": {
|
||||
"cast": 0,
|
||||
"recast": 250,
|
||||
"names": {
|
||||
"en": "Nascent Flash",
|
||||
"de": "Urflackern",
|
||||
"fr": "Exaltation naissante",
|
||||
"jp": "原初の猛り"
|
||||
},
|
||||
"icon": null,
|
||||
"shield": null
|
||||
},
|
||||
"25861": {
|
||||
"cast": 0,
|
||||
"recast": 600,
|
||||
"names": {
|
||||
"en": "Aquaveil",
|
||||
"de": "Wasserschleier",
|
||||
"fr": "Aquavoile",
|
||||
"jp": "アクアヴェール"
|
||||
},
|
||||
"icon": null,
|
||||
"shield": null
|
||||
},
|
||||
"25873": {
|
||||
"cast": 0,
|
||||
"recast": 600,
|
||||
"names": {
|
||||
"en": "Exaltation",
|
||||
"de": "Exaltation",
|
||||
"fr": "Exaltation",
|
||||
"jp": "エクザルテーション"
|
||||
},
|
||||
"icon": null,
|
||||
"shield": null
|
||||
}
|
||||
}
|
||||
|
||||
152
css/planner.css
@ -320,10 +320,9 @@
|
||||
font-size: 13px;
|
||||
color: var(--t2);
|
||||
}
|
||||
.badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); }
|
||||
.badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); }
|
||||
.badge-assign-shield { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); }
|
||||
.badge-assign-personal { background: rgba(177,112,255,.08); border-color: rgba(177,112,255,.4); color: #dbc7ff; }
|
||||
.badge-assign-buff { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); }
|
||||
.badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); }
|
||||
.badge-assign-shield { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); }
|
||||
|
||||
.badge-assign--missing-job {
|
||||
border-style: dashed;
|
||||
@ -486,10 +485,9 @@
|
||||
}
|
||||
.ability-chip:hover { background: var(--bg3); color: var(--t1); }
|
||||
|
||||
.ability-chip.badge-assign-buff.ability-chip--active { background: rgba(200,168,75,.18); border-color: rgba(200,168,75,.6); color: var(--gold); }
|
||||
.ability-chip.badge-assign-debuff.ability-chip--active { background: rgba(224,92,92,.18); border-color: rgba(224,92,92,.6); color: var(--red); }
|
||||
.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); }
|
||||
.ability-chip.badge-assign-personal.ability-chip--active { background: rgba(177,112,255,.18); border-color: rgba(177,112,255,.6); color: #dbc7ff; }
|
||||
.ability-chip.badge-assign-buff.ability-chip--active { background: rgba(200,168,75,.18); border-color: rgba(200,168,75,.6); color: var(--gold); }
|
||||
.ability-chip.badge-assign-debuff.ability-chip--active { background: rgba(224,92,92,.18); border-color: rgba(224,92,92,.6); color: var(--red); }
|
||||
.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); }
|
||||
|
||||
.ability-chip--other-job { opacity: 0.45; }
|
||||
|
||||
@ -528,9 +526,9 @@
|
||||
|
||||
/* ── Info Panel ──────────────────────────────────────────────────────────────── */
|
||||
.planner-info-panel {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-section { margin-bottom: 12px; }
|
||||
@ -562,10 +560,9 @@
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-legend-dot--buff { background: rgba(200,168,75,.8); }
|
||||
.info-legend-dot--debuff { background: var(--red); }
|
||||
.info-legend-dot--shield { background: var(--blue); }
|
||||
.info-legend-dot--personal { background: rgba(177,112,255,.9); }
|
||||
.info-legend-dot--buff { background: rgba(200,168,75,.8); }
|
||||
.info-legend-dot--debuff { background: var(--red); }
|
||||
.info-legend-dot--shield { background: var(--blue); }
|
||||
|
||||
.info-legend-label {
|
||||
font-size: 12px;
|
||||
@ -757,8 +754,6 @@
|
||||
overflow-y: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg1);
|
||||
scrollbar-color: var(--border) var(--bg1);
|
||||
scrollbar-width: thin;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
@ -769,11 +764,6 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.timeline-scroll::-webkit-scrollbar { height: 6px; }
|
||||
.timeline-scroll::-webkit-scrollbar-track { background: var(--bg1); }
|
||||
.timeline-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
.timeline-scroll::-webkit-scrollbar-thumb:hover { background: var(--t3); }
|
||||
|
||||
.timeline-grid {
|
||||
width: calc(180px + var(--timeline-width));
|
||||
display: grid;
|
||||
@ -1220,124 +1210,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── View Toggle ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.view-toggle-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
background: transparent;
|
||||
color: var(--t2);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-b);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.view-toggle-btn:hover {
|
||||
color: var(--t1);
|
||||
border-color: var(--t3);
|
||||
}
|
||||
|
||||
.view-toggle-btn.active {
|
||||
background: var(--gold);
|
||||
border-color: var(--gold);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Meine Spells ────────────────────────────────────────────────────────── */
|
||||
|
||||
.myspells-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.myspells-controls select {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
background: var(--bg2);
|
||||
color: var(--t1);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.myspells-row {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 180px 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 4px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.myspells-abilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.myspells-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.myspells-time {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
color: var(--gold);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.myspells-mechanic {
|
||||
color: var(--t2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.myspells-ability {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: var(--t1);
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 3px 8px 3px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.myspells-ability.myspells-type--debuff { color: var(--orange); border-color: rgba(255,140,0,0.3); }
|
||||
.myspells-ability.myspells-type--shield { color: var(--blue); border-color: rgba(88,166,255,0.3); }
|
||||
.myspells-ability.myspells-type--personal { color: #dbc7ff; border-color: rgba(177,112,255,0.4); }
|
||||
|
||||
.myspells-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.myspells-empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: var(--t3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.planner-layout {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@ -605,12 +605,10 @@
|
||||
].map(m => {
|
||||
const iconSrc = mitigationIcon(m);
|
||||
if (!iconSrc) return '';
|
||||
const dr = m.dr > 0 ? ` −${m.dr}%` : '';
|
||||
const jobAbbr = m.sourcePlayerType ? (JOB_ABBR[m.sourcePlayerType] ?? '') : '';
|
||||
const label = jobAbbr ? `${jobAbbr} · ${m.name}` : m.name;
|
||||
const dr = m.dr > 0 ? ` −${m.dr}%` : '';
|
||||
return m.missing
|
||||
? `<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="${label}${dr}">`;
|
||||
? `<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" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
|
||||
}).join('');
|
||||
|
||||
// Current targets
|
||||
|
||||
@ -36,7 +36,6 @@
|
||||
{ name: 'Bulwark', buffType: 'buff', extraAbilityGameID: 22, duration: 10 },
|
||||
{ name: 'Holy Sheltron', buffType: 'buff', extraAbilityGameID: 25746, duration: 8 },
|
||||
{ name: 'Intervention', buffType: 'buff', extraAbilityGameID: 7382, duration: 8 },
|
||||
{ name: 'Knight\'s Resolve', buffType: 'buff', extraAbilityGameID: 7382, duration: 4 }, // Proc von Intervention
|
||||
{ name: 'Reprisal', buffType: 'debuff' },
|
||||
],
|
||||
'WAR': [
|
||||
@ -48,9 +47,6 @@
|
||||
{ name: 'Thrill of Battle', buffType: 'buff', extraAbilityGameID: 40, duration: 10 },
|
||||
{ name: 'Raw Intuition', buffType: 'buff', extraAbilityGameID: 3551, duration: 6 },
|
||||
{ name: 'Bloodwhetting', buffType: 'shield', extraAbilityGameID: 25751, duration: 8 },
|
||||
{ name: 'Nascent Glint', buffType: 'buff', extraAbilityGameID: 16464, duration: 8 }, // Proc von Nascent Flash auf Ziel
|
||||
{ name: 'Stem the Flow', buffType: 'buff', extraAbilityGameID: 25751, duration: 4 }, // Proc von Bloodwhetting / Nascent Flash
|
||||
{ name: 'Stem the Tide', buffType: 'shield', extraAbilityGameID: 25751, duration: 4 }, // Proc von Bloodwhetting / Nascent Flash
|
||||
{ name: 'Reprisal', buffType: 'debuff' },
|
||||
],
|
||||
'DRK': [
|
||||
@ -73,12 +69,10 @@
|
||||
{ name: 'Camouflage', buffType: 'buff', extraAbilityGameID: 16140, duration: 20 },
|
||||
{ name: 'Heart of Stone', buffType: 'buff', extraAbilityGameID: 16161, duration: 7 },
|
||||
{ name: 'Heart of Corundum', buffType: 'buff', extraAbilityGameID: 25758, duration: 8 },
|
||||
{ name: 'Clarity of Corundum', buffType: 'buff', extraAbilityGameID: 25758, duration: 4 }, // Proc von HoC, geht auf beliebiges Partymitglied
|
||||
{ name: 'Reprisal', buffType: 'debuff' },
|
||||
],
|
||||
'WHM': [
|
||||
{ name: 'Temperance', buffType: 'buff' },
|
||||
{ name: 'Aquaveil', buffType: 'buff', extraAbilityGameID: 25861, duration: 8 }, // Personal, WHM auf Ziel
|
||||
{ name: 'Divine Benison', buffType: 'shield', extraAbilityGameID: 7432, duration: 15 },
|
||||
{ name: 'Divine Caress', buffType: 'shield' },
|
||||
],
|
||||
@ -92,7 +86,6 @@
|
||||
],
|
||||
'AST': [
|
||||
{ name: 'Collective Unconscious', buffType: 'buff' },
|
||||
{ name: 'Exaltation', buffType: 'buff', extraAbilityGameID: 25873, duration: 8 }, // Personal, AST auf Ziel
|
||||
{ name: 'Neutral Sect', buffType: 'shield' },
|
||||
{ name: 'Intersection', buffType: 'shield', extraAbilityGameID: 16556, duration: 30 },
|
||||
{ name: 'the Spire', buffType: 'shield', extraAbilityGameID: 37025, duration: 30 },
|
||||
@ -155,21 +148,19 @@
|
||||
'Passage of Arms': 'PLD', 'Divine Veil': 'PLD',
|
||||
'Hallowed Ground': 'PLD', 'Sentinel': 'PLD', 'Guardian': 'PLD',
|
||||
'Bulwark': 'PLD', 'Holy Sheltron': 'PLD', 'Intervention': 'PLD',
|
||||
'Knight\'s Resolve': 'PLD',
|
||||
'Shake It Off': 'WAR', 'Holmgang': 'WAR', 'Vengeance': 'WAR',
|
||||
'Damnation': 'WAR', 'Thrill of Battle': 'WAR', 'Raw Intuition': 'WAR',
|
||||
'Bloodwhetting': 'WAR', 'Nascent Glint': 'WAR',
|
||||
'Stem the Flow': 'WAR', 'Stem the Tide': 'WAR',
|
||||
'Bloodwhetting': 'WAR',
|
||||
'Dark Missionary': 'DRK', 'Living Dead': 'DRK', 'Shadow Wall': 'DRK',
|
||||
'Shadowed Vigil': 'DRK', 'Dark Mind': 'DRK', 'The Blackest Night': 'DRK',
|
||||
'Oblation': 'DRK',
|
||||
'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB',
|
||||
'Great Nebula': 'GNB', 'Camouflage': 'GNB', 'Heart of Stone': 'GNB',
|
||||
'Heart of Corundum': 'GNB', 'Clarity of Corundum': 'GNB',
|
||||
'Temperance': 'WHM', 'Aquaveil': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
|
||||
'Heart of Corundum': 'GNB',
|
||||
'Temperance': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
|
||||
'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH',
|
||||
'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH',
|
||||
'Collective Unconscious': 'AST', 'Exaltation': 'AST', 'Neutral Sect': 'AST',
|
||||
'Collective Unconscious': 'AST', 'Neutral Sect': 'AST',
|
||||
'Intersection': 'AST', 'the Spire': 'AST',
|
||||
'Kerachole': 'SGE', 'Holos': 'SGE', 'Holosakos': 'SGE',
|
||||
'Panhaima': 'SGE', 'Haima': 'SGE',
|
||||
@ -228,15 +219,6 @@
|
||||
'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png',
|
||||
'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png',
|
||||
'Improvised Finish': 'assets/icons/mitigation/improvised-finish.png',
|
||||
'Aquaveil': 'assets/icons/mitigation/aquaveil.png',
|
||||
'Exaltation': 'assets/icons/mitigation/exaltation.png',
|
||||
'Heart of Corundum': 'assets/icons/mitigation/heart-of-corundum.png',
|
||||
'Clarity of Corundum': 'assets/icons/mitigation/clarity-of-corundum.png',
|
||||
'Heart of Stone': 'assets/icons/mitigation/heart-of-stone.png',
|
||||
'Knight\'s Resolve': 'assets/icons/mitigation/knights-resolve.png',
|
||||
'Nascent Glint': 'assets/icons/mitigation/nascent-glint.png',
|
||||
'Stem the Flow': 'assets/icons/mitigation/stem-the-flow.png',
|
||||
'Stem the Tide': 'assets/icons/mitigation/stem-the-tide.png',
|
||||
};
|
||||
|
||||
const ABILITY_DR = {
|
||||
@ -263,15 +245,10 @@
|
||||
'Guardian': 0.40,
|
||||
'Holy Sheltron': 0.15,
|
||||
'Intervention': 0.10,
|
||||
'Knight\'s Resolve': 0.10,
|
||||
'Aquaveil': 0.15,
|
||||
'Exaltation': 0.10,
|
||||
'Vengeance': 0.30,
|
||||
'Damnation': 0.40,
|
||||
'Raw Intuition': 0.10,
|
||||
'Bloodwhetting': 0.10,
|
||||
'Nascent Glint': 0.10,
|
||||
'Stem the Flow': 0.10,
|
||||
'Shadow Wall': 0.30,
|
||||
'Shadowed Vigil': 0.40,
|
||||
'Dark Mind': 0.20,
|
||||
@ -282,7 +259,6 @@
|
||||
'Camouflage': 0.10,
|
||||
'Heart of Stone': 0.15,
|
||||
'Heart of Corundum': 0.15,
|
||||
'Clarity of Corundum': 0.15,
|
||||
'Riddle of Earth': 0.20,
|
||||
'Third Eye': 0.10,
|
||||
};
|
||||
|
||||
197
js/planner.js
@ -461,26 +461,10 @@ function renderPlanDetail(plan) {
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<div class="card-title">Mechaniken</div>
|
||||
<div class="view-toggle-btns">
|
||||
<button class="view-toggle-btn active" data-view="mechanics">Mechaniken</button>
|
||||
<button class="view-toggle-btn" data-view="myspells">★ Meine Spells</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mechanic-list">
|
||||
${renderMechanicListHtml(plan)}
|
||||
</div>
|
||||
<div id="myspells-panel" style="display:none">
|
||||
<div class="myspells-controls">
|
||||
<select id="myspells-job-select">
|
||||
<option value="">— Job wählen —</option>
|
||||
${(plan.jobComposition ?? []).filter(Boolean).filter((j, i, a) => a.indexOf(j) === i).map(j =>
|
||||
`<option value="${escHtml(j)}">${escHtml(j)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
<button id="myspells-copy-btn" class="btn btn-sm" title="Als Text kopieren">⎘ Kopieren</button>
|
||||
</div>
|
||||
<div id="myspells-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -494,118 +478,10 @@ function renderPlanDetail(plan) {
|
||||
initTimelineOptions(plan.id);
|
||||
initTimeline(plan.id);
|
||||
initMechanicClicks(plan.id);
|
||||
initMySpells(plan.id);
|
||||
renderInfoPanel(plan);
|
||||
ensureActionMetaLoaded().then(() => refreshMechanicList(plan.id));
|
||||
}
|
||||
|
||||
// ── Meine Spells ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderMySpellsHtml(plan, job) {
|
||||
if (!job) return '<div class="myspells-empty">Job auswählen um Assignments zu sehen.</div>';
|
||||
|
||||
const mechanics = visiblePlanMechanics(plan);
|
||||
const rows = [];
|
||||
for (const mechanic of mechanics) {
|
||||
const mine = (mechanic.assignments ?? []).filter(a => a.job === job);
|
||||
if (!mine.length) continue;
|
||||
rows.push({ mechanic, assignments: mine });
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return `<div class="myspells-empty">Keine Assignments für ${escHtml(job)} in diesem Plan.</div>`;
|
||||
}
|
||||
|
||||
return rows.map(({ mechanic, assignments }) => {
|
||||
const time = escHtml(fmtTimestamp(mechanic.timestamp));
|
||||
const mechName = escHtml(mechanic.name);
|
||||
const abilities = assignments.map(a => {
|
||||
const iconSrc = abilityIcon(a.ability);
|
||||
const icon = iconSrc
|
||||
? `<img src="${escHtml(iconSrc)}" class="myspells-icon" alt="">`
|
||||
: '';
|
||||
const name = escHtml(assignmentAbilityName(a, plan));
|
||||
const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(a.ability);
|
||||
const typeClass = a.buffType === 'debuff' ? 'myspells-type--debuff'
|
||||
: a.buffType === 'shield' ? 'myspells-type--shield'
|
||||
: isPersonal ? 'myspells-type--personal'
|
||||
: 'myspells-type--buff';
|
||||
return `<span class="myspells-ability ${typeClass}">${icon}${name}</span>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="myspells-row">
|
||||
<span class="myspells-time">${time}</span>
|
||||
<span class="myspells-mechanic">${mechName}</span>
|
||||
<div class="myspells-abilities">${abilities}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function mySpellsPlainText(plan, job) {
|
||||
if (!job) return '';
|
||||
const mechanics = visiblePlanMechanics(plan);
|
||||
const lines = [`Meine Spells — ${job} (${plan.name})`, '─'.repeat(36)];
|
||||
for (const mechanic of mechanics) {
|
||||
const mine = (mechanic.assignments ?? []).filter(a => a.job === job);
|
||||
if (!mine.length) continue;
|
||||
const time = fmtTimestamp(mechanic.timestamp);
|
||||
const names = mine.map(a => assignmentAbilityName(a, plan)).join(', ');
|
||||
lines.push(`${time.padEnd(6)} ${mechanic.name.padEnd(22)} → ${names}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function initMySpells(planId) {
|
||||
const viewBtns = document.querySelectorAll('.view-toggle-btn');
|
||||
const mechList = document.getElementById('mechanic-list');
|
||||
const myPanel = document.getElementById('myspells-panel');
|
||||
const jobSelect = document.getElementById('myspells-job-select');
|
||||
const myList = document.getElementById('myspells-list');
|
||||
const copyBtn = document.getElementById('myspells-copy-btn');
|
||||
if (!mechList || !myPanel || !jobSelect || !myList) return;
|
||||
|
||||
viewBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
viewBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const view = btn.dataset.view;
|
||||
mechList.style.display = view === 'mechanics' ? '' : 'none';
|
||||
myPanel.style.display = view === 'myspells' ? '' : 'none';
|
||||
if (view === 'myspells') {
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Gespeicherten Job wiederherstellen
|
||||
const savedJob = localStorage.getItem('ff14-planner-myspells-job') ?? '';
|
||||
if (savedJob && [...jobSelect.options].some(o => o.value === savedJob)) {
|
||||
jobSelect.value = savedJob;
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (plan) myList.innerHTML = renderMySpellsHtml(plan, savedJob);
|
||||
}
|
||||
|
||||
jobSelect.addEventListener('change', () => {
|
||||
localStorage.setItem('ff14-planner-myspells-job', jobSelect.value);
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value);
|
||||
});
|
||||
|
||||
copyBtn?.addEventListener('click', () => {
|
||||
const plan = loadPlans().find(p => p.id === planId);
|
||||
if (!plan) return;
|
||||
const text = mySpellsPlainText(plan, jobSelect.value);
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copyBtn.textContent = '✓ Kopiert';
|
||||
setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800);
|
||||
}).catch(() => {
|
||||
copyBtn.textContent = '✗ Fehler';
|
||||
setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function avgNonTankMaxHp(plan) {
|
||||
const roster = plan.playerRoster ?? [];
|
||||
const jobComp = plan.jobComposition ?? [];
|
||||
@ -617,40 +493,10 @@ function avgNonTankMaxHp(plan) {
|
||||
return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length);
|
||||
}
|
||||
|
||||
function avgTankMaxHp(plan) {
|
||||
const roster = plan.playerRoster ?? [];
|
||||
const jobComp = plan.jobComposition ?? [];
|
||||
const hps = jobComp
|
||||
.map((job, i) => ({ job, maxHp: roster[i]?.maxHp ?? 0 }))
|
||||
.filter(p => p.job && JOB_ROLE[p.job] === 'tank' && p.maxHp > 0)
|
||||
.map(p => p.maxHp);
|
||||
if (!hps.length) return 0;
|
||||
// Tankbuster trifft einen Tank → Durchschnitt über alle gefundenen Tanks
|
||||
// (1 Tank → sein MaxHP direkt; 2 Tanks → (hp1 + hp2) / 2)
|
||||
return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length);
|
||||
}
|
||||
|
||||
// Ermittelt den maxHP-Wert direkt aus dem aoe_event (für präzise Tankbuster-Anzeige)
|
||||
function tankMaxHpFromEvent(ev) {
|
||||
if (!ev?.isHeavyTankbuster) return 0;
|
||||
const tankTargets = (ev.targets ?? []).filter(t => t.role === 'tank' && (t.maxHp ?? 0) > 0);
|
||||
if (tankTargets.length === 1) return tankTargets[0].maxHp;
|
||||
if (tankTargets.length > 1) {
|
||||
// Mehrere Tanks gleichzeitig → Durchschnitt
|
||||
return Math.round(tankTargets.reduce((s, t) => s + t.maxHp, 0) / tankTargets.length);
|
||||
}
|
||||
// Fallback: erstes Target mit maxHp (z.B. wenn Role nicht gesetzt)
|
||||
const any = (ev.targets ?? []).find(t => (t.maxHp ?? 0) > 0);
|
||||
return any?.maxHp ?? 0;
|
||||
}
|
||||
|
||||
function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []) {
|
||||
const isTankbuster = !!mechanic.isHeavyTankbuster;
|
||||
let mult = 1;
|
||||
for (const a of assignments) {
|
||||
if (a.buffType === 'shield') continue;
|
||||
// Persönliche Mitigation nur bei Tankbustern einrechnen
|
||||
if (!isTankbuster && TIMELINE_PERSONAL_ABILITIES.has(a.ability)) continue;
|
||||
mult *= (1 - (ABILITY_DR[a.ability] ?? 0));
|
||||
}
|
||||
return mult;
|
||||
@ -664,13 +510,6 @@ function plannedAssignmentsForMechanic(plan, targetMechanic) {
|
||||
for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) {
|
||||
if (targetTime < entry.start - tolerance || targetTime > entry.end + tolerance) continue;
|
||||
|
||||
// Persönliche Mitigation zeigt nur auf der Mechanik, der sie direkt zugewiesen ist
|
||||
// (verhindert dass DRK-Cooldowns auf GNB-Tankbuster erscheinen und umgekehrt)
|
||||
if (TIMELINE_PERSONAL_ABILITIES.has(entry.assignment.ability)
|
||||
&& entry.mechanic.id !== targetMechanic.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
...entry.assignment,
|
||||
sourceMechanicId: entry.mechanic.id,
|
||||
@ -696,22 +535,16 @@ function renderMechanicListHtml(plan) {
|
||||
}
|
||||
|
||||
const activeJobSet = new Set(plan.jobComposition.filter(j => j));
|
||||
const nonTankAvgHp = avgNonTankMaxHp(plan);
|
||||
const tankAvgHp = avgTankMaxHp(plan);
|
||||
const avgHp = avgNonTankMaxHp(plan);
|
||||
|
||||
return mechanics.map(m => {
|
||||
// Tankbuster: gespeicherter maxHP des getroffenen Tanks hat Vorrang (präziser als Roster-Durchschnitt)
|
||||
const avgHp = m.isHeavyTankbuster
|
||||
? ((m.tankMaxHp ?? 0) > 0 ? m.tankMaxHp : tankAvgHp)
|
||||
: nonTankAvgHp;
|
||||
const planned = plannedAssignmentsForMechanic(plan, m);
|
||||
const sorted = sortedAssignments(planned);
|
||||
const assignHtml = sorted.length === 0
|
||||
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
||||
: sorted.map(a => {
|
||||
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
||||
: a.buffType === 'shield' ? 'badge-assign-shield'
|
||||
: TIMELINE_PERSONAL_ABILITIES.has(a.ability) ? 'badge-assign-personal'
|
||||
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
||||
: a.buffType === 'shield' ? 'badge-assign-shield'
|
||||
: 'badge-assign-buff';
|
||||
const isMissing = !!a.job && !activeJobSet.has(a.job);
|
||||
const icon = abilityIcon(a.ability);
|
||||
@ -755,7 +588,7 @@ function renderMechanicListHtml(plan) {
|
||||
: ''
|
||||
}
|
||||
${hasDrAssign || hasShield ? `<div class="mechanic-dmg mechanic-mitig-row">
|
||||
${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> nach DR` : ''}
|
||||
${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> mitigiert` : ''}
|
||||
${hasShield ? `<span class="mechanic-mitig-shield${fullCls ? ' ' + fullCls : ''}">Mitigation mit Schild ${fmtNumber(mitigFull)}</span>` : ''}
|
||||
</div>` : ''}
|
||||
<div class="mechanic-assignments">${assignHtml}</div>
|
||||
@ -860,18 +693,12 @@ const TIMELINE_PERSONAL_ABILITIES = new Set([
|
||||
'Bulwark',
|
||||
'Holy Sheltron',
|
||||
'Intervention',
|
||||
'Knight\'s Resolve',
|
||||
'Aquaveil',
|
||||
'Exaltation',
|
||||
'Holmgang',
|
||||
'Vengeance',
|
||||
'Damnation',
|
||||
'Thrill of Battle',
|
||||
'Raw Intuition',
|
||||
'Bloodwhetting',
|
||||
'Nascent Glint',
|
||||
'Stem the Flow',
|
||||
'Stem the Tide',
|
||||
'Living Dead',
|
||||
'Shadow Wall',
|
||||
'Shadowed Vigil',
|
||||
@ -884,7 +711,6 @@ const TIMELINE_PERSONAL_ABILITIES = new Set([
|
||||
'Camouflage',
|
||||
'Heart of Stone',
|
||||
'Heart of Corundum',
|
||||
'Clarity of Corundum',
|
||||
'Divine Benison',
|
||||
'Intersection',
|
||||
'the Spire',
|
||||
@ -2035,7 +1861,6 @@ function renderInfoPanel(plan) {
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--buff"></span><span class="info-legend-label">Mitigation</span></div>
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--debuff"></span><span class="info-legend-label">Debuff</span></div>
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--shield"></span><span class="info-legend-label">Schild</span></div>
|
||||
<div class="info-legend-row"><span class="info-legend-dot info-legend-dot--personal"></span><span class="info-legend-label">Personal</span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@ -2370,11 +2195,7 @@ function sortedAssignments(assignments) {
|
||||
);
|
||||
}
|
||||
|
||||
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] ?? '';
|
||||
}
|
||||
function guessJob(abilityName, players) {
|
||||
if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName];
|
||||
const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? '');
|
||||
if (abilityName === 'Reprisal') {
|
||||
@ -2429,7 +2250,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
||||
ability: key,
|
||||
abilityName: mitigationDisplayName(m) || mitigationNames[key],
|
||||
actionId: m.extraAbilityGameID ?? null,
|
||||
job: guessJob(key, players, m),
|
||||
job: guessJob(key, players),
|
||||
buffType: m.buffType ?? '',
|
||||
timestamp: Math.max(0, relTs + IMPORT_OFFSET_MS),
|
||||
});
|
||||
@ -2446,7 +2267,6 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
||||
phase: phase?.name ?? '',
|
||||
unmitigatedDamage: avgUnmit,
|
||||
isHeavyTankbuster: !!ev.isHeavyTankbuster,
|
||||
tankMaxHp: tankMaxHpFromEvent(ev),
|
||||
notes: '',
|
||||
assignments,
|
||||
};
|
||||
@ -2567,10 +2387,9 @@ async function refreshPlanLanguage(planId) {
|
||||
}));
|
||||
return {
|
||||
...mechanic,
|
||||
name: match.abilityName ?? mechanic.name,
|
||||
abilityId: match.abilityId ?? mechanic.abilityId,
|
||||
name: match.abilityName ?? mechanic.name,
|
||||
abilityId: match.abilityId ?? mechanic.abilityId,
|
||||
isHeavyTankbuster: mechanic.mechanicTypeManual ? !!mechanic.isHeavyTankbuster : !!match.isHeavyTankbuster,
|
||||
tankMaxHp: mechanic.mechanicTypeManual ? (mechanic.tankMaxHp ?? 0) : tankMaxHpFromEvent(match),
|
||||
assignments,
|
||||
};
|
||||
});
|
||||
|
||||
@ -2,9 +2,6 @@
|
||||
|
||||
<!-- Left: Plan list sidebar -->
|
||||
<div class="plan-sidebar">
|
||||
|
||||
<div id="planner-info-panel" class="planner-info-panel"></div>
|
||||
|
||||
<div class="plan-sidebar-header">
|
||||
<div class="card-title">Pläne</div>
|
||||
<button id="planner-new-folder-btn" class="btn btn-sm" title="Neuer Ordner">+ Ordner</button>
|
||||
@ -29,6 +26,7 @@
|
||||
|
||||
<div id="plan-list"></div>
|
||||
|
||||
<div id="planner-info-panel" class="planner-info-panel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Plan detail -->
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="report">⚔ Report</button>
|
||||
<button class="tab" data-tab="analysis">⚖ Analyse</button>
|
||||
<button class="tab" data-tab="planner">☰ Planer</button>
|
||||
<button class="tab" data-tab="planner">📋 Planer</button>
|
||||
</nav>
|
||||
<div class="topbar-user">Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?></div>
|
||||
</header>
|
||||
|
||||