Compare commits

...

4 Commits

Author SHA1 Message Date
xziino
a9b3cc8666 Planer: Clarity of Corundum + separate Tankbuster-Fixes
- Clarity of Corundum (GNB Proc, 15% DR, statusId 1002684) neu in
  MITIGATION_ABILITIES, ABILITY_DR, JOB_ABILITIES, TIMELINE_PERSONAL_ABILITIES
- statusId fuer Heart of Corundum (1002683) und Great Nebula (1003838)
  nachgetragen (stabilere Erkennung via buffs-Feld)
- Icons heruntergeladen: heart-of-corundum.png, heart-of-stone.png,
  clarity-of-corundum.png (teilt HoC-Action-Icon vorlaeufig)
- Cache-Version v3 -> v4 (erzwingt Neu-Fetch nach Erkennungs-Aenderungen)

Separate Tankbuster:
- tankMaxHpFromEvent(): maxHP direkt aus dem aoe_event des getroffenen
  Tanks statt Roster-Durchschnitt (ein Tank getroffen = sein MaxHP)
- renderMechanicListHtml: m.tankMaxHp hat Vorrang vor avgTankMaxHp(plan)
- plannedAssignmentsForMechanic: persoenliche Mitigation erscheint nur
  auf der direkt zugewiesenen Mechanik (DRK-CDs nicht mehr auf GNB-TB)
- DR-Label: 'mitigiert' -> 'nach DR' (verbleibender Schaden, nicht
  reduzierter Betrag)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:04:21 +02:00
Akurosia Kamo
7eeeb5ef56 Merge remote-tracking branch 'origin/main' into akus_schabernack4 2026-05-24 11:55:11 +02:00
Akurosia Kamo
e8863d83a5 more personals 2026-05-24 11:45:46 +02:00
Akurosia Kamo
90fcbb69a5 more personals 2026-05-24 11:39:51 +02:00
9 changed files with 290 additions and 96 deletions

View File

@ -99,46 +99,47 @@ const MITIGATION_ABILITIES = [
'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826, 'extraAbilityGameID' => 16012], 'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826, 'extraAbilityGameID' => 16012],
'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707, 'extraAbilityGameID' => 25857], 'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707, 'extraAbilityGameID' => 25857],
// ── Personal / targeted mitigation ───────────────────────────────────── // ── Personal / targeted mitigation ─────────────────────────────────────
'Rampart' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7531], 'Rampart' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7531],
// PLD // PLD
'Hallowed Ground' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 30], 'Hallowed Ground' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 30],
'Sentinel' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 17], 'Sentinel' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 17],
'Bulwark' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 22], 'Bulwark' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 22],
'Holy Sheltron' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25746], 'Holy Sheltron' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 25746],
'Intervention' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7382], 'Intervention' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 7382],
// WAR // WAR
'Holmgang' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 43], 'Holmgang' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 43],
'Vengeance' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 44], 'Vengeance' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 44],
'Damnation' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36923], 'Damnation' => ['dr' => 40, 'buffType' => 'buff', 'extraAbilityGameID' => 36923],
'Thrill of Battle' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 40], 'Thrill of Battle' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 40],
'Raw Intuition' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3551], 'Raw Intuition' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 3551],
// DRK // DRK
'Living Dead' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3638], 'Living Dead' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3638],
'Shadow Wall' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3636], 'Shadow Wall' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 3636],
'Shadowed Vigil' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36927], 'Shadowed Vigil' => ['dr' => 40, 'buffType' => 'buff', 'extraAbilityGameID' => 36927],
'Dark Mind' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3634], 'Dark Mind' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 3634],
'The Blackest Night' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 7393], 'The Blackest Night' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 7393],
'Oblation' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25754], 'Oblation' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 25754],
// GNB // GNB
'Superbolide' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16152], 'Superbolide' => ['dr' => 100, 'buffType' => 'buff', 'extraAbilityGameID' => 16152],
'Nebula' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16148], 'Nebula' => ['dr' => 30, 'buffType' => 'buff', 'extraAbilityGameID' => 16148],
'Great Nebula' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36935], 'Great Nebula' => ['dr' => 40, 'buffType' => 'buff', 'statusId' => 1003838, 'extraAbilityGameID' => 36935],
'Camouflage' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16140], 'Camouflage' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 16140],
'Heart of Stone' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16161], 'Heart of Stone' => ['dr' => 15, 'buffType' => 'buff', 'extraAbilityGameID' => 16161],
'Heart of Corundum' => ['dr' => 0, '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 // DPS
'Riddle of Earth' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7394], 'Riddle of Earth' => ['dr' => 20, 'buffType' => 'buff', 'extraAbilityGameID' => 7394],
'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241], 'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241],
'Third Eye' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7498], 'Third Eye' => ['dr' => 10, 'buffType' => 'buff', 'extraAbilityGameID' => 7498],
'Arcane Crest' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 24404], 'Arcane Crest' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 24404],
'Manaward' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 157], 'Manaward' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 157],
// ── Shields ───────────────────────────────────────────────────────────── // ── Shields ─────────────────────────────────────────────────────────────
// PLD // PLD
'Divine Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001362, 'extraAbilityGameID' => 3540], 'Divine Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001362, 'extraAbilityGameID' => 3540],
'Guardian' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003830, 'extraAbilityGameID' => 36920], // FFLogs: "Guardian's Will" 'Guardian' => ['dr' => 40, 'buffType' => 'shield', 'statusId' => 1003830, 'extraAbilityGameID' => 36920], // FFLogs: "Guardian's Will"
// WAR // WAR
'Shake It Off' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001457, 'extraAbilityGameID' => 7388], 'Shake It Off' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001457, 'extraAbilityGameID' => 7388],
'Bloodwhetting' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1002678, 'extraAbilityGameID' => 25751], 'Bloodwhetting' => ['dr' => 10, 'buffType' => 'shield', 'statusId' => 1002678, 'extraAbilityGameID' => 25751],
// WHM // WHM
'Divine Benison' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001218, 'extraAbilityGameID' => 7432], 'Divine Benison' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001218, 'extraAbilityGameID' => 7432],
'Divine Caress' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003903, 'extraAbilityGameID' => 37011], 'Divine Caress' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1003903, 'extraAbilityGameID' => 37011],

