diff --git a/api/analysis.php b/api/analysis.php index 9aa9802..bed8f73 100644 --- a/api/analysis.php +++ b/api/analysis.php @@ -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], diff --git a/assets/jsons/Action.json b/assets/jsons/Action.json index dc75ce4..c60ca89 100644 --- a/assets/jsons/Action.json +++ b/assets/jsons/Action.json @@ -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" } } diff --git a/js/analysis.js b/js/analysis.js index d690e48..b1868d7 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -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 ``; @@ -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 ``; @@ -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'; diff --git a/js/ffxiv-data.js b/js/ffxiv-data.js index 4b4767b..7fbf417 100644 --- a/js/ffxiv-data.js +++ b/js/ffxiv-data.js @@ -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', diff --git a/js/planner.js b/js/planner.js index 73e3734..03338db 100644 --- a/js/planner.js +++ b/js/planner.js @@ -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(` `); } - 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 ` @@ -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 ` $_) { $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), ]; }