personal mitigation

This commit is contained in:
Akurosia Kamo 2026-05-24 11:23:01 +02:00
parent 646a7252c8
commit fc2dc513ca
6 changed files with 877 additions and 78 deletions

View File

@ -98,6 +98,40 @@ const MITIGATION_ABILITIES = [
'Tactician' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001951, 'extraAbilityGameID' => 16889],
'Shield Samba' => ['dr' => 15, 'buffType' => 'buff', 'statusId' => 1001826, 'extraAbilityGameID' => 16012],
'Magick Barrier' => ['dr' => 10, 'buffType' => 'buff', 'statusId' => 1002707, 'extraAbilityGameID' => 25857],
// ── Personal / targeted mitigation ─────────────────────────────────────
'Rampart' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7531],
// PLD
'Hallowed Ground' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 30],
'Sentinel' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 17],
'Bulwark' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 22],
'Holy Sheltron' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25746],
'Intervention' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7382],
// WAR
'Holmgang' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 43],
'Vengeance' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 44],
'Damnation' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36923],
'Thrill of Battle' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 40],
'Raw Intuition' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3551],
// DRK
'Living Dead' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3638],
'Shadow Wall' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3636],
'Shadowed Vigil' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36927],
'Dark Mind' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 3634],
'The Blackest Night' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 7393],
'Oblation' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25754],
// GNB
'Superbolide' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16152],
'Nebula' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16148],
'Great Nebula' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 36935],
'Camouflage' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16140],
'Heart of Stone' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 16161],
'Heart of Corundum' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 25758],
// DPS
'Riddle of Earth' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7394],
'Shade Shift' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 2241],
'Third Eye' => ['dr' => 0, 'buffType' => 'buff', 'extraAbilityGameID' => 7498],
'Arcane Crest' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 24404],
'Manaward' => ['dr' => 0, 'buffType' => 'shield', 'extraAbilityGameID' => 157],
// ── Shields ─────────────────────────────────────────────────────────────
// PLD
'Divine Veil' => ['dr' => 0, 'buffType' => 'shield', 'statusId' => 1001362, 'extraAbilityGameID' => 3540],

View File