View File

@ -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 = 'v3'; const CACHED_LOG_VERSION = 'v4';
function cache_language(string $language): string { function cache_language(string $language): string {
$language = strtolower(trim($language)); $language = strtolower(trim($language));

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -8,7 +8,8 @@
"fr": "Sentinelle", "fr": "Sentinelle",
"jp": "センチネル" "jp": "センチネル"
}, },
"icon": "https://xivapi.com/i/000000/000151_hr1.png" "icon": "https://xivapi.com/i/000000/000151_hr1.png",
"shield": null
}, },
"22": { "22": {
"cast": 0, "cast": 0,
@ -19,7 +20,8 @@
"fr": "Forteresse", "fr": "Forteresse",
"jp": "ブルワーク" "jp": "ブルワーク"
}, },
"icon": "https://xivapi.com/i/000000/000167_hr1.png" "icon": "https://xivapi.com/i/000000/000167_hr1.png",
"shield": null
}, },
"30": { "30": {
"cast": 0, "cast": 0,
@ -30,7 +32,8 @@
"fr": "Invincible", "fr": "Invincible",
"jp": "インビンシブル" "jp": "インビンシブル"
}, },
"icon": "https://xivapi.com/i/002000/002502_hr1.png" "icon": "https://xivapi.com/i/002000/002502_hr1.png",
"shield": null
}, },
"40": { "40": {
"cast": 0, "cast": 0,
@ -41,7 +44,8 @@
"fr": "Frisson de la bataille", "fr": "Frisson de la bataille",
"jp": "スリル・オブ・バトル" "jp": "スリル・オブ・バトル"
}, },
"icon": "https://xivapi.com/i/000000/000263_hr1.png" "icon": "https://xivapi.com/i/000000/000263_hr1.png",
"shield": null
}, },
"43": { "43": {
"cast": 0, "cast": 0,
@ -52,7 +56,8 @@
"fr": "Holmgang", "fr": "Holmgang",
"jp": "ホルムギャング" "jp": "ホルムギャング"
}, },
"icon": "https://xivapi.com/i/000000/000266_hr1.png" "icon": "https://xivapi.com/i/000000/000266_hr1.png",
"shield": null
}, },
"44": { "44": {
"cast": 0, "cast": 0,
@ -63,7 +68,8 @@
"fr": "Représailles", "fr": "Représailles",
"jp": "ヴェンジェンス" "jp": "ヴェンジェンス"
}, },
"icon": "https://xivapi.com/i/000000/000267_hr1.png" "icon": "https://xivapi.com/i/000000/000267_hr1.png",
"shield": null
}, },
"157": { "157": {
"cast": 0, "cast": 0,
@ -74,7 +80,8 @@
"fr": "Barrière de mana", "fr": "Barrière de mana",
"jp": "マバリア" "jp": "マバリア"
}, },
"icon": "https://xivapi.com/i/000000/000463_hr1.png" "icon": "https://xivapi.com/i/000000/000463_hr1.png",
"shield": "30% max HP"
}, },
"185": { "185": {
"cast": 20, "cast": 20,
@ -85,7 +92,8 @@
"fr": "Traité du réconfort", "fr": "Traité du réconfort",
"jp": "鼓舞激励の策" "jp": "鼓舞激励の策"
}, },
"icon": "https://xivapi.com/i/002000/002801_hr1.png" "icon": "https://xivapi.com/i/002000/002801_hr1.png",
"shield": null
}, },
"188": { "188": {
"cast": 0, "cast": 0,
@ -96,7 +104,8 @@
"fr": "Dogme de survie", "fr": "Dogme de survie",
"jp": "野戦治療の陣" "jp": "野戦治療の陣"
}, },
"icon": "https://xivapi.com/i/002000/002804_hr1.png" "icon": "https://xivapi.com/i/002000/002804_hr1.png",
"shield": null
}, },
"2241": { "2241": {
"cast": 0, "cast": 0,
@ -107,7 +116,8 @@
"fr": "Décalage d'ombre", "fr": "Décalage d'ombre",
"jp": "残影" "jp": "残影"
}, },
"icon": "https://xivapi.com/i/000000/000607_hr1.png" "icon": "https://xivapi.com/i/000000/000607_hr1.png",
"shield": "20% max HP"
}, },
"3540": { "3540": {
"cast": 0, "cast": 0,
@ -118,7 +128,8 @@
"fr": "Voile divin", "fr": "Voile divin",
"jp": "ディヴァインヴェール" "jp": "ディヴァインヴェール"
}, },
"icon": "https://xivapi.com/i/002000/002508_hr1.png" "icon": "https://xivapi.com/i/002000/002508_hr1.png",
"shield": "10% max HP"
}, },
"3551": { "3551": {
"cast": 0, "cast": 0,
@ -129,7 +140,8 @@
"fr": "Intuition pure", "fr": "Intuition pure",
"jp": "原初の直感" "jp": "原初の直感"
}, },
"icon": "https://xivapi.com/i/002000/002559_hr1.png" "icon": "https://xivapi.com/i/002000/002559_hr1.png",
"shield": null
}, },
"3613": { "3613": {
"cast": 0, "cast": 0,
@ -140,7 +152,8 @@
"fr": "Inconscient collectif", "fr": "Inconscient collectif",
"jp": "運命の輪" "jp": "運命の輪"
}, },
"icon": "https://xivapi.com/i/003000/003140_hr1.png" "icon": "https://xivapi.com/i/003000/003140_hr1.png",
"shield": null
}, },
"3634": { "3634": {
"cast": 0, "cast": 0,
@ -151,7 +164,8 @@
"fr": "Esprit ténébreux", "fr": "Esprit ténébreux",
"jp": "ダークマインド" "jp": "ダークマインド"
}, },
"icon": "https://xivapi.com/i/003000/003076_hr1.png" "icon": "https://xivapi.com/i/003000/003076_hr1.png",
"shield": null
}, },
"3636": { "3636": {
"cast": 0, "cast": 0,
@ -162,7 +176,8 @@
"fr": "Mur d'ombre", "fr": "Mur d'ombre",
"jp": "シャドウウォール" "jp": "シャドウウォール"
}, },
"icon": "https://xivapi.com/i/003000/003075_hr1.png" "icon": "https://xivapi.com/i/003000/003075_hr1.png",
"shield": null
}, },
"3638": { "3638": {
"cast": 0, "cast": 0,
@ -173,7 +188,8 @@
"fr": "Mort-vivant", "fr": "Mort-vivant",
"jp": "リビングデッド" "jp": "リビングデッド"
}, },
"icon": "https://xivapi.com/i/003000/003077_hr1.png" "icon": "https://xivapi.com/i/003000/003077_hr1.png",
"shield": null
}, },
"7382": { "7382": {
"cast": 0, "cast": 0,
@ -184,7 +200,8 @@
"fr": "Intervention", "fr": "Intervention",
"jp": "インターベンション" "jp": "インターベンション"
}, },
"icon": "https://xivapi.com/i/002000/002512_hr1.png" "icon": "https://xivapi.com/i/002000/002512_hr1.png",
"shield": null
}, },
"7385": { "7385": {
"cast": 0, "cast": 0,
@ -195,7 +212,8 @@
"fr": "Passe d'armes", "fr": "Passe d'armes",
"jp": "パッセージ・オブ・アームズ" "jp": "パッセージ・オブ・アームズ"
}, },
"icon": "https://xivapi.com/i/002000/002515_hr1.png" "icon": "https://xivapi.com/i/002000/002515_hr1.png",
"shield": null
}, },
"7388": { "7388": {
"cast": 0, "cast": 0,
@ -206,7 +224,8 @@
"fr": "Débarrassage", "fr": "Débarrassage",
"jp": "シェイクオフ" "jp": "シェイクオフ"
}, },
"icon": "https://xivapi.com/i/002000/002563_hr1.png" "icon": "https://xivapi.com/i/002000/002563_hr1.png",
"shield": "15% max HP"
}, },
"7393": { "7393": {
"cast": 0, "cast": 0,
@ -217,7 +236,8 @@
"fr": "Nuit noirissime", "fr": "Nuit noirissime",
"jp": "ブラックナイト" "jp": "ブラックナイト"
}, },
"icon": "https://xivapi.com/i/003000/003081_hr1.png" "icon": "https://xivapi.com/i/003000/003081_hr1.png",
"shield": "25% max HP"
}, },
"7394": { "7394": {
"cast": 0, "cast": 0,
@ -228,7 +248,8 @@
"fr": "Énigme de la terre", "fr": "Énigme de la terre",
"jp": "金剛の極意" "jp": "金剛の極意"
}, },
"icon": "https://xivapi.com/i/002000/002537_hr1.png" "icon": "https://xivapi.com/i/002000/002537_hr1.png",
"shield": null
}, },
"7405": { "7405": {
"cast": 0, "cast": 0,
@ -239,7 +260,8 @@
"fr": "Troubadour", "fr": "Troubadour",
"jp": "トルバドゥール" "jp": "トルバドゥール"
}, },
"icon": "https://xivapi.com/i/002000/002612_hr1.png" "icon": "https://xivapi.com/i/002000/002612_hr1.png",
"shield": null
}, },
"7432": { "7432": {
"cast": 0, "cast": 0,
@ -250,7 +272,8 @@
"fr": "Faveur divine", "fr": "Faveur divine",
"jp": "ディヴァインベニゾン" "jp": "ディヴァインベニゾン"
}, },
"icon": "https://xivapi.com/i/002000/002638_hr1.png" "icon": "https://xivapi.com/i/002000/002638_hr1.png",
"shield": "500 potency"
}, },
"7498": { "7498": {
"cast": 0, "cast": 0,
@ -261,7 +284,8 @@
"fr": "Troisième œil", "fr": "Troisième œil",
"jp": "心眼" "jp": "心眼"
}, },
"icon": "https://xivapi.com/i/003000/003153_hr1.png" "icon": "https://xivapi.com/i/003000/003153_hr1.png",
"shield": null
}, },
"7531": { "7531": {
"cast": 0, "cast": 0,
@ -272,7 +296,8 @@
"fr": "Rempart", "fr": "Rempart",
"jp": "ランパート" "jp": "ランパート"
}, },
"icon": "https://xivapi.com/i/000000/000801_hr1.png" "icon": "https://xivapi.com/i/000000/000801_hr1.png",
"shield": null
}, },
"7535": { "7535": {
"cast": 0, "cast": 0,
@ -283,7 +308,8 @@
"fr": "Rétorsion", "fr": "Rétorsion",
"jp": "リプライザル" "jp": "リプライザル"
}, },
"icon": "https://xivapi.com/i/000000/000806_hr1.png" "icon": "https://xivapi.com/i/000000/000806_hr1.png",
"shield": null
}, },
"7549": { "7549": {
"cast": 0, "cast": 0,
@ -294,7 +320,8 @@
"fr": "Restreinte", "fr": "Restreinte",
"jp": "牽制" "jp": "牽制"
}, },
"icon": "https://xivapi.com/i/000000/000828_hr1.png" "icon": "https://xivapi.com/i/000000/000828_hr1.png",
"shield": null
}, },
"7560": { "7560": {
"cast": 0, "cast": 0,
@ -305,7 +332,8 @@
"fr": "Embrouillement", "fr": "Embrouillement",
"jp": "アドル" "jp": "アドル"
}, },
"icon": "https://xivapi.com/i/000000/000861_hr1.png" "icon": "https://xivapi.com/i/000000/000861_hr1.png",
"shield": null
}, },
"16012": { "16012": {
"cast": 0, "cast": 0,
@ -316,7 +344,8 @@
"fr": "Samba protectrice", "fr": "Samba protectrice",
"jp": "守りのサンバ" "jp": "守りのサンバ"
}, },
"icon": "https://xivapi.com/i/003000/003469_hr1.png" "icon": "https://xivapi.com/i/003000/003469_hr1.png",
"shield": null
}, },
"16140": { "16140": {
"cast": 0, "cast": 0,
@ -327,7 +356,8 @@
"fr": "Camouflage", "fr": "Camouflage",
"jp": "カモフラージュ" "jp": "カモフラージュ"
}, },
"icon": "https://xivapi.com/i/003000/003404_hr1.png" "icon": "https://xivapi.com/i/003000/003404_hr1.png",
"shield": null
}, },
"16148": { "16148": {
"cast": 0, "cast": 0,
@ -338,7 +368,8 @@
"fr": "Nébuleuse", "fr": "Nébuleuse",
"jp": "ネビュラ" "jp": "ネビュラ"
}, },
"icon": "https://xivapi.com/i/003000/003412_hr1.png" "icon": "https://xivapi.com/i/003000/003412_hr1.png",
"shield": null
}, },
"16152": { "16152": {
"cast": 0, "cast": 0,
@ -349,7 +380,8 @@
"fr": "Bolide", "fr": "Bolide",
"jp": "ボーライド" "jp": "ボーライド"
}, },
"icon": "https://xivapi.com/i/003000/003416_hr1.png" "icon": "https://xivapi.com/i/003000/003416_hr1.png",
"shield": null
}, },
"16160": { "16160": {
"cast": 0, "cast": 0,
@ -360,7 +392,8 @@
"fr": "Cœur de Lumière", "fr": "Cœur de Lumière",
"jp": "ハート・オブ・ライト" "jp": "ハート・オブ・ライト"
}, },
"icon": "https://xivapi.com/i/003000/003424_hr1.png" "icon": "https://xivapi.com/i/003000/003424_hr1.png",
"shield": null
}, },
"16161": { "16161": {
"cast": 0, "cast": 0,
@ -371,7 +404,8 @@
"fr": "Cœur de pierre", "fr": "Cœur de pierre",
"jp": "ハート・オブ・ストーン" "jp": "ハート・オブ・ストーン"
}, },
"icon": "https://xivapi.com/i/003000/003425_hr1.png" "icon": "https://xivapi.com/i/003000/003425_hr1.png",
"shield": null
}, },
"16471": { "16471": {
"cast": 0, "cast": 0,
@ -382,7 +416,8 @@
"fr": "Missionnaire des Ténèbres", "fr": "Missionnaire des Ténèbres",
"jp": "ダークミッショナリー" "jp": "ダークミッショナリー"
}, },
"icon": "https://xivapi.com/i/003000/003087_hr1.png" "icon": "https://xivapi.com/i/003000/003087_hr1.png",
"shield": null
}, },
"16536": { "16536": {
"cast": 0, "cast": 0,
@ -393,7 +428,8 @@
"fr": "Tempérance", "fr": "Tempérance",
"jp": "テンパランス" "jp": "テンパランス"
}, },
"icon": "https://xivapi.com/i/002000/002645_hr1.png" "icon": "https://xivapi.com/i/002000/002645_hr1.png",
"shield": null
}, },
"16538": { "16538": {
"cast": 0, "cast": 0,
@ -404,7 +440,8 @@
"fr": "Illumination féérique", "fr": "Illumination féérique",
"jp": "フェイイルミネーション" "jp": "フェイイルミネーション"
}, },
"icon": "https://xivapi.com/i/002000/002853_hr1.png" "icon": "https://xivapi.com/i/002000/002853_hr1.png",
"shield": null
}, },
"16548": { "16548": {
"cast": 0, "cast": 0,
@ -415,7 +452,8 @@
"fr": "Voile séraphique", "fr": "Voile séraphique",
"jp": "セラフィックヴェール" "jp": "セラフィックヴェール"
}, },
"icon": "https://xivapi.com/i/002000/002847_hr1.png" "icon": "https://xivapi.com/i/002000/002847_hr1.png",
"shield": null
}, },
"16556": { "16556": {
"cast": 0, "cast": 0,
@ -426,7 +464,8 @@
"fr": "Rencontre céleste", "fr": "Rencontre céleste",
"jp": "星天交差" "jp": "星天交差"
}, },
"icon": "https://xivapi.com/i/003000/003556_hr1.png" "icon": "https://xivapi.com/i/003000/003556_hr1.png",
"shield": "200% of HP restored"
}, },
"16559": { "16559": {
"cast": 0, "cast": 0,
@ -437,7 +476,8 @@
"fr": "Adepte de la neutralité", "fr": "Adepte de la neutralité",
"jp": "ニュートラルセクト" "jp": "ニュートラルセクト"
}, },
"icon": "https://xivapi.com/i/003000/003552_hr1.png" "icon": "https://xivapi.com/i/003000/003552_hr1.png",
"shield": "250% of HP restored"
}, },
"16889": { "16889": {
"cast": 0, "cast": 0,
@ -448,7 +488,8 @@
"fr": "Tacticien", "fr": "Tacticien",
"jp": "タクティシャン" "jp": "タクティシャン"
}, },
"icon": "https://xivapi.com/i/003000/003040_hr1.png" "icon": "https://xivapi.com/i/003000/003040_hr1.png",
"shield": null
}, },
"24291": { "24291": {
"cast": 0, "cast": 0,
@ -459,7 +500,8 @@
"fr": "Diagnosis eucrasique", "fr": "Diagnosis eucrasique",
"jp": "エウクラシア・ディアグノシス" "jp": "エウクラシア・ディアグノシス"
}, },
"icon": "https://xivapi.com/i/003000/003659_hr1.png" "icon": "https://xivapi.com/i/003000/003659_hr1.png",
"shield": null
}, },
"24292": { "24292": {
"cast": 0, "cast": 0,
@ -470,7 +512,8 @@
"fr": "Prognosis eucrasique", "fr": "Prognosis eucrasique",
"jp": "エウクラシア・プログノシス" "jp": "エウクラシア・プログノシス"
}, },
"icon": "https://xivapi.com/i/003000/003660_hr1.png" "icon": "https://xivapi.com/i/003000/003660_hr1.png",
"shield": "320% of HP restored"
}, },
"24298": { "24298": {
"cast": 0, "cast": 0,
@ -481,7 +524,8 @@
"fr": "Kerachole", "fr": "Kerachole",
"jp": "ケーラコレ" "jp": "ケーラコレ"
}, },
"icon": "https://xivapi.com/i/003000/003666_hr1.png" "icon": "https://xivapi.com/i/003000/003666_hr1.png",
"shield": null
}, },
"24305": { "24305": {
"cast": 0, "cast": 0,
@ -492,7 +536,8 @@
"fr": "Haima", "fr": "Haima",
"jp": "ハイマ" "jp": "ハイマ"
}, },
"icon": "https://xivapi.com/i/003000/003673_hr1.png" "icon": "https://xivapi.com/i/003000/003673_hr1.png",
"shield": "300 potency"
}, },
"24310": { "24310": {
"cast": 0, "cast": 0,
@ -503,7 +548,8 @@
"fr": "Holos", "fr": "Holos",
"jp": "ホーリズム" "jp": "ホーリズム"
}, },
"icon": "https://xivapi.com/i/003000/003678_hr1.png" "icon": "https://xivapi.com/i/003000/003678_hr1.png",
"shield": null
}, },
"24311": { "24311": {
"cast": 0, "cast": 0,
@ -514,7 +560,8 @@
"fr": "Panhaima", "fr": "Panhaima",
"jp": "パンハイマ" "jp": "パンハイマ"
}, },
"icon": "https://xivapi.com/i/003000/003679_hr1.png" "icon": "https://xivapi.com/i/003000/003679_hr1.png",
"shield": "200 potency"
}, },
"24404": { "24404": {
"cast": 0, "cast": 0,
@ -525,7 +572,8 @@
"fr": "Blason arcanique", "fr": "Blason arcanique",
"jp": "アルケインクレスト" "jp": "アルケインクレスト"
}, },
"icon": "https://xivapi.com/i/003000/003632_hr1.png" "icon": "https://xivapi.com/i/003000/003632_hr1.png",
"shield": "10% max HP"
}, },
"25746": { "25746": {
"cast": 0, "cast": 0,
@ -536,7 +584,8 @@
"fr": "Schiltron sacré", "fr": "Schiltron sacré",
"jp": "ホーリーシェルトロン" "jp": "ホーリーシェルトロン"
}, },
"icon": "https://xivapi.com/i/002000/002950_hr1.png" "icon": "https://xivapi.com/i/002000/002950_hr1.png",
"shield": null
}, },
"25751": { "25751": {
"cast": 0, "cast": 0,
@ -547,7 +596,8 @@
"fr": "Intuition fougueuse", "fr": "Intuition fougueuse",
"jp": "原初の血気" "jp": "原初の血気"
}, },
"icon": "https://xivapi.com/i/002000/002569_hr1.png" "icon": "https://xivapi.com/i/002000/002569_hr1.png",
"shield": "400 potency"
}, },
"25754": { "25754": {
"cast": 0, "cast": 0,
@ -558,7 +608,8 @@
"fr": "Oblation", "fr": "Oblation",
"jp": "オブレーション" "jp": "オブレーション"
}, },
"icon": "https://xivapi.com/i/003000/003089_hr1.png" "icon": "https://xivapi.com/i/003000/003089_hr1.png",
"shield": null
}, },
"25758": { "25758": {
"cast": 0, "cast": 0,
@ -569,7 +620,8 @@
"fr": "Cœur de corindon", "fr": "Cœur de corindon",
"jp": "ハート・オブ・コランダム" "jp": "ハート・オブ・コランダム"
}, },
"icon": "https://xivapi.com/i/003000/003430_hr1.png" "icon": "https://xivapi.com/i/003000/003430_hr1.png",
"shield": null
}, },
"25789": { "25789": {
"cast": 0, "cast": 0,
@ -580,7 +632,8 @@
"fr": "Final improvisé", "fr": "Final improvisé",
"jp": "インプロビゼーション・フィニッシュ" "jp": "インプロビゼーション・フィニッシュ"
}, },
"icon": "https://xivapi.com/i/003000/003479_hr1.png" "icon": "https://xivapi.com/i/003000/003479_hr1.png",
"shield": null
}, },
"25799": { "25799": {
"cast": 0, "cast": 0,
@ -591,7 +644,8 @@
"fr": "Égide rayonnante", "fr": "Égide rayonnante",
"jp": "守りの光" "jp": "守りの光"
}, },
"icon": "https://xivapi.com/i/002000/002750_hr1.png" "icon": "https://xivapi.com/i/002000/002750_hr1.png",
"shield": "20% max HP"
}, },
"25857": { "25857": {
"cast": 0, "cast": 0,
@ -602,7 +656,8 @@
"fr": "Barrière anti-magie", "fr": "Barrière anti-magie",
"jp": "バマジク" "jp": "バマジク"
}, },
"icon": "https://xivapi.com/i/003000/003237_hr1.png" "icon": "https://xivapi.com/i/003000/003237_hr1.png",
"shield": null
}, },
"25868": { "25868": {
"cast": 0, "cast": 0,
@ -613,7 +668,8 @@
"fr": "Thèse fluidique", "fr": "Thèse fluidique",
"jp": "疾風怒濤の計" "jp": "疾風怒濤の計"
}, },
"icon": "https://xivapi.com/i/002000/002878_hr1.png" "icon": "https://xivapi.com/i/002000/002878_hr1.png",
"shield": null
}, },
"34685": { "34685": {
"cast": 0, "cast": 0,
@ -624,7 +680,8 @@
"fr": "Enduit a tempera", "fr": "Enduit a tempera",
"jp": "テンペラコート" "jp": "テンペラコート"
}, },
"icon": "https://xivapi.com/i/003000/003835_hr1.png" "icon": "https://xivapi.com/i/003000/003835_hr1.png",
"shield": "20% max HP"
}, },
"34686": { "34686": {
"cast": 0, "cast": 0,
@ -635,7 +692,8 @@
"fr": "Tempera grassa", "fr": "Tempera grassa",
"jp": "テンペラグラッサ" "jp": "テンペラグラッサ"
}, },
"icon": "https://xivapi.com/i/003000/003836_hr1.png" "icon": "https://xivapi.com/i/003000/003836_hr1.png",
"shield": "10% max HP"
}, },
"36920": { "36920": {
"cast": 0, "cast": 0,
@ -646,7 +704,8 @@
"fr": "Garde extrême", "fr": "Garde extrême",
"jp": "エクストリームガード" "jp": "エクストリームガード"
}, },
"icon": "https://xivapi.com/i/002000/002524_hr1.png" "icon": "https://xivapi.com/i/002000/002524_hr1.png",
"shield": null
}, },
"36923": { "36923": {
"cast": 0, "cast": 0,
@ -657,7 +716,8 @@
"fr": "Damnation", "fr": "Damnation",
"jp": "ダムネーション" "jp": "ダムネーション"
}, },
"icon": "https://xivapi.com/i/002000/002573_hr1.png" "icon": "https://xivapi.com/i/002000/002573_hr1.png",
"shield": null
}, },
"36927": { "36927": {
"cast": 0, "cast": 0,
@ -668,7 +728,8 @@
"fr": "Vigile ténébreux", "fr": "Vigile ténébreux",
"jp": "シャドウヴィジル" "jp": "シャドウヴィジル"
}, },
"icon": "https://xivapi.com/i/003000/003094_hr1.png" "icon": "https://xivapi.com/i/003000/003094_hr1.png",
"shield": null
}, },
"36935": { "36935": {
"cast": 0, "cast": 0,
@ -679,7 +740,8 @@
"fr": "Grande nébuleuse", "fr": "Grande nébuleuse",
"jp": "グレートネビュラ" "jp": "グレートネビュラ"
}, },
"icon": "https://xivapi.com/i/003000/003435_hr1.png" "icon": "https://xivapi.com/i/003000/003435_hr1.png",
"shield": null
}, },
"37011": { "37011": {
"cast": 0, "cast": 0,
@ -690,7 +752,8 @@
"fr": "Caresse divine", "fr": "Caresse divine",
"jp": "ディヴァインカレス" "jp": "ディヴァインカレス"
}, },
"icon": "https://xivapi.com/i/002000/002128_hr1.png" "icon": "https://xivapi.com/i/002000/002128_hr1.png",
"shield": "400 potency"
}, },
"37025": { "37025": {
"cast": 0, "cast": 0,
@ -701,7 +764,8 @@
"fr": "La Tour", "fr": "La Tour",
"jp": "ビエルゴの塔" "jp": "ビエルゴの塔"
}, },
"icon": "https://xivapi.com/i/003000/003115_hr1.png" "icon": "https://xivapi.com/i/003000/003115_hr1.png",
"shield": "400 potency"
}, },
"37034": { "37034": {
"cast": 0, "cast": 0,
@ -712,6 +776,7 @@
"fr": "Prognosis eucrasique II", "fr": "Prognosis eucrasique II",
"jp": "エウクラシア・プログシスII" "jp": "エウクラシア・プログシスII"
}, },
"icon": "https://xivapi.com/i/003000/003689_hr1.png" "icon": "https://xivapi.com/i/003000/003689_hr1.png",
"shield": "360% of HP restored"
} }
} }

View File

@ -69,6 +69,7 @@
{ name: 'Camouflage', buffType: 'buff', extraAbilityGameID: 16140, duration: 20 }, { name: 'Camouflage', buffType: 'buff', extraAbilityGameID: 16140, duration: 20 },
{ name: 'Heart of Stone', buffType: 'buff', extraAbilityGameID: 16161, duration: 7 }, { name: 'Heart of Stone', buffType: 'buff', extraAbilityGameID: 16161, duration: 7 },
{ name: 'Heart of Corundum', buffType: 'buff', extraAbilityGameID: 25758, duration: 8 }, { 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' }, { name: 'Reprisal', buffType: 'debuff' },
], ],
'WHM': [ 'WHM': [
@ -156,7 +157,7 @@
'Oblation': 'DRK', 'Oblation': 'DRK',
'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB', 'Heart of Light': 'GNB', 'Superbolide': 'GNB', 'Nebula': 'GNB',
'Great Nebula': 'GNB', 'Camouflage': 'GNB', 'Heart of Stone': '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', 'Temperance': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH', 'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH',
'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH', 'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH',
@ -219,6 +220,9 @@
'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png', 'Tempera Coat': 'assets/icons/mitigation/tempera-coat.png',
'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png', 'Tempera Grassa': 'assets/icons/mitigation/tempera-grassa.png',
'Improvised Finish': 'assets/icons/mitigation/improvised-finish.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 = { const ABILITY_DR = {
@ -239,6 +243,29 @@
'Reprisal': 0.10, 'Reprisal': 0.10,
'Feint': 0.05, 'Feint': 0.05,
'Addle': 0.10, 'Addle': 0.10,
'Rampart': 0.20,
'Hallowed Ground': 1.00,
'Sentinel': 0.30,
'Guardian': 0.40,
'Holy Sheltron': 0.15,
'Intervention': 0.10,
'Vengeance': 0.30,
'Damnation': 0.40,
'Raw Intuition': 0.10,
'Bloodwhetting': 0.10,
'Shadow Wall': 0.30,
'Shadowed Vigil': 0.40,
'Dark Mind': 0.20,
'Oblation': 0.10,
'Superbolide': 1.00,
'Nebula': 0.30,
'Great Nebula': 0.40,
'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,
}; };
const ABILITY_JOBS = {}; const ABILITY_JOBS = {};

