diff --git a/api/analysis.php b/api/analysis.php index 5fd55f6..805415f 100644 --- a/api/analysis.php +++ b/api/analysis.php @@ -122,10 +122,11 @@ const MITIGATION_ABILITIES = [ // GNB 'Superbolide' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 16152], 'Nebula' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 16148], - 'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'extraAbilityGameID' => 36935], + 'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'statusId' => 1003838, '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', 'extraAbilityGameID' => 25758], + '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 // DPS 'Riddle of Earth' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7394], 'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241], diff --git a/api/cache.php b/api/cache.php index e77fa93..1ad39d3 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 = 'v3'; +const CACHED_LOG_VERSION = 'v4'; function cache_language(string $language): string { $language = strtolower(trim($language)); diff --git a/assets/icons/mitigation/clarity-of-corundum.png b/assets/icons/mitigation/clarity-of-corundum.png new file mode 100644 index 0000000..326d844 Binary files /dev/null and b/assets/icons/mitigation/clarity-of-corundum.png differ diff --git a/assets/icons/mitigation/heart-of-corundum.png b/assets/icons/mitigation/heart-of-corundum.png new file mode 100644 index 0000000..326d844 Binary files /dev/null and b/assets/icons/mitigation/heart-of-corundum.png differ diff --git a/assets/icons/mitigation/heart-of-stone.png b/assets/icons/mitigation/heart-of-stone.png new file mode 100644 index 0000000..8466d30 Binary files /dev/null and b/assets/icons/mitigation/heart-of-stone.png differ diff --git a/js/ffxiv-data.js b/js/ffxiv-data.js index 4305def..43b71c3 100644 --- a/js/ffxiv-data.js +++ b/js/ffxiv-data.js @@ -69,6 +69,7 @@ { 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': [ @@ -156,7 +157,7 @@ 'Oblation': 'DRK', 'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB', 'Great Nebula': 'GNB', 'Camouflage': 'GNB', 'Heart of Stone': 'GNB', - 'Heart of Corundum': 'GNB', + 'Heart of Corundum': 'GNB', 'Clarity 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', @@ -219,6 +220,9 @@ 'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png', 'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png', 'Improvised Finish': 'assets/icons/mitigation/improvised-finish.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', }; const ABILITY_DR = { @@ -259,6 +263,7 @@ '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, }; diff --git a/js/planner.js b/js/planner.js index 320366a..2d8e58b 100644 --- a/js/planner.js +++ b/js/planner.js @@ -493,10 +493,40 @@ 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; @@ -510,6 +540,13 @@ 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, @@ -535,9 +572,14 @@ function renderMechanicListHtml(plan) { } const activeJobSet = new Set(plan.jobComposition.filter(j => j)); - const avgHp = avgNonTankMaxHp(plan); + const nonTankAvgHp = avgNonTankMaxHp(plan); + const tankAvgHp = avgTankMaxHp(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 @@ -588,7 +630,7 @@ function renderMechanicListHtml(plan) { : '' } ${hasDrAssign || hasShield ? `