@ -1,150 +1,717 @@
{
"17": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Sentinel",
"de": "Sentinel",
"fr": "Sentinelle",
"jp": "センチネル"
},
"icon": "https://xivapi.com/i/000000/000151_hr1.png"
},
"22": {
"cast": 0,
"recast": 900,
"names": {
"en": "Bulwark",
"de": "Bollwerk",
"fr": "Forteresse",
"jp": "ブルワーク"
},
"icon": "https://xivapi.com/i/000000/000167_hr1.png"
},
"30": {
"cast": 0,
"recast": 4200,
"names": {
"en": "Hallowed Ground",
"de": "Heiliger Boden",
"fr": "Invincible",
"jp": "インビンシブル"
},
"icon": "https://xivapi.com/i/002000/002502_hr1.png"
},
"40": {
"cast": 0,
"recast": 900,
"names": {
"en": "Thrill of Battle",
"de": "Kampfrausch",
"fr": "Frisson de la bataille",
"jp": "スリル・オブ・バトル"
},
"icon": "https://xivapi.com/i/000000/000263_hr1.png"
},
"43": {
"cast": 0,
"recast": 2400,
"names": {
"en": "Holmgang",
"de": "Holmgang",
"fr": "Holmgang",
"jp": "ホルムギャング"
},
"icon": "https://xivapi.com/i/000000/000266_hr1.png"
},
"44": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Vengeance",
"de": "Rachsucht",
"fr": "Représailles",
"jp": "ヴェンジェンス"
},
"icon": "https://xivapi.com/i/000000/000267_hr1.png"
},
"157": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Manaward",
"de": "Mana-Schild",
"fr": "Barrière de mana",
"jp": "マバリア"
},
"icon": "https://xivapi.com/i/000000/000463_hr1.png"
},
"185": {
"cast": 20,
"recast": 25
"recast": 25,
"names": {
"en": "Adloquium",
"de": "Adloquium",
"fr": "Traité du réconfort",
"jp": "鼓舞激励の策"
},
"icon": "https://xivapi.com/i/002000/002801_hr1.png"
},
"188": {
"cast": 0,
"recast": 300
"recast": 300,
"names": {
"en": "Sacred Soil",
"de": "Geweihte Erde",
"fr": "Dogme de survie",
"jp": "野戦治療の陣"
},
"icon": "https://xivapi.com/i/002000/002804_hr1.png"
},
"2241": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Shade Shift",
"de": "Superkniff",
"fr": "Décalage d'ombre",
"jp": "残影"
},
"icon": "https://xivapi.com/i/000000/000607_hr1.png"
},
"3540": {
"cast": 0,
"recast": 900
"recast": 900,
"names": {
"en": "Divine Veil",
"de": "Heiliger Quell",
"fr": "Voile divin",
"jp": "ディヴァインヴェール"
},
"icon": "https://xivapi.com/i/002000/002508_hr1.png"
},
"3551": {
"cast": 0,
"recast": 250,
"names": {
"en": "Raw Intuition",
"de": "Urinstinkt",
"fr": "Intuition pure",
"jp": "原初の直感"
},
"icon": "https://xivapi.com/i/002000/002559_hr1.png"
},
"3613": {
"cast": 0,
"recast": 600
"recast": 600,
"names": {
"en": "Collective Unconscious",
"de": "Numinosum",
"fr": "Inconscient collectif",
"jp": "運命の輪"
},
"icon": "https://xivapi.com/i/003000/003140_hr1.png"
},
"3634": {
"cast": 0,
"recast": 600,
"names": {
"en": "Dark Mind",
"de": "Dunkler Geist",
"fr": "Esprit ténébreux",
"jp": "ダークマインド"
},
"icon": "https://xivapi.com/i/003000/003076_hr1.png"
},
"3636": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Shadow Wall",
"de": "Schattenwand",
"fr": "Mur d'ombre",
"jp": "シャドウウォール"
},
"icon": "https://xivapi.com/i/003000/003075_hr1.png"
},
"3638": {
"cast": 0,
"recast": 3000,
"names": {
"en": "Living Dead",
"de": "Totenerweckung",
"fr": "Mort-vivant",
"jp": "リビングデッド"
},
"icon": "https://xivapi.com/i/003000/003077_hr1.png"
},
"7382": {
"cast": 0,
"recast": 100,
"names": {
"en": "Intervention",
"de": "Intervention",
"fr": "Intervention",
"jp": "インターベンション"
},
"icon": "https://xivapi.com/i/002000/002512_hr1.png"
},
"7385": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Passage of Arms",
"de": "Waffengang",
"fr": "Passe d'armes",
"jp": "パッセージ・オブ・アームズ"
},
"icon": "https://xivapi.com/i/002000/002515_hr1.png"
},
"7388": {
"cast": 0,
"recast": 900
"recast": 900,
"names": {
"en": "Shake It Off",
"de": "Abschütteln",
"fr": "Débarrassage",
"jp": "シェイクオフ"
},
"icon": "https://xivapi.com/i/002000/002563_hr1.png"
},
"7393": {
"cast": 0,
"recast": 150,
"names": {
"en": "The Blackest Night",
"de": "Schwärzeste Nacht",
"fr": "Nuit noirissime",
"jp": "ブラックナイト"
},
"icon": "https://xivapi.com/i/003000/003081_hr1.png"
},
"7394": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Riddle of Earth",
"de": "Steinernes Enigma",
"fr": "Énigme de la terre",
"jp": "金剛の極意"
},
"icon": "https://xivapi.com/i/002000/002537_hr1.png"
},
"7405": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Troubadour",
"de": "Troubadour",
"fr": "Troubadour",
"jp": "トルバドゥール"
},
"icon": "https://xivapi.com/i/002000/002612_hr1.png"
},
"7432": {
"cast": 0,
"recast": 300
"recast": 300,
"names": {
"en": "Divine Benison",
"de": "Göttlicher Segen",
"fr": "Faveur divine",
"jp": "ディヴァインベニゾン"
},
"icon": "https://xivapi.com/i/002000/002638_hr1.png"
},
"7498": {
"cast": 0,
"recast": 150,
"names": {
"en": "Third Eye",
"de": "Drittes Auge",
"fr": "Troisième œil",
"jp": "心眼"
},
"icon": "https://xivapi.com/i/003000/003153_hr1.png"
},
"7531": {
"cast": 0,
"recast": 900,
"names": {
"en": "Rampart",
"de": "Schutzwall",
"fr": "Rempart",
"jp": "ランパート"
},
"icon": "https://xivapi.com/i/000000/000801_hr1.png"
},
"7535": {
"cast": 0,
"recast": 600
"recast": 600,
"names": {
"en": "Reprisal",
"de": "Reflexion",
"fr": "Rétorsion",
"jp": "リプライザル"
},
"icon": "https://xivapi.com/i/000000/000806_hr1.png"
},
"7549": {
"cast": 0,
"recast": 900
"recast": 900,
"names": {
"en": "Feint",
"de": "Zermürben",
"fr": "Restreinte",
"jp": "牽制"
},
"icon": "https://xivapi.com/i/000000/000828_hr1.png"
},
"7560": {
"cast": 0,
"recast": 900
"recast": 900,
"names": {
"en": "Addle",
"de": "Stumpfsinn",
"fr": "Embrouillement",
"jp": "アドル"
},
"icon": "https://xivapi.com/i/000000/000861_hr1.png"
},
"16012": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Shield Samba",
"de": "Schildsamba",
"fr": "Samba protectrice",
"jp": "守りのサンバ"
},
"icon": "https://xivapi.com/i/003000/003469_hr1.png"
},
"16140": {
"cast": 0,
"recast": 900,
"names": {
"en": "Camouflage",
"de": "Camouflage",
"fr": "Camouflage",
"jp": "カモフラージュ"
},
"icon": "https://xivapi.com/i/003000/003404_hr1.png"
},
"16148": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Nebula",
"de": "Nebula",
"fr": "Nébuleuse",
"jp": "ネビュラ"
},
"icon": "https://xivapi.com/i/003000/003412_hr1.png"
},
"16152": {
"cast": 0,
"recast": 3600,
"names": {
"en": "Superbolide",
"de": "Meteoritenfall",
"fr": "Bolide",
"jp": "ボーライド"
},
"icon": "https://xivapi.com/i/003000/003416_hr1.png"
},
"16160": {
"cast": 0,
"recast": 900
"recast": 900,
"names": {
"en": "Heart of Light",
"de": "Wackeres Herz",
"fr": "Cœur de Lumière",
"jp": "ハート・オブ・ライト"
},
"icon": "https://xivapi.com/i/003000/003424_hr1.png"
},
"16161": {
"cast": 0,
"recast": 250,
"names": {
"en": "Heart of Stone",
"de": "Steinernes Herz",
"fr": "Cœur de pierre",
"jp": "ハート・オブ・ストーン"
},
"icon": "https://xivapi.com/i/003000/003425_hr1.png"
},
"16471": {
"cast": 0,
"recast": 900
"recast": 900,
"names": {
"en": "Dark Missionary",
"de": "Dunkler Bote",
"fr": "Missionnaire des Ténèbres",
"jp": "ダークミッショナリー"
},
"icon": "https://xivapi.com/i/003000/003087_hr1.png"
},
"16536": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Temperance",
"de": "Linderung",
"fr": "Tempérance",
"jp": "テンパランス"
},
"icon": "https://xivapi.com/i/002000/002645_hr1.png"
},
"16538": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Fey Illumination",
"de": "Illumination",
"fr": "Illumination féérique",
"jp": "フェイイルミネーション"
},
"icon": "https://xivapi.com/i/002000/002853_hr1.png"
},
"16548": {
"cast": 0,
"recast": 30
"recast": 30,
"names": {
"en": "Seraphic Veil",
"de": "Schleier der Seraphim",
"fr": "Voile séraphique",
"jp": "セラフィックヴェール"
},
"icon": "https://xivapi.com/i/002000/002847_hr1.png"
},
"16556": {
"cast": 0,
"recast": 300
"recast": 300,
"names": {
"en": "Celestial Intersection",
"de": "Kongruenz",
"fr": "Rencontre céleste",
"jp": "星天交差"
},
"icon": "https://xivapi.com/i/003000/003556_hr1.png"
},
"16559": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Neutral Sect",
"de": "Neutral",
"fr": "Adepte de la neutralité",
"jp": "ニュートラルセクト"
},
"icon": "https://xivapi.com/i/003000/003552_hr1.png"
},
"16889": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Tactician",
"de": "Taktiker",
"fr": "Tacticien",
"jp": "タクティシャン"
},
"icon": "https://xivapi.com/i/003000/003040_hr1.png"
},
"24291": {
"cast": 0,
"recast": 15
"recast": 15,
"names": {
"en": "Eukrasian Diagnosis",
"de": "Eukratische Diagnose",
"fr": "Diagnosis eucrasique",
"jp": "エウクラシア・ディアグノシス"
},
"icon": "https://xivapi.com/i/003000/003659_hr1.png"
},
"24292": {
"cast": 0,
"recast": 15
"recast": 15,
"names": {
"en": "Eukrasian Prognosis",
"de": "Eukratische Prognose",
"fr": "Prognosis eucrasique",
"jp": "エウクラシア・プログノシス"
},
"icon": "https://xivapi.com/i/003000/003660_hr1.png"
},
"24298": {
"cast": 0,
"recast": 300
"recast": 300,
"names": {
"en": "Kerachole",
"de": "Kerachole",
"fr": "Kerachole",
"jp": "ケーラコレ"
},
"icon": "https://xivapi.com/i/003000/003666_hr1.png"
},
"24305": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Haima",
"de": "Haima",
"fr": "Haima",
"jp": "ハイマ"
},
"icon": "https://xivapi.com/i/003000/003673_hr1.png"
},
"24310": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Holos",
"de": "Holos",
"fr": "Holos",
"jp": "ホーリズム"
},
"icon": "https://xivapi.com/i/003000/003678_hr1.png"
},
"24311": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Panhaima",
"de": "Panhaima",
"fr": "Panhaima",
"jp": "パンハイマ"
},
"icon": "https://xivapi.com/i/003000/003679_hr1.png"
},
"24404": {
"cast": 0,
"recast": 300,
"names": {
"en": "Arcane Crest",
"de": "Arkanes Wappen",
"fr": "Blason arcanique",
"jp": "アルケインクレスト"
},
"icon": "https://xivapi.com/i/003000/003632_hr1.png"
},
"25746": {
"cast": 0,
"recast": 50,
"names": {
"en": "Holy Sheltron",
"de": "Heiliges Schiltron",
"fr": "Schiltron sacré",
"jp": "ホーリーシェルトロン"
},
"icon": "https://xivapi.com/i/002000/002950_hr1.png"
},
"25751": {
"cast": 0,
"recast": 250
"recast": 250,
"names": {
"en": "Bloodwhetting",
"de": "Urimpuls",
"fr": "Intuition fougueuse",
"jp": "原初の血気"
},
"icon": "https://xivapi.com/i/002000/002569_hr1.png"
},
"25754": {
"cast": 0,
"recast": 600,
"names": {
"en": "Oblation",
"de": "Opfergabe",
"fr": "Oblation",
"jp": "オブレーション"
},
"icon": "https://xivapi.com/i/003000/003089_hr1.png"
},
"25758": {
"cast": 0,
"recast": 250,
"names": {
"en": "Heart of Corundum",
"de": "Herz des Korunds",
"fr": "Cœur de corindon",
"jp": "ハート・オブ・コランダム"
},
"icon": "https://xivapi.com/i/003000/003430_hr1.png"
},
"25789": {
"cast": 0,
"recast": 15
"recast": 15,
"names": {
"en": "Improvised Finish",
"de": "Improvisiertes Finale",
"fr": "Final improvisé",
"jp": "インプロビゼーション・フィニッシュ"
},
"icon": "https://xivapi.com/i/003000/003479_hr1.png"
},
"25799": {
"cast": 0,
"recast": 600
"recast": 600,
"names": {
"en": "Radiant Aegis",
"de": "Schimmerschild",
"fr": "Égide rayonnante",
"jp": "守りの光"
},
"icon": "https://xivapi.com/i/002000/002750_hr1.png"
},
"25857": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Magick Barrier",
"de": "Magiebarriere",
"fr": "Barrière anti-magie",
"jp": "バマジク"
},
"icon": "https://xivapi.com/i/003000/003237_hr1.png"
},
"25868": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Expedient",
"de": "Sturm und Drang",
"fr": "Thèse fluidique",
"jp": "疾風怒濤の計"
},
"icon": "https://xivapi.com/i/002000/002878_hr1.png"
},
"34685": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Tempera Coat",
"de": "Tempera-Schicht",
"fr": "Enduit a tempera",
"jp": "テンペラコート"
},
"icon": "https://xivapi.com/i/003000/003835_hr1.png"
},
"34686": {
"cast": 0,
"recast": 10
"recast": 10,
"names": {
"en": "Tempera Grassa",
"de": "Fette Tempera",
"fr": "Tempera grassa",
"jp": "テンペラグラッサ"
},
"icon": "https://xivapi.com/i/003000/003836_hr1.png"
},
"36920": {
"cast": 0,
"recast": 1200
"recast": 1200,
"names": {
"en": "Guardian",
"de": "Heilige Wacht",
"fr": "Garde extrême",
"jp": "エクストリームガード"
},
"icon": "https://xivapi.com/i/002000/002524_hr1.png"
},
"36923": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Damnation",
"de": "Verdammnis",
"fr": "Damnation",
"jp": "ダムネーション"
},
"icon": "https://xivapi.com/i/002000/002573_hr1.png"
},
"36927": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Shadowed Vigil",
"de": "Schattenwacht",
"fr": "Vigile ténébreux",
"jp": "シャドウヴィジル"
},
"icon": "https://xivapi.com/i/003000/003094_hr1.png"
},
"36935": {
"cast": 0,
"recast": 1200,
"names": {
"en": "Great Nebula",
"de": "Große Nebula",
"fr": "Grande nébuleuse",
"jp": "グレートネビュラ"
},
"icon": "https://xivapi.com/i/003000/003435_hr1.png"
},
"37011": {
"cast": 0,
"recast": 10
"recast": 10,
"names": {
"en": "Divine Caress",
"de": "Göttliche Umarmung",
"fr": "Caresse divine",
"jp": "ディヴァインカレス"
},
"icon": "https://xivapi.com/i/002000/002128_hr1.png"
},
"37025": {
"cast": 0,
"recast": 10
"recast": 10,
"names": {
"en": "the Spire",
"de": "Turm",
"fr": "La Tour",
"jp": "ビエルゴの塔"
},
"icon": "https://xivapi.com/i/003000/003115_hr1.png"
},
"37034": {
"cast": 0,
"recast": 15
"recast": 15,
"names": {
"en": "Eukrasian Prognosis II",
"de": "Eukratische Prognose II",
"fr": "Prognosis eucrasique II",
"jp": "エウクラシア・プログシスII"
},
"icon": "https://xivapi.com/i/003000/003689_hr1.png"
}
}