View File

@ -150,6 +150,21 @@ function abilityIcon(ability) {
return MITIG_ICONS[ability] ?? actionMetaByName[ability]?.icon ?? ''; return MITIG_ICONS[ability] ?? actionMetaByName[ability]?.icon ?? '';
} }
function abilityShieldText(ability) {
return actionMetaByName[ability]?.shield ?? '';
}
function abilityEffectTooltip(ability, buffType, plan = null) {
const lines = [localizedAbilityName(ability, plan)];
const dr = ABILITY_DR[ability] ?? 0;
const shield = abilityShieldText(ability);
if (dr > 0) lines.push(`Mitigation: ${Math.round(dr * 100)}%`);
if (shield) lines.push(`Shield: ${shield}`);
else if (buffType === 'shield') lines.push('Shield');
if (dr <= 0 && !shield && buffType !== 'shield') lines.push('No direct damage reduction');
return lines.join('\n');
}
async function ensureActionMetaLoaded() { async function ensureActionMetaLoaded() {
if (actionMetaPromise) return actionMetaPromise; if (actionMetaPromise) return actionMetaPromise;
actionMetaPromise = (async () => { actionMetaPromise = (async () => {
@ -179,6 +194,7 @@ async function ensureActionMetaLoaded() {
jp: action.Name_ja ?? '', jp: action.Name_ja ?? '',
}, },
icon: compact?.[id]?.icon ?? null, icon: compact?.[id]?.icon ?? null,
shield: compact?.[id]?.shield ?? '',
cast: (parseInt(compact?.[id]?.cast ?? action.Cast100ms ?? 0, 10) || 0) / 10, cast: (parseInt(compact?.[id]?.cast ?? action.Cast100ms ?? 0, 10) || 0) / 10,
recast: (parseInt(compact?.[id]?.recast ?? action.Recast100ms ?? 0, 10) || 0) / 10, recast: (parseInt(compact?.[id]?.recast ?? action.Recast100ms ?? 0, 10) || 0) / 10,
duration: durations.find(Number.isFinite) ?? 15, duration: durations.find(Number.isFinite) ?? 15,
@ -193,10 +209,11 @@ async function ensureActionMetaLoaded() {
name: byId[id]?.name ?? names.en ?? '', name: byId[id]?.name ?? names.en ?? '',
names: { ...(byId[id]?.names ?? {}), ...names }, names: { ...(byId[id]?.names ?? {}), ...names },
icon: byId[id]?.icon ?? action.icon ?? '', icon: byId[id]?.icon ?? action.icon ?? '',
shield: byId[id]?.shield ?? action.shield ?? '',
cast: (parseInt(action.cast ?? 0, 10) || 0) / 10, cast: (parseInt(action.cast ?? 0, 10) || 0) / 10,
recast: (parseInt(action.recast ?? 0, 10) || 0) / 10, recast: (parseInt(action.recast ?? 0, 10) || 0) / 10,
}; };
if (names.en && !byName[names.en]) byName[names.en] = byId[id]; if (names.en) byName[names.en] = byId[id];
} }
actionMetaById = byId; actionMetaById = byId;
actionMetaByName = byName; actionMetaByName = byName;
@ -476,10 +493,40 @@ function avgNonTankMaxHp(plan) {
return Math.round(hps.reduce((s, v) => s + v, 0) / hps.length); 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 ?? []) { function simulateDrMultiplier(mechanic, assignments = mechanic.assignments ?? []) {
const isTankbuster = !!mechanic.isHeavyTankbuster;
let mult = 1; let mult = 1;
for (const a of assignments) { for (const a of assignments) {
if (a.buffType === 'shield') continue; 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)); mult *= (1 - (ABILITY_DR[a.ability] ?? 0));
} }
return mult; return mult;
@ -493,6 +540,13 @@ function plannedAssignmentsForMechanic(plan, targetMechanic) {
for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) { for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) {
if (targetTime < entry.start - tolerance || targetTime > entry.end + tolerance) continue; 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({ result.push({
...entry.assignment, ...entry.assignment,
sourceMechanicId: entry.mechanic.id, sourceMechanicId: entry.mechanic.id,
@ -518,9 +572,14 @@ function renderMechanicListHtml(plan) {
} }
const activeJobSet = new Set(plan.jobComposition.filter(j => j)); 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 => { 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 planned = plannedAssignmentsForMechanic(plan, m);
const sorted = sortedAssignments(planned); const sorted = sortedAssignments(planned);
const assignHtml = sorted.length === 0 const assignHtml = sorted.length === 0
@ -571,7 +630,7 @@ function renderMechanicListHtml(plan) {
: '' : ''
} }
${hasDrAssign || hasShield ? `<div class="mechanic-dmg mechanic-mitig-row"> ${hasDrAssign || hasShield ? `<div class="mechanic-dmg mechanic-mitig-row">
${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> mitigiert` : ''} ${hasDrAssign ? `<span class="mechanic-mitig-val${drOnlyCls ? ' ' + drOnlyCls : ''}">→ ${fmtNumber(drOnly)}</span> nach DR` : ''}
${hasShield ? `<span class="mechanic-mitig-shield${fullCls ? ' ' + fullCls : ''}">Mitigation mit Schild ${fmtNumber(mitigFull)}</span>` : ''} ${hasShield ? `<span class="mechanic-mitig-shield${fullCls ? ' ' + fullCls : ''}">Mitigation mit Schild ${fmtNumber(mitigFull)}</span>` : ''}
</div>` : ''} </div>` : ''}
<div class="mechanic-assignments">${assignHtml}</div> <div class="mechanic-assignments">${assignHtml}</div>
@ -694,6 +753,7 @@ const TIMELINE_PERSONAL_ABILITIES = new Set([
'Camouflage', 'Camouflage',
'Heart of Stone', 'Heart of Stone',
'Heart of Corundum', 'Heart of Corundum',
'Clarity of Corundum',
'Divine Benison', 'Divine Benison',
'Intersection', 'Intersection',
'the Spire', 'the Spire',
@ -1115,12 +1175,13 @@ function renderTimelineHtml(plan) {
} }
const icon = abilityIcon(row.ability); const icon = abilityIcon(row.ability);
const abilityDisplayName = localizedAbilityName(row.ability, plan); const abilityDisplayName = localizedAbilityName(row.ability, plan);
const abilityTooltip = abilityEffectTooltip(row.ability, row.buffType, plan);
const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : ''; const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : '';
return ` return `
<div class="timeline-row timeline-player-row${jobStartCls}" data-row-idx="${row.idx}" data-job="${escHtml(row.job)}" data-ability="${escHtml(row.ability)}"> <div class="timeline-row timeline-player-row${jobStartCls}" data-row-idx="${row.idx}" data-job="${escHtml(row.job)}" data-ability="${escHtml(row.ability)}">
<div class="timeline-row-label"> <div class="timeline-row-label">
<span class="timeline-job role-${escHtml(row.role)}">${escHtml(row.job)}</span> <span class="timeline-job role-${escHtml(row.role)}">${escHtml(row.job)}</span>
<span class="timeline-row-ability"> <span class="timeline-row-ability" title="${escHtml(abilityTooltip)}">
${icon ? `<img src="${escHtml(icon)}" alt="" class="timeline-row-ability-icon">` : ''} ${icon ? `<img src="${escHtml(icon)}" alt="" class="timeline-row-ability-icon">` : ''}
<span class="timeline-row-ability-name">${escHtml(abilityDisplayName)}</span> <span class="timeline-row-ability-name">${escHtml(abilityDisplayName)}</span>
</span> </span>
@ -2249,6 +2310,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
phase: phase?.name ?? '', phase: phase?.name ?? '',
unmitigatedDamage: avgUnmit, unmitigatedDamage: avgUnmit,
isHeavyTankbuster: !!ev.isHeavyTankbuster, isHeavyTankbuster: !!ev.isHeavyTankbuster,
tankMaxHp: tankMaxHpFromEvent(ev),
notes: '', notes: '',
assignments, assignments,
}; };
@ -2372,6 +2434,7 @@ async function refreshPlanLanguage(planId) {
name: match.abilityName ?? mechanic.name, name: match.abilityName ?? mechanic.name,
abilityId: match.abilityId ?? mechanic.abilityId, abilityId: match.abilityId ?? mechanic.abilityId,
isHeavyTankbuster: mechanic.mechanicTypeManual ? !!mechanic.isHeavyTankbuster : !!match.isHeavyTankbuster, isHeavyTankbuster: mechanic.mechanicTypeManual ? !!mechanic.isHeavyTankbuster : !!match.isHeavyTankbuster,
tankMaxHp: mechanic.mechanicTypeManual ? (mechanic.tankMaxHp ?? 0) : tankMaxHpFromEvent(match),
assignments, assignments,
}; };
}); });
@ -2462,11 +2525,12 @@ function renderAbilityModalContent() {
const icon = abilityIcon(ab.name); const icon = abilityIcon(ab.name);
const assignedName = assigned ? assignmentAbilityName(assigned, plan) : ''; const assignedName = assigned ? assignmentAbilityName(assigned, plan) : '';
const label = assignedName || localizedAbilityName(ab.name, plan); const label = assignedName || localizedAbilityName(ab.name, plan);
const effectTitle = [title, abilityEffectTooltip(ab.name, ab.buffType, plan)].filter(Boolean).join('\n\n');
return `<button class="ability-chip ${cls}${activeClass}${otherClass}" return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
data-ability="${escHtml(ab.name)}" data-ability="${escHtml(ab.name)}"
data-job="${escHtml(job)}" data-job="${escHtml(job)}"
data-buff-type="${escHtml(ab.buffType)}" data-buff-type="${escHtml(ab.buffType)}"
${title ? `title="${title}"` : ''} title="${escHtml(effectTitle)}"
>${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}${escHtml(label)}</button>`; >${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}${escHtml(label)}</button>`;
}).join(''); }).join('');

View File

@ -324,6 +324,42 @@ function action_icon_url(array $action): ?string
return 'https://xivapi.com/i/' . $matches[1] . '/' . $matches[2] . '.png'; return 'https://xivapi.com/i/' . $matches[1] . '/' . $matches[2] . '.png';
} }
function plain_action_text(?string $text): string
{
if ($text === null || $text === '') {
return '';
}
$text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
return trim((string)preg_replace('/\s+/u', ' ', $text));
}
function action_shield_text(array $action): ?string
{
$description = plain_action_text(action_text_field($action, 'Description_en'));
if ($description === '' || !preg_match('/barrier|absorbs|absorbed|nullif(?:y|ies)/i', $description)) {
return null;
}
if (preg_match('/(?:absorbs|absorb|nullifies|nullify)[^.]*?(?:totaling|up to)?\s*(\d+)%\s*(?:of\s*)?(?:your\s+|target\'s\s+)?maximum HP/i', $description, $m)) {
return $m[1] . '% max HP';
}
if (preg_match('/barrier[^.]*?(?:absorbs|absorb|nullifies|nullify)[^.]*?(\d+)%\s*(?:of\s*)?(?:your\s+|target\'s\s+)?maximum HP/i', $description, $m)) {
return $m[1] . '% max HP';
}
if (preg_match('/barrier[^.]*?(\d+)%\s+of\s+the\s+amount\s+of\s+HP\s+restored/i', $description, $m)) {
return $m[1] . '% of HP restored';
}
if (preg_match('/barrier[^.]*?(?:heal of|Cure Potency:)\s*(\d+)\s*potency/i', $description, $m)) {
return $m[1] . ' potency';
}
return null;
}
$plannerAbilityNames = read_planner_ability_names($plannerDataSource); $plannerAbilityNames = read_planner_ability_names($plannerDataSource);
$actionIds = read_mitigation_action_ids($mitigationSource, $plannerAbilityNames); $actionIds = read_mitigation_action_ids($mitigationSource, $plannerAbilityNames);
$wanted = array_fill_keys(array_map('strval', $actionIds), true); $wanted = array_fill_keys(array_map('strval', $actionIds), true);
@ -353,6 +389,7 @@ foreach ($wanted as $id => $_) {
'jp' => action_text_field($action, 'Name_ja'), 'jp' => action_text_field($action, 'Name_ja'),
], ],
'icon' => action_icon_url($action), 'icon' => action_icon_url($action),
'shield' => action_shield_text($action),
]; ];
} }