View File

@ -76,6 +76,27 @@
let extReportCode = '';
let mitigationNames = {};
let planRefId = '';
let actionIconPromise = null;
const actionIconsByName = {};
function mitigationIcon(m) {
return MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name] ?? actionIconsByName[m.key] ?? actionIconsByName[m.name] ?? '';
}
async function ensureActionIconCache() {
if (actionIconPromise) return actionIconPromise;
actionIconPromise = (async () => {
try {
const res = await fetch('assets/jsons/Action.json', { cache: 'no-cache' });
if (!res.ok) return;
const actions = await res.json();
for (const action of Object.values(actions ?? {})) {
if (action?.names?.en && action?.icon) actionIconsByName[action.names.en] = action.icon;
}
} catch { }
})();
return actionIconPromise;
}
// ── Player grid ──────────────────────────────────────────────────────────
@ -388,6 +409,7 @@
refEvents = json.aoe_events ?? [];
refFightStart = json.fight_start ?? fight.startTime;
refPlayers = json.players ?? [];
await ensureActionIconCache();
window.App.setUrlState?.({
compareReportCode: extReportCode,
compareFightId: refId,
@ -581,7 +603,7 @@
...eventDebuffs.map(m => ({ ...m, missing: false })),
...eventMissingDebuffs.map(m => ({ ...m, missing: true })),
].map(m => {
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
const iconSrc = mitigationIcon(m);
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
return m.missing
@ -608,7 +630,7 @@
// DR buff icons (shown below player box)
const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
const iconSrc = mitigationIcon(m);
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
@ -655,7 +677,7 @@
const refDebuffIconsHtml = refVisible.flatMap(t => (t.mitigations ?? []))
.filter(m => m.buffType === 'debuff' && !seenRefDebuffKeys.has(m.key ?? m.name) && seenRefDebuffKeys.add(m.key ?? m.name))
.map(m => {
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
const iconSrc = mitigationIcon(m);
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
@ -673,7 +695,7 @@
: '';
const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
const iconSrc = mitigationIcon(m);
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
const k = m.key ?? m.name;
@ -786,6 +808,7 @@
setupPhases(window.App?.phases ?? []);
renderPlayers(json.players ?? []);
mitigationNames = json.mitigation_names ?? {};
await ensureActionIconCache();
renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart);
document.getElementById('analysis-loading').style.display = 'none';

View File

@ -29,25 +29,51 @@
'PLD': [
{ name: 'Passage of Arms', buffType: 'buff' },
{ name: 'Divine Veil', buffType: 'shield' },
{ name: 'Guardian', buffType: 'shield' },
{ name: 'Rampart', buffType: 'buff', extraAbilityGameID: 7531, duration: 20 },
{ name: 'Hallowed Ground', buffType: 'buff', extraAbilityGameID: 30, duration: 10 },
{ name: 'Sentinel', buffType: 'buff', extraAbilityGameID: 17, duration: 15 },
{ name: 'Guardian', buffType: 'shield', extraAbilityGameID: 36920, duration: 15 },
{ 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: 'Reprisal', buffType: 'debuff' },
],
'WAR': [
{ name: 'Shake It Off', buffType: 'shield' },
{ name: 'Bloodwhetting', buffType: 'shield' },
{ name: 'Rampart', buffType: 'buff', extraAbilityGameID: 7531, duration: 20 },
{ name: 'Holmgang', buffType: 'buff', extraAbilityGameID: 43, duration: 10 },
{ name: 'Vengeance', buffType: 'buff', extraAbilityGameID: 44, duration: 15 },
{ name: 'Damnation', buffType: 'buff', extraAbilityGameID: 36923, duration: 15 },
{ 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: 'Reprisal', buffType: 'debuff' },
],
'DRK': [
{ name: 'Dark Missionary', buffType: 'buff' },
{ name: 'Rampart', buffType: 'buff', extraAbilityGameID: 7531, duration: 20 },
{ name: 'Living Dead', buffType: 'buff', extraAbilityGameID: 3638, duration: 10 },
{ name: 'Shadow Wall', buffType: 'buff', extraAbilityGameID: 3636, duration: 15 },
{ name: 'Shadowed Vigil', buffType: 'buff', extraAbilityGameID: 36927, duration: 15 },
{ name: 'Dark Mind', buffType: 'buff', extraAbilityGameID: 3634, duration: 10 },
{ name: 'The Blackest Night', buffType: 'shield', extraAbilityGameID: 7393, duration: 7 },
{ name: 'Oblation', buffType: 'buff', extraAbilityGameID: 25754, duration: 10 },
{ name: 'Reprisal', buffType: 'debuff' },
],
'GNB': [
{ name: 'Heart of Light', buffType: 'buff' },
{ name: 'Rampart', buffType: 'buff', extraAbilityGameID: 7531, duration: 20 },
{ name: 'Superbolide', buffType: 'buff', extraAbilityGameID: 16152, duration: 10 },
{ name: 'Nebula', buffType: 'buff', extraAbilityGameID: 16148, duration: 15 },
{ name: 'Great Nebula', buffType: 'buff', extraAbilityGameID: 36935, duration: 15 },
{ 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: 'Reprisal', buffType: 'debuff' },
],
'WHM': [
{ name: 'Temperance', buffType: 'buff' },
{ name: 'Divine Benison', buffType: 'shield' },
{ name: 'Divine Benison', buffType: 'shield', extraAbilityGameID: 7432, duration: 15 },
{ name: 'Divine Caress', buffType: 'shield' },
],
'SCH': [
@ -55,14 +81,14 @@
{ name: 'Expedient', buffType: 'buff' },
{ name: 'Fey Illumination', buffType: 'buff' },
{ name: 'Galvanize', buffType: 'shield' },
{ name: 'Seraphic Veil', buffType: 'shield' },
{ name: 'Seraphic Veil', buffType: 'shield', extraAbilityGameID: 16548, duration: 30 },
{ name: 'Catalyze', buffType: 'shield' },
],
'AST': [
{ name: 'Collective Unconscious', buffType: 'buff' },
{ name: 'Neutral Sect', buffType: 'shield' },
{ name: 'Intersection', buffType: 'shield' },
{ name: 'the Spire', buffType: 'shield' },
{ name: 'Intersection', buffType: 'shield', extraAbilityGameID: 16556, duration: 30 },
{ name: 'the Spire', buffType: 'shield', extraAbilityGameID: 37025, duration: 30 },
],
'SGE': [
{ name: 'Kerachole', buffType: 'buff' },
@ -71,9 +97,9 @@
{ name: 'Panhaima', buffType: 'shield' },
{ name: 'Eukrasian Prognosis', buffType: 'shield' },
{ name: 'Eukrasian Prognosis II', buffType: 'shield' },
{ name: 'Eukrasian Diagnosis', buffType: 'shield' },
{ name: 'Differential Diagnosis', buffType: 'shield' },
{ name: 'Haima', buffType: 'shield' },
{ name: 'Eukrasian Diagnosis', buffType: 'shield', extraAbilityGameID: 24291, duration: 30 },
{ name: 'Differential Diagnosis', buffType: 'shield', extraAbilityGameID: 24291, duration: 30 },
{ name: 'Haima', buffType: 'shield', extraAbilityGameID: 24305, duration: 15 },
],
'BRD': [{ name: 'Troubadour', buffType: 'buff' }],
'MCH': [{ name: 'Tactician', buffType: 'buff' }],
@ -81,16 +107,31 @@
{ name: 'Shield Samba', buffType: 'buff' },
{ name: 'Improvised Finish', buffType: 'shield' },
],
'MNK': [{ name: 'Feint', buffType: 'debuff' }],
'MNK': [
{ name: 'Riddle of Earth', buffType: 'buff', extraAbilityGameID: 7394, duration: 10 },
{ name: 'Feint', buffType: 'debuff' },
],
'DRG': [{ name: 'Feint', buffType: 'debuff' }],
'NIN': [{ name: 'Feint', buffType: 'debuff' }],
'SAM': [{ name: 'Feint', buffType: 'debuff' }],
'RPR': [{ name: 'Feint', buffType: 'debuff' }],
'NIN': [
{ name: 'Shade Shift', buffType: 'shield', extraAbilityGameID: 2241, duration: 20 },
{ name: 'Feint', buffType: 'debuff' },
],
'SAM': [
{ name: 'Third Eye', buffType: 'buff', extraAbilityGameID: 7498, duration: 4 },
{ name: 'Feint', buffType: 'debuff' },
],
'RPR': [
{ name: 'Arcane Crest', buffType: 'shield', extraAbilityGameID: 24404, duration: 5 },
{ name: 'Feint', buffType: 'debuff' },
],
'VPR': [{ name: 'Feint', buffType: 'debuff' }],
'BLM': [{ name: 'Addle', buffType: 'debuff' }],
'BLM': [
{ name: 'Manaward', buffType: 'shield', extraAbilityGameID: 157, duration: 20 },
{ name: 'Addle', buffType: 'debuff' },
],
'SMN': [
{ name: 'Addle', buffType: 'debuff' },
{ name: 'Radiant Aegis', buffType: 'shield' },
{ name: 'Radiant Aegis', buffType: 'shield', extraAbilityGameID: 25799, duration: 30 },
],
'RDM': [
{ name: 'Addle', buffType: 'debuff' },
@ -98,16 +139,24 @@
],
'PCT': [
{ name: 'Addle', buffType: 'debuff' },
{ name: 'Tempera Coat', buffType: 'shield' },
{ name: 'Tempera Coat', buffType: 'shield', extraAbilityGameID: 34685, duration: 10 },
{ name: 'Tempera Grassa', buffType: 'shield' },
],
};
const ABILITY_JOB_MAP = {
'Passage of Arms': 'PLD', 'Divine Veil': 'PLD', 'Guardian': 'PLD',
'Shake It Off': 'WAR', 'Bloodwhetting': 'WAR',
'Dark Missionary': 'DRK',
'Heart of Light': 'GNB',
'Passage of Arms': 'PLD', 'Divine Veil': 'PLD',
'Hallowed Ground': 'PLD', 'Sentinel': 'PLD', 'Guardian': 'PLD',
'Bulwark': 'PLD', 'Holy Sheltron': 'PLD', 'Intervention': 'PLD',
'Shake It Off': 'WAR', 'Holmgang': 'WAR', 'Vengeance': 'WAR',
'Damnation': 'WAR', 'Thrill of Battle': 'WAR', 'Raw Intuition': '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',
'Temperance': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH',
'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH',
@ -120,6 +169,11 @@
'Troubadour': 'BRD',
'Tactician': 'MCH',
'Shield Samba': 'DNC', 'Improvised Finish': 'DNC',
'Riddle of Earth': 'MNK',
'Shade Shift': 'NIN',
'Third Eye': 'SAM',
'Arcane Crest': 'RPR',
'Manaward': 'BLM',
'Radiant Aegis': 'SMN',
'Magick Barrier': 'RDM',
'Tempera Coat': 'PCT', 'Tempera Grassa': 'PCT',

View File

@ -127,13 +127,29 @@ function fmtNumber(n) {
function assignmentAbilityName(assignment, plan = null) {
const key = assignment?.ability ?? '';
return assignment?.abilityName ?? plan?.mitigationNames?.[key] ?? key;
const localized = localizedAbilityName(key, plan);
if (localized !== key) return localized;
return assignment?.abilityName ?? key;
}
function plannerLanguage() {
return window.App?.language || localStorage.getItem('ff14-mitigator-language') || 'en';
}
function actionLocalizedName(ability) {
const language = plannerLanguage();
const meta = actionMetaByName[ability] ?? null;
return meta?.names?.[language] || meta?.names?.en || null;
}
function localizedAbilityName(ability, plan = null) {
return plan?.mitigationNames?.[ability] ?? actionLocalizedName(ability) ?? ability;
}
function abilityIcon(ability) {
return MITIG_ICONS[ability] ?? actionMetaByName[ability]?.icon ?? '';
}
async function ensureActionMetaLoaded() {
if (actionMetaPromise) return actionMetaPromise;
actionMetaPromise = (async () => {
@ -156,6 +172,13 @@ async function ensureActionMetaLoaded() {
const meta = {
id,
name: action.Name_en ?? '',
names: {
en: action.Name_en ?? '',
de: action.Name_de ?? '',
fr: action.Name_fr ?? '',
jp: action.Name_ja ?? '',
},
icon: compact?.[id]?.icon ?? null,
cast: (parseInt(compact?.[id]?.cast ?? action.Cast100ms ?? 0, 10) || 0) / 10,
recast: (parseInt(compact?.[id]?.recast ?? action.Recast100ms ?? 0, 10) || 0) / 10,
duration: durations.find(Number.isFinite) ?? 15,
@ -164,11 +187,16 @@ async function ensureActionMetaLoaded() {
if (meta.name) byName[meta.name] = meta;
}
for (const [id, action] of Object.entries(compact ?? {})) {
const names = action.names ?? {};
byId[id] = {
...(byId[id] ?? { id }),
name: byId[id]?.name ?? names.en ?? '',
names: { ...(byId[id]?.names ?? {}), ...names },
icon: byId[id]?.icon ?? action.icon ?? '',
cast: (parseInt(action.cast ?? 0, 10) || 0) / 10,
recast: (parseInt(action.recast ?? 0, 10) || 0) / 10,
};
if (names.en && !byName[names.en]) byName[names.en] = byId[id];
}
actionMetaById = byId;
actionMetaByName = byName;
@ -434,7 +462,7 @@ function renderPlanDetail(plan) {
initTimeline(plan.id);
initMechanicClicks(plan.id);
renderInfoPanel(plan);
ensureActionMetaLoaded().then(() => refreshTimeline(plan.id));
ensureActionMetaLoaded().then(() => refreshMechanicList(plan.id));
}
function avgNonTankMaxHp(plan) {
@ -502,7 +530,7 @@ function renderMechanicListHtml(plan) {
: a.buffType === 'shield' ? 'badge-assign-shield'
: 'badge-assign-buff';
const isMissing = !!a.job && !activeJobSet.has(a.job);
const icon = MITIG_ICONS[a.ability] ?? '';
const icon = abilityIcon(a.ability);
const ability = assignmentAbilityName(a, plan);
const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability);
const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : '';
@ -641,8 +669,31 @@ const JOB_GANTT_ORDER = {
};
const TIMELINE_PERSONAL_ABILITIES = new Set([
'Rampart',
'Hallowed Ground',
'Sentinel',
'Guardian',
'Bulwark',
'Holy Sheltron',
'Intervention',
'Holmgang',
'Vengeance',
'Damnation',
'Thrill of Battle',
'Raw Intuition',
'Bloodwhetting',
'Living Dead',
'Shadow Wall',
'Shadowed Vigil',
'Dark Mind',
'The Blackest Night',
'Oblation',
'Superbolide',
'Nebula',
'Great Nebula',
'Camouflage',
'Heart of Stone',
'Heart of Corundum',
'Divine Benison',
'Intersection',
'the Spire',
@ -652,6 +703,11 @@ const TIMELINE_PERSONAL_ABILITIES = new Set([
'Seraphic Veil',
'Radiant Aegis',
'Tempera Coat',
'Riddle of Earth',
'Shade Shift',
'Third Eye',
'Arcane Crest',
'Manaward',
]);
function timelineOptions(plan) {
@ -664,7 +720,7 @@ function timelineOptions(plan) {
function timelineAbilityVisible(ability, options) {
const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(ability.name);
const isShield = ability.buffType === 'shield';
if (isPersonal && !options.includePersonal) return false;
if (isPersonal) return !!options.includePersonal;
if (isShield && !options.includeShields && ability.name !== 'Panhaima') return false;
return true;
}
@ -700,6 +756,18 @@ function jobCanUseAbility(job, ability) {
return (JOB_ABILITIES[job] ?? []).some(a => a.name === ability);
}
function abilityDefinition(ability, job = '') {
if (job) {
const own = (JOB_ABILITIES[job] ?? []).find(a => a.name === ability);
if (own) return own;
}
for (const abilities of Object.values(JOB_ABILITIES)) {
const found = abilities.find(a => a.name === ability);
if (found) return found;
}
return null;
}
function timelineRowsForAssignment(rows, assignment) {
const assignedJob = assignment.job ?? '';
return rows.filter(row => {
@ -949,8 +1017,17 @@ function assignmentOverlapsJob(plan, job, ability, timestamp, ignore = null, can
}
function actionMetaForAssignment(assignment) {
const id = String(assignment?.actionId ?? assignment?.extraAbilityGameID ?? '');
return (id && actionMetaById[id]) || actionMetaByName[assignment?.ability] || null;
const def = abilityDefinition(assignment?.ability, assignment?.job ?? '');
const id = String(assignment?.actionId ?? assignment?.extraAbilityGameID ?? def?.extraAbilityGameID ?? '');
const meta = (id && actionMetaById[id]) || actionMetaByName[assignment?.ability] || null;
if (!def) return meta;
return {
...(meta ?? {}),
id: id || meta?.id,
name: meta?.name ?? assignment?.ability,
recast: meta?.recast ?? def.recast,
duration: def.duration ?? meta?.duration,
};
}
function assignmentCooldownSeconds(assignment) {
@ -1020,7 +1097,7 @@ function renderTimelineHtml(plan) {
const cls = a.buffType === 'debuff' ? 'timeline-mitigation--debuff'
: a.buffType === 'shield' ? 'timeline-mitigation--shield'
: 'timeline-mitigation--buff';
const icon = MITIG_ICONS[a.ability] ?? '';
const icon = abilityIcon(a.ability);
const abilityLabel = assignmentAbilityName(a, plan);
blocks.push(`
<button class="timeline-mitigation ${cls}${selected}${unresolved}"
@ -1036,8 +1113,8 @@ function renderTimelineHtml(plan) {
</button>
`);
}
const icon = MITIG_ICONS[row.ability] ?? '';
const abilityDisplayName = plan.mitigationNames?.[row.ability] ?? row.ability;
const icon = abilityIcon(row.ability);
const abilityDisplayName = localizedAbilityName(row.ability, plan);
const jobStartCls = row.firstForJob ? ' timeline-player-row--job-start' : '';
return `
<div class="timeline-row timeline-player-row${jobStartCls}" data-row-idx="${row.idx}" data-job="${escHtml(row.job)}" data-ability="${escHtml(row.ability)}">
@ -1166,11 +1243,12 @@ function addTimelineAssignment(planId, mechanicId, ability, job, buffType, times
if (!plan || !jobCanUseAbility(job, ability)) return;
const mechanic = plan.mechanics.find(m => m.id === mechanicId);
if (!mechanic) return;
const def = abilityDefinition(ability, job);
mechanic.assignments = mechanic.assignments ?? [];
const assignment = {
ability,
abilityName: plan.mitigationNames?.[ability],
actionId: actionMetaByName[ability]?.id ?? null,
abilityName: localizedAbilityName(ability, plan),
actionId: def?.extraAbilityGameID ?? actionMetaByName[ability]?.id ?? null,
job,
buffType,
timestamp: Math.max(0, Math.round(timestamp)),
@ -1613,7 +1691,7 @@ function initTimeline(planId) {
.filter((row, idx, arr) => arr.findIndex(r => r.job === row.job) === idx)
.map(row => ({
label: `${row.job}${row.name ? ` · ${row.name}` : ''}`,
icon: MITIG_ICONS[block.dataset.ability] ?? '',
icon: abilityIcon(block.dataset.ability),
disabled: assignmentOverlapsJob(plan, row.job, block.dataset.ability, timestamp, {
mechanicId: block.dataset.mechanicId,
ability: block.dataset.ability,
@ -1641,7 +1719,7 @@ function initTimeline(planId) {
const ab = (JOB_ABILITIES[rowJob] ?? []).find(a => a.name === rowAbility);
const candidate = ab ? {
ability: rowAbility,
actionId: actionMetaByName[rowAbility]?.id ?? null,
actionId: ab.extraAbilityGameID ?? actionMetaByName[rowAbility]?.id ?? null,
job: rowJob,
buffType: ab.buffType,
} : null;
@ -2381,9 +2459,9 @@ function renderAbilityModalContent() {
const activeClass = isActive ? ' ability-chip--active' : '';
const otherClass = byOtherJob ? ' ability-chip--other-job' : '';
const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : '';
const icon = MITIG_ICONS[ab.name] ?? '';
const icon = abilityIcon(ab.name);
const assignedName = assigned ? assignmentAbilityName(assigned, plan) : '';
const label = assignedName || plan.mitigationNames?.[ab.name] || ab.name;
const label = assignedName || localizedAbilityName(ab.name, plan);
return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
data-ability="${escHtml(ab.name)}"
data-job="${escHtml(job)}"
@ -2431,10 +2509,11 @@ function toggleAbilityAssignment(abilityName, job, buffType) {
assignment.job = job;
}
} else {
const def = abilityDefinition(abilityName, job);
const assignment = {
ability: abilityName,
abilityName: plan.mitigationNames?.[abilityName],
actionId: actionMetaByName[abilityName]?.id ?? null,
abilityName: localizedAbilityName(abilityName, plan),
actionId: def?.extraAbilityGameID ?? actionMetaByName[abilityName]?.id ?? null,
job,
buffType,
};

View File

@ -289,6 +289,41 @@ function action_field(array $action, string $field): ?int
return null;
}
function action_text_field(array $action, string $field): ?string
{
if (array_key_exists($field, $action) && is_scalar($action[$field])) {
$value = trim((string)$action[$field]);
return $value !== '' ? $value : null;
}
$fields = $action['fields'] ?? null;
if (is_array($fields) && array_key_exists($field, $fields) && is_scalar($fields[$field])) {
$value = trim((string)$fields[$field]);
return $value !== '' ? $value : null;
}
return null;
}
function action_icon_url(array $action): ?string
{
$icon = $action['Icon'] ?? $action['fields']['Icon'] ?? null;
if (!is_array($icon)) {
return null;
}
$path = $icon['path_hr1'] ?? $icon['path'] ?? null;
if (!is_string($path) || $path === '') {
return null;
}
if (!preg_match('#ui/icon/([^/]+)/([^/]+)\.tex$#', $path, $matches)) {
return null;
}
return 'https://xivapi.com/i/' . $matches[1] . '/' . $matches[2] . '.png';
}
$plannerAbilityNames = read_planner_ability_names($plannerDataSource);
$actionIds = read_mitigation_action_ids($mitigationSource, $plannerAbilityNames);
$wanted = array_fill_keys(array_map('strval', $actionIds), true);
@ -311,6 +346,13 @@ foreach ($wanted as $id => $_) {
$filtered[$id] = [
'cast' => action_field($action, 'Cast100ms'),
'recast' => action_field($action, 'Recast100ms'),
'names' => [
'en' => action_text_field($action, 'Name_en'),
'de' => action_text_field($action, 'Name_de'),
'fr' => action_text_field($action, 'Name_fr'),
'jp' => action_text_field($action, 'Name_ja'),
],
'icon' => action_icon_url($action),
];
}