From e2dda9d432484fb9245c5d6d8abccdeb46ebdd19 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 2 Jun 2026 21:29:18 -0400 Subject: [PATCH 01/20] Add p1 timeline draft and initial trigger file. --- .../data/07-dt/ultimate/dancing_mad.ts | 45 +++++ .../data/07-dt/ultimate/dancing_mad.txt | 186 ++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 ui/raidboss/data/07-dt/ultimate/dancing_mad.ts create mode 100644 ui/raidboss/data/07-dt/ultimate/dancing_mad.txt diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts new file mode 100644 index 0000000000..b481d8e8ca --- /dev/null +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -0,0 +1,45 @@ +import ZoneId from '../../../../../resources/zone_id'; +import { RaidbossData } from '../../../../../types/data'; +import { TriggerSet } from '../../../../../types/trigger'; + +type Phase = 'p1' | 'p2'; +const phases: { [id: string]: Phase } = { + 'C24C': 'p2', // Ultimate Embrace, God Kefka +}; + +//const centerX = 100; +//const centerY = 100; + +export interface Data extends RaidbossData { + // General + phase: Phase | 'unknown'; +} + +const triggerSet: TriggerSet = { + id: 'DancingMadUltimate', + zoneId: ZoneId.DancingMadUltimate, + timelineFile: 'dancing_mad.txt', + initData: () => { + return { + phase: 'p1', + }; + }, + triggers: [ + { + id: 'DMU Phase Tracker', + type: 'StartsUsing', + netRegex: { id: Object.keys(phases) }, + run: (data, matches) => data.phase = phases[matches.id] ?? 'unknown', + }, + ], + timelineReplace: [ + { + 'locale': 'en', + 'replaceText': { + 'Future\'s End/Past\'s End': 'Future/Past\'s End', + }, + }, + ], +}; + +export default triggerSet; \ No newline at end of file diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt new file mode 100644 index 0000000000..f026ff8aa7 --- /dev/null +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt @@ -0,0 +1,186 @@ +### DANCING MAD (ULTIMATE) +# ZoneId: DancingMadUltimate + +# -ii C252 BA9E BAA0 BAAB BA95 BAAD BAD6 BAD8 BAD7 BAD9 +# -p C403:12.1 C24C:216.5 +# -it Kefka + +hideall "--Reset--" +hideall "--sync--" + +0.0 "--Reset--" ActorControl { command: "4000000F" } window 0,100000 jump 0 + +0.0 "--sync--" InCombat { inGameCombat: "1" } window 0,1 + +# TODO: Replace these FFLogs (IINACT uploads) with ACT Network Log timings +### Phase 1 - Kefka +# TODO: Add voiceline capture and headmarker +# https://xivapi.com/NpcYell/?pretty=true +# en (auto-translate): 'This is my first time, so please take it easy!' +#0.0 "--sync--" NpcYell { npcYellId: "" } +12.1 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } window 15,15 +15.2 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } +21.3 "Enhanced Thrill Of War II" Ability { id: "C3FD", source: "Kefka" } + +26.0 "Graven Image 1" Ability { id: "BCF2", source: "Kefka" } +31.9 "Pulse Wave" Ability { id: "BAA9", source: "Graven Image" } +34.2 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } +34.2 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } +35.1 "Flagrant Fire III" Ability { id: ["BAA2", "BAA3"], source: "Kefka" } +39.3 "Wave Cannon x4" Ability { id: "BAA8", source: "Graven Image" } +41.4 "Double-Trouble Trap" Ability { id: "BAA6", source: "Kefka" } +43.0 "Explosion x4" Ability { id: "BAAA", source: "Kefka" } +46.7 "Double-Trouble Trap x2" Ability { id: "BAA7", source: "Kefka" } +50.7 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } +50.7 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } +50.7 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } +59.7 "Light of Judgment" Ability { id: "C622", source: "Kefka" } +62.8 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } +64.8 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } +66.8 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } +72.6 "Enhanced Thrill Of War II" Ability { id: "C3FD", source: "Kefka" } + +77.0 "Graven Image 2" Ability { id: "BCF2", source: "Kefka" } +84.1 "Blizzard III Blowout" Ability { id: ["BA9B", "BA98"], source: "Kefka" } +84.2 "Gravitas x4" Ability { id: "BAAC", source: "Graven Image" } +88.2 "Vitrophyre x4" Ability { id: "BAB0", source: "Graven Image" } +94.3 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } +97.4 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } +98.0 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } +102.7 "Gravitas x4" Ability { id: "BAAC", source: "Graven Image" } +106.7 "Vitrophyre x4" Ability { id: "BAB0", source: "Graven Image" } +111.5 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } +115.1 "Double-Trouble Trap x2" Ability { id: "BAA7", source: "Kefka" } # NOTE: If it was passed after first set. +129.5 "Light of Judgment" Ability { id: "C622", source: "Kefka" } +132.6 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } +134.6 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } +136.6 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } +148.4 "Tele-Trouncing" Ability { id: "BAB9", source: "Kefka" } +156.3 "Tele-Trouncing 1" Ability { id: "BABA", source: "Kefka" } +159.3 "Tele-Trouncing 2" Ability { id: "BABA", source: "Kefka" } + +160.5 "Graven Image 3" Ability { id: "BCF2", source: "Kefka" } +165.6 "--sync--" Ability { id: "C554", source: "Kefka" } +170.4 "Indulgent Will x4" Ability { id: "BAB5", source: "Graven Image" } +170.4 "Idyllic Will x4" #Ability { id: "BAB6", source: "Graven Image" } +174.7 "--sync--" Ability { id: "C555", source: "Kefka" } +176.7 "Enhanced Thrill Of War II" Ability { id: "C3FD", source: "Kefka" } +183.1 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } +183.1 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } +183.3 "Indolent Will/Ave Maria" Ability { id: ["BAB4", "BAB3"], source: "Graven Image" } +183.9 "Flagrant Fire III" Ability { id: ["BAA2", "BAA3"], source: "Kefka" } +199.3 "Light of Judgment (enrage?)" Ability { id: "BABB", source: "Kefka" } # Kefka >15% HP + +### Phase 2 - God Kefka +# TODO: Add voiceline +# https://xivapi.com/NpcYell/?pretty=true +# en: 'Yes... I am filled with glorious purpose!' +#200.0 "--sync--" NpcYell { npcYellId: "" } window 200,5 +216.5 "Ultimate Embrace" Ability { id: "C24C", source: "Kefka" } window 220,5 +231.7 "Forsaken" Ability { id: "BABC", source: "Kefka" } +244.9 "The Path of Light 1" Ability { id: "BABE", source: "Kefka" } +245.6 "Spelldriver" #Ability { id: "BAC0", source: "Kefka" } +245.6 "Spellwave" #Ability { id: "BAC2", source: "Kefka" } +245.6 "Spellscatter" #Ability { id: "BAC1", source: "Kefka" } +254.4 "Future's End/Past's End" Ability { id: ["BAD2", "BAD3"], source: "Kefka" } + +254.8 "The Path of Light 2" Ability { id: "BABE", source: "Kefka" } +255.5 "Spellwave" #Ability { id: "BAC2", source: "Kefka" } +255.5 "Spellscatter" #Ability { id: "BAC1", source: "Kefka" } +265.6 "All Things Ending" #Ability { id: ["BACD", "BADD"], source: "Kefka" } +265.7 "The Path of Light" Ability { id: "BABE", source: "Kefka" } +266.4 "Spellwave" #Ability { id: "BAC2", source: "Kefka" } +266.4 "Spelldriver" #Ability { id: "BAC0", source: "Kefka" } +266.4 "Spellscatter" #Ability { id: "BAC1", source: "Kefka" } +275.6 "Future's End/Past's End" Ability { id: ["BAD2", "BAD3"], source: "Kefka" } + +275.6 "The Path of Light 3" Ability { id: "BABE", source: "Kefka" } +276.3 "Spellwave" #Ability { id: "BAC2", source: "Kefka" } +276.3 "Spellscatter" #Ability { id: "BAC1", source: "Kefka" } +276.7 "The River of Light" Ability { id: "BABF", source: "Kefka" } +286.1 "All Things Ending" #Ability { id: ["BACD", "BADD"], source: "Kefka" } +286.4 "The Path of Light" Ability { id: "BABE", source: "Kefka" } +287.1 "Spellwave" #Ability { id: "BAC2", source: "Kefka" } +287.5 "The River of Light" Ability { id: "BABF", source: "Kefka" } +295.5 "Future's End/Past's End" Ability { id: ["BAD2", "BAD3"], source: "Kefka" } + +296.4 "The Path of Light 4" Ability { id: "BABE", source: "Kefka" } +297.1 "Spellwave" #Ability { id: "BAC2", source: "Kefka" } +297.5 "The River of Light" Ability { id: "BABF", source: "Kefka" } +306.7 "All Things Ending" #Ability { id: ["BACD", "BADD"], source: "Kefka" } +307.2 "The Path of Light" Ability { id: "BABE", source: "Kefka" } +308.2 "The River of Light" Ability { id: "BABF", source: "Kefka" } +316.1 "Future's End/Past's End" Ability { id: ["BAD2", "BAD3"], source: "Kefka" } + +317.2 "The Path of Light 5" Ability { id: "BABE", source: "Kefka" } +318.2 "The River of Light" Ability { id: "BABF", source: "Kefka" } +327.8 "All Things Ending" #Ability { id: "BADD", source: "Kefka" } + +# TODO: 2 more sets of towers => aoe => summon trines => left/right => trines + aoes => tankbuster => ... => "enrage", or fake end or phase 3? +# Note: Enrage appears to be at > 0% if getting fake end + +# IGNORED ABILITIES +# C252 Attack: Phase 1 boss attack +# BA9E Blizzard III Blowout: Damage +# BA95 Blizzard III Blowout: VFX +# BAA0 Thrumming Thunder III: VFX +# BAAB Unmitigated Explosion: Failing to soak a tower from BAA8 Wave Cannon +# BAAD Gravitational Explosion: BAB0 Vitrophyre aoe overlaps with BAAC Gravitas puddle +# BAD6 Future's End: Damage (On 1 player) +# BAD7 Past's End: Damage (On 1 player) +# BAD8 Future's End: Damage (On 3 players) +# BAD9 Past's End: Damage (On 3 players) + +# ALL ENCOUNTER ABILITIES +# BA94 Mystery Magic +# BA95 Blizzard III Blowout: VFX (paired with BA98) +# BA98 Blizzard III Blowout: Damage for Fake? (paired with BA95) +# BA9B Blizzard III Blowout: VFX (paired with BA9E) +# BA9E Blizzard III Blowout: Damage (paired with BA9B) +# BA9F Thrumming Thunder III: Damage for Fake? (No corresponding VFX) +# BAA0 Thrumming Thunder III: VFX (paired with BAA1) +# BAA1 Thrumming Thunder III: Damage (paired with BAA0) +# BAA2 Flagrant Fire III: Spread Damage +# BAA3 Flagrant Fire III: Stack Damage +# BAA6 Double-Trouble Trap: VFX +# BAA7 Double-Trouble Trap +# BAA8 Wave Cannon +# BAA9 Pulse Wave +# BAAA Explosion: Cast by towers that are dropped from getting hit by BAA8 Wave Cannon +# BAAB Unmitigated Explosion: Failing to soak a tower from BAA8 Wave Cannon +# BAAC Gravitas +# BAAD Gravitational Explosion: BAB0 Vitrophyre aoe overlaps with BAAC Gravitas puddle +# BAB0 Vitrophyre +# BAB1 Gravitational Wave +# BAB2 Intemperate Will +# BAB3 Ave Maria +# BAB4 Indolent Will +# BAB5 Indulgent Will +# BAB6 Idyllic Will +# BAB9 Tele-Trouncing: VFX +# BABA Tele-Trouncing +# BABB Light of Judgment: P1 Enrage +# BABC Forsaken +# BABE The Path of Light +# BABF The River of Light +# BAC0 Spelldriver +# BAC1 Spellscatter +# BAC2 Spellwave +# BAD2 Future's End: VFX +# BAD3 Past's End: VFX +# BAD6 Future's End: Damage (On 1 player) +# BAD7 Past's End: Damage (On 1 player) +# BAD8 Future's End: Damage (On 3 players) +# BAD9 Past's End: Damage (On 3 players) +# BADC All Things Ending +# BADD All Things Ending +# BCF2 Graven Image +# C24B Hyperdrive +# C24C Ultimate Embrace +# C252 Attack +# C3FD Enhanced Thrill Of War II +# C403 Revolting Ruin III +# C4E1 Revolting Ruin III +# C554 --sync-- +# C555 --sync-- +# C622 Light of Judgment From 3287abc4beba03b23df99cf3c78f740f0e5521df Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 2 Jun 2026 21:35:08 -0400 Subject: [PATCH 02/20] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index b481d8e8ca..4603456ebb 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -7,8 +7,8 @@ const phases: { [id: string]: Phase } = { 'C24C': 'p2', // Ultimate Embrace, God Kefka }; -//const centerX = 100; -//const centerY = 100; +// const centerX = 100; +// const centerY = 100; export interface Data extends RaidbossData { // General @@ -33,7 +33,7 @@ const triggerSet: TriggerSet = { }, ], timelineReplace: [ - { + { 'locale': 'en', 'replaceText': { 'Future\'s End/Past\'s End': 'Future/Past\'s End', @@ -42,4 +42,4 @@ const triggerSet: TriggerSet = { ], }; -export default triggerSet; \ No newline at end of file +export default triggerSet; From 696de11d30ed7d6ff397d0abfd7698e15a76fe49 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 2 Jun 2026 23:41:10 -0400 Subject: [PATCH 03/20] adjust p1 to act network log --- .../data/07-dt/ultimate/dancing_mad.txt | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt index f026ff8aa7..ace3fc7fc5 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt @@ -1,8 +1,8 @@ ### DANCING MAD (ULTIMATE) # ZoneId: DancingMadUltimate -# -ii C252 BA9E BAA0 BAAB BA95 BAAD BAD6 BAD8 BAD7 BAD9 -# -p C403:12.1 C24C:216.5 +# -ii C252 BA9E BAA0 BAAB BA95 BAAF BAAD BAD6 BAD8 BAD7 BAD9 +# -p C403:15.6 C24C:216.5 # -it Kefka hideall "--Reset--" @@ -12,70 +12,68 @@ hideall "--sync--" 0.0 "--sync--" InCombat { inGameCombat: "1" } window 0,1 -# TODO: Replace these FFLogs (IINACT uploads) with ACT Network Log timings ### Phase 1 - Kefka -# TODO: Add voiceline capture and headmarker -# https://xivapi.com/NpcYell/?pretty=true +# TODO: Add voiceline sync? # en (auto-translate): 'This is my first time, so please take it easy!' -#0.0 "--sync--" NpcYell { npcYellId: "" } -12.1 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } window 15,15 -15.2 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } -21.3 "Enhanced Thrill Of War II" Ability { id: "C3FD", source: "Kefka" } - -26.0 "Graven Image 1" Ability { id: "BCF2", source: "Kefka" } -31.9 "Pulse Wave" Ability { id: "BAA9", source: "Graven Image" } -34.2 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } -34.2 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } -35.1 "Flagrant Fire III" Ability { id: ["BAA2", "BAA3"], source: "Kefka" } -39.3 "Wave Cannon x4" Ability { id: "BAA8", source: "Graven Image" } -41.4 "Double-Trouble Trap" Ability { id: "BAA6", source: "Kefka" } -43.0 "Explosion x4" Ability { id: "BAAA", source: "Kefka" } -46.7 "Double-Trouble Trap x2" Ability { id: "BAA7", source: "Kefka" } -50.7 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } -50.7 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } -50.7 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } -59.7 "Light of Judgment" Ability { id: "C622", source: "Kefka" } -62.8 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } -64.8 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } -66.8 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } -72.6 "Enhanced Thrill Of War II" Ability { id: "C3FD", source: "Kefka" } - -77.0 "Graven Image 2" Ability { id: "BCF2", source: "Kefka" } -84.1 "Blizzard III Blowout" Ability { id: ["BA9B", "BA98"], source: "Kefka" } -84.2 "Gravitas x4" Ability { id: "BAAC", source: "Graven Image" } -88.2 "Vitrophyre x4" Ability { id: "BAB0", source: "Graven Image" } -94.3 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } -97.4 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } -98.0 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } -102.7 "Gravitas x4" Ability { id: "BAAC", source: "Graven Image" } -106.7 "Vitrophyre x4" Ability { id: "BAB0", source: "Graven Image" } -111.5 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } -115.1 "Double-Trouble Trap x2" Ability { id: "BAA7", source: "Kefka" } # NOTE: If it was passed after first set. -129.5 "Light of Judgment" Ability { id: "C622", source: "Kefka" } -132.6 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } -134.6 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } -136.6 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } -148.4 "Tele-Trouncing" Ability { id: "BAB9", source: "Kefka" } -156.3 "Tele-Trouncing 1" Ability { id: "BABA", source: "Kefka" } -159.3 "Tele-Trouncing 2" Ability { id: "BABA", source: "Kefka" } - -160.5 "Graven Image 3" Ability { id: "BCF2", source: "Kefka" } -165.6 "--sync--" Ability { id: "C554", source: "Kefka" } -170.4 "Indulgent Will x4" Ability { id: "BAB5", source: "Graven Image" } -170.4 "Idyllic Will x4" #Ability { id: "BAB6", source: "Graven Image" } -174.7 "--sync--" Ability { id: "C555", source: "Kefka" } -176.7 "Enhanced Thrill Of War II" Ability { id: "C3FD", source: "Kefka" } -183.1 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } -183.1 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } -183.3 "Indolent Will/Ave Maria" Ability { id: ["BAB4", "BAB3"], source: "Graven Image" } -183.9 "Flagrant Fire III" Ability { id: ["BAA2", "BAA3"], source: "Kefka" } -199.3 "Light of Judgment (enrage?)" Ability { id: "BABB", source: "Kefka" } # Kefka >15% HP +10.6 "--sync--" StartsUsing { id: "C403", source: "Kefka" } window 20,10 +15.6 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } +18.7 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } +24.8 "--sync--" Ability { id: "C3FD", source: "Kefka" } + +29.2 "Graven Image 1" Ability { id: "BCF2", source: "Kefka" } +35.1 "Pulse Wave" Ability { id: "BAA9", source: "Graven Image" } +37.4 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } +37.4 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } +38.3 "Flagrant Fire III" Ability { id: ["BAA2", "BAA3"], source: "Kefka" } +42.5 "Wave Cannon x4" Ability { id: "BAA8", source: "Graven Image" } +44.6 "Double-trouble Trap" Ability { id: "BAA6", source: "Kefka" } +46.0 "Explosion x4" Ability { id: "BAAA", source: "Kefka" } +49.7 "Double-trouble Trap x2" Ability { id: "BAA7", source: "Kefka" } +53.7 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } +53.7 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } +53.7 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } +62.7 "Light of Judgment" Ability { id: "C622", source: "Kefka" } +65.8 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } +67.8 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } +69.8 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } +75.6 "--sync--" Ability { id: "C3FD", source: "Kefka" } + +80.0 "Graven Image 2" Ability { id: "BCF2", source: "Kefka" } +87.1 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } +87.2 "Gravitas x4" Ability { id: "BAAC", source: "Graven Image" } +91.2 "Vitrophyre x4" Ability { id: "BAB0", source: "Graven Image" } +97.1 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } +100.2 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } +101.1 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } +105.8 "Gravitas x4" Ability { id: "BAAC", source: "Graven Image" } +109.8 "Vitrophyre x4" Ability { id: "BAB0", source: "Graven Image" } +114.4 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } +118.9 "Double-Trouble Trap x2" Ability { id: "BAA7", source: "Kefka" } # NOTE: If it was passed after first set. +121.3 "Gravity III" #Ability { id: "BAAF", source: "Kefka" } # TODO: Adjust timing/wording to puddles safe to pop, make it a duration? +132.4 "Light of Judgment" Ability { id: "C622", source: "Kefka" } +135.6 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } +137.7 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } +139.7 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } +151.5 "Tele-trouncing" Ability { id: "BAB9", source: "Kefka" } +159.4 "Tele-trouncing 1" Ability { id: "BABA", source: "Kefka" } +162.4 "Tele-trouncing 2" Ability { id: "BABA", source: "Kefka" } + +163.6 "Graven Image 3" Ability { id: "BCF2", source: "Kefka" } +168.7 "--sync--" Ability { id: "C554", source: "Kefka" } +173.4 "Indulgent Will x4" Ability { id: "BAB5", source: "Graven Image" } +173.4 "Idyllic Will x4" #Ability { id: "BAB6", source: "Graven Image" } +177.7 "--sync--" Ability { id: "C555", source: "Kefka" } +179.7 "--sync--" Ability { id: "C3FD", source: "Kefka" } +186.3 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } +186.3 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } +186.3 "Indolent Will/Ave Maria" #Ability { id: ["BAB4", "BAB3"], source: "Graven Image" } +187.1 "Flagrant Fire III" Ability { id: ["BAA2", "BAA3"], source: "Kefka" } +202.5 "Light of Judgment (enrage?)" Ability { id: "BABB", source: "Kefka" } # Kefka >15% HP ### Phase 2 - God Kefka -# TODO: Add voiceline -# https://xivapi.com/NpcYell/?pretty=true +# TODO: Update with network log, this uses FFLOGS uploads from IINACT +# TODO: Add voiceline sync? # en: 'Yes... I am filled with glorious purpose!' -#200.0 "--sync--" NpcYell { npcYellId: "" } window 200,5 216.5 "Ultimate Embrace" Ability { id: "C24C", source: "Kefka" } window 220,5 231.7 "Forsaken" Ability { id: "BABC", source: "Kefka" } 244.9 "The Path of Light 1" Ability { id: "BABE", source: "Kefka" } @@ -126,6 +124,7 @@ hideall "--sync--" # BAA0 Thrumming Thunder III: VFX # BAAB Unmitigated Explosion: Failing to soak a tower from BAA8 Wave Cannon # BAAD Gravitational Explosion: BAB0 Vitrophyre aoe overlaps with BAAC Gravitas puddle +# BAAF Gravity III: Soaking the Gravitas puddles at the correct time # BAD6 Future's End: Damage (On 1 player) # BAD7 Past's End: Damage (On 1 player) # BAD8 Future's End: Damage (On 3 players) @@ -149,6 +148,7 @@ hideall "--sync--" # BAAA Explosion: Cast by towers that are dropped from getting hit by BAA8 Wave Cannon # BAAB Unmitigated Explosion: Failing to soak a tower from BAA8 Wave Cannon # BAAC Gravitas +# BAAF Gravity III: Soaking the Gravitas puddles at the correct time # BAAD Gravitational Explosion: BAB0 Vitrophyre aoe overlaps with BAAC Gravitas puddle # BAB0 Vitrophyre # BAB1 Gravitational Wave @@ -178,7 +178,7 @@ hideall "--sync--" # C24B Hyperdrive # C24C Ultimate Embrace # C252 Attack -# C3FD Enhanced Thrill Of War II +# C3FD --sync-- # C403 Revolting Ruin III # C4E1 Revolting Ruin III # C554 --sync-- From d7ec929f08fd15273ec29fd2a59094c909311ffa Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 01:01:39 -0400 Subject: [PATCH 04/20] change a sync to middle --- ui/raidboss/data/07-dt/ultimate/dancing_mad.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt index ace3fc7fc5..b0bdaf52e3 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt @@ -18,7 +18,7 @@ hideall "--sync--" 10.6 "--sync--" StartsUsing { id: "C403", source: "Kefka" } window 20,10 15.6 "Revolting Ruin III 1" Ability { id: "C403", source: "Kefka" } 18.7 "Revolting Ruin III 2" Ability { id: "C4E1", source: "Kefka" } -24.8 "--sync--" Ability { id: "C3FD", source: "Kefka" } +24.8 "--middle--" Ability { id: "C3FD", source: "Kefka" } 29.2 "Graven Image 1" Ability { id: "BCF2", source: "Kefka" } 35.1 "Pulse Wave" Ability { id: "BAA9", source: "Graven Image" } @@ -36,7 +36,7 @@ hideall "--sync--" 65.8 "Hyperdrive 1" #Ability { id: "C24B", source: "Kefka" } 67.8 "Hyperdrive 2" #Ability { id: "C24B", source: "Kefka" } 69.8 "Hyperdrive 3" #Ability { id: "C24B", source: "Kefka" } -75.6 "--sync--" Ability { id: "C3FD", source: "Kefka" } +75.6 "--middle--" Ability { id: "C3FD", source: "Kefka" } 80.0 "Graven Image 2" Ability { id: "BCF2", source: "Kefka" } 87.1 "Blizzard III Blowout" #Ability { id: ["BA9B", "BA98"], source: "Kefka" } @@ -63,7 +63,7 @@ hideall "--sync--" 173.4 "Indulgent Will x4" Ability { id: "BAB5", source: "Graven Image" } 173.4 "Idyllic Will x4" #Ability { id: "BAB6", source: "Graven Image" } 177.7 "--sync--" Ability { id: "C555", source: "Kefka" } -179.7 "--sync--" Ability { id: "C3FD", source: "Kefka" } +179.7 "--middle--" Ability { id: "C3FD", source: "Kefka" } 186.3 "Mystery Magic" Ability { id: "BA94", source: "Kefka" } 186.3 "Thrumming Thunder III" #Ability { id: ["BAA1", "BA9F"], source: "Kefka" } 186.3 "Indolent Will/Ave Maria" #Ability { id: ["BAB4", "BAB3"], source: "Graven Image" } @@ -178,7 +178,7 @@ hideall "--sync--" # C24B Hyperdrive # C24C Ultimate Embrace # C252 Attack -# C3FD --sync-- +# C3FD --sync--: Boss jumps to middle # C403 Revolting Ruin III # C4E1 Revolting Ruin III # C554 --sync-- From 83f0c7d998791f353051c4289730072848e350b9 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 02:04:48 -0400 Subject: [PATCH 05/20] add initial triggers --- .../data/07-dt/ultimate/dancing_mad.ts | 300 +++++++++++++++++- 1 file changed, 299 insertions(+), 1 deletion(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 4603456ebb..927e828c33 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,6 +1,8 @@ +import Outputs from '../../../../../resources/outputs'; +import { Responses } from '../../../../../resources/responses'; import ZoneId from '../../../../../resources/zone_id'; import { RaidbossData } from '../../../../../types/data'; -import { TriggerSet } from '../../../../../types/trigger'; +import { OutputStrings, TriggerSet } from '../../../../../types/trigger'; type Phase = 'p1' | 'p2'; const phases: { [id: string]: Phase } = { @@ -13,8 +15,112 @@ const phases: { [id: string]: Phase } = { export interface Data extends RaidbossData { // General phase: Phase | 'unknown'; + // Phase 1 + fireMarker?: string; + isFireTrue?: boolean; + isIceTrue?: boolean; + isThunderTrue?: boolean; } +const headMarkerData = { + // Phase 1 Boss + 'fakeFire': '02A1', + 'trueFire': '02A2', + 'fakeIce': '02A3', + 'trueIce': '02A4', + 'fakeThunder': '02A5', + 'trueThunder': '02A6', + // Phase 1 Players + 'tankbuster': '00DA', // Revolting Ruin III tankbuster + 'dorito': '007F', // spread (real) or stack (fake) + 'stack': '0080', // spread (fake) or stack (real) +} as const; + +const mysteryMagicOutputStrings: OutputStrings = { + spread: Outputs.spread, + stack: { + en: 'Stack', + de: 'Stacken', + fr: 'Packez-vous', + ja: 'スタック', + cn: '集合', + ko: '집합', + tc: '集合', + }, + trueThunder: { + en: 'True Thunder', + de: 'Wahrer Blitz', + fr: 'Vraie foudre', + ja: '真サンダガ', + cn: '真雷', + ko: '진실 선더가', + tc: '真雷', + }, + fakeThunder: { + en: 'Fake Thunder', + de: 'Falscher Blitz', + fr: 'Fausse foudre', + ja: 'にせサンダガ', + cn: '假雷', + ko: '거짓 선더가', + tc: '假雷', + }, + trueIce: { + en: 'True Ice', + de: 'Wahres Eis', + fr: 'Vraie glace', + ja: '真ブリザガ', + cn: '真冰', + ko: '진실 블리자가', + tc: '真冰', + }, + fakeIce: { + en: 'Fake Ice', + de: 'Falsches Eis', + fr: 'Fausse glace', + ja: 'にせブリザガ', + cn: '假冰', + ko: '거짓 블리자가', + tc: '假冰', + }, + stackTrueIce: { + en: '${mech} + ${ice}', + }, + stackFakeIce: { + en: '${mech} + ${ice}', + }, + spreadTrueIce: { + en: '${mech} + ${ice}', + }, + spreadFakeIce: { + en: '${mech} + ${ice}', + }, + trueIceTrueThunder: { + en: '${ice} + ${thunder}', + }, + fakeIceTrueThunder: { + en: '${ice} + ${thunder}', + }, + trueIceFakeThunder: { + en: '${ice} + ${thunder}', + }, + fakeIceFakeThunder: { + en: '${ice} + ${thunder}', + }, + stackTrueThunder: { + en: '${mech} + ${thunder}', + }, + stackFakeThunder: { + en: '${mech} + ${thunder}', + }, + spreadTrueThunder: { + en: '${mech} + ${thunder}', + }, + spreadFakeThunder: { + en: '${mech} + ${thunder}', + }, +}; + const triggerSet: TriggerSet = { id: 'DancingMadUltimate', zoneId: ZoneId.DancingMadUltimate, @@ -31,6 +137,198 @@ const triggerSet: TriggerSet = { netRegex: { id: Object.keys(phases) }, run: (data, matches) => data.phase = phases[matches.id] ?? 'unknown', }, + { + id: 'DMU P1 Revolting Ruin III', + // Tankbuster targets highest enmity then the nearest player that is not the highest enmity + // Offtank can provoke to cause the main tank to take both hits so long as main tank is closest + type: 'HeadMarker', + netRegex: { id: headMarkerData['tankbuster'], capture: true }, + response: Responses.tankBuster(), + }, + { + id: 'DMU P1 Mystery Magic Collect', + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['trueFire'], + headMarkerData['trueIce'], + headMarkerData['trueThunder'], + headMarkerData['fakeFire'], + headMarkerData['fakeIce'], + headMarkerData['fakeThunder'], + ], + capture: true, + }, + run: (data, matches) => { + switch (matches.id) { + case headMarkerData['trueFire']: + data.isFireTrue = true; + return; + case headMarkerData['fakeFire']: + data.isFireTrue = false; + return; + case headMarkerData['trueIce']: + data.isIceTrue = true; + return; + case headMarkerData['fakeIce']: + data.isIceTrue = false; + return; + case headMarkerData['trueThunder']: + data.isThunderTrue = true; + return; + case headMarkerData['fakeThunder']: + data.isThunderTrue = false; + return; + } + }, + }, + { + id: 'DMU P1 Fire Head Marker Collect', + type: 'HeadMarker', + netRegex: { id: [headMarkerData['dorito'], headMarkerData['stack']], capture: true }, + suppressSeconds: 2, + run: (data, matches) => data.fireMarker = matches.id, + }, + { + id: 'DMU P1 Mystery Magic Ice and Fire', + // Set 1: Only Ice and Fire should be set + type: 'StartsUsing', + netRegex: { id: 'BA94', source: 'Kefka', capture: false }, + condition: (data) => { + return data.isIceTrue !== undefined && data.isFireTrue !== undefined; + }, + infoText: (data, _matches, output) => { + const fireMarker = data.fireMarker; + if ( + (fireMarker === headMarkerData['dorito'] && data.isFireTrue) || + (fireMarker === headMarkerData['stack'] && !data.isFireTrue) + ) + return data.isIceTrue + ? output.spreadTrueIce!({ mech: output.spread!(), ice: output.trueIce!() }) + : output.spreadFakeIce!({ mech: output.spread!(), ice: output.fakeIce!() }); + + if ( + (fireMarker === headMarkerData['dorito'] && !data.isFireTrue) || + (fireMarker === headMarkerData['stack'] && data.isFireTrue) + ) { + return data.isIceTrue + ? output.stackTrueIce!({ mech: output.stack!(), ice: output.trueIce!() }) + : output.stackFakeIce!({ mech: output.stack!(), ice: output.fakeIce!() }); + } + }, + outputStrings: mysteryMagicOutputStrings, + }, + { + id: 'DMU P1 Mystery Magic Ice and Thunder', + // Set 2: Only Ice and Thunder should be set + type: 'StartsUsing', + netRegex: { id: 'BA94', source: 'Kefka', capture: false }, + condition: (data) => { + return data.isIceTrue !== undefined && data.isThunderTrue !== undefined; + }, + infoText: (data, _matches, output) => { + if (data.isThunderTrue) { + return data.isIceTrue + ? output.trueIceTrueThunder!({ + ice: output.trueIce!(), + thunder: output.trueThunder!(), + }) + : output.fakeIceTrueThunder!({ + ice: output.fakeIce!(), + thunder: output.trueThunder!(), + }); + } + return data.isIceTrue + ? output.trueIceTrueThunder!({ + ice: output.trueIce!(), + thunder: output.fakeThunder!(), + }) + : output.fakeIceFakeThunder!({ + ice: output.fakeIce!(), + thunder: output.fakeThunder!(), + }); + }, + outputStrings: mysteryMagicOutputStrings, + }, + { + id: 'DMU P1 Mystery Magic Fire and Thunder', + // Set 2: Only Ice and Thunder should be set + type: 'StartsUsing', + netRegex: { id: 'BA94', source: 'Kefka', capture: false }, + condition: (data) => { + return data.isFireTrue !== undefined && data.isThunderTrue !== undefined; + }, + infoText: (data, _matches, output) => { + const fireMarker = data.fireMarker; + if ( + (fireMarker === headMarkerData['dorito'] && data.isFireTrue) || + (fireMarker === headMarkerData['stack'] && !data.isFireTrue) + ) + return data.isThunderTrue + ? output.spreadTrueThunder!({ + mech: output.spread!(), + thunder: output.trueThunder!(), + }) + : output.spreadFakeThunder!({ + mech: output.spread!(), + thunder: output.fakeThunder!(), + }); + + if ( + (fireMarker === headMarkerData['dorito'] && !data.isFireTrue) || + (fireMarker === headMarkerData['stack'] && data.isFireTrue) + ) { + return data.isThunderTrue + ? output.stackTrueThunder!({ + mech: output.stack!(), + thunder: output.trueThunder!(), + }) + : output.stackFakeThunder!({ + mech: output.stack!(), + thunder: output.fakeThunder!(), + }); + } + }, + outputStrings: mysteryMagicOutputStrings, + }, + { + id: 'DMU P1 Mystery Magic Cleanup', + // C622 Light of Judgment to reset for the Graven Image 2 + type: 'StartsUsing', + netRegex: { id: ['BA94', 'C622'], source: 'Kefka', capture: false }, + run: (data) => { + delete data.isFireTrue; + delete data.isIceTrue; + delete data.isThunderTrue; + delete data.fireMarker; + }, + }, + { + id: 'DMU P1 Light of Judgment', + type: 'StartsUsing', + netRegex: { id: 'C622', source: 'Kefka', capture: false }, + response: Responses.bigAoe(), + }, + { + id: 'DMU P1 Hyperdrive', + // This hits three times + type: 'StartsUsing', + netRegex: { id: 'C24B', source: 'Kefka' }, + suppressSeconds: 5, + response: Responses.tankBuster(), + }, + { + id: 'DMU P1 Intemperate Will', + type: 'StartsUsing', + netRegex: { id: 'BAB2', source: 'Graven Image', capture: false }, + response: Responses.goWest(), + }, + { + id: 'DMU P1 Gravitational Wave', + type: 'StartsUsing', + netRegex: { id: 'BAB1', source: 'Graven Image', capture: false }, + response: Responses.goEast(), + }, ], timelineReplace: [ { From bcd18ec837bd973e5e2cc9fbfa7ba30b8dbb06f0 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 19:57:34 -0400 Subject: [PATCH 06/20] Hyperdrive tankbuster update + Remove Intemperate Will/Gravitational Wave --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 927e828c33..de738b059a 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -312,23 +312,12 @@ const triggerSet: TriggerSet = { { id: 'DMU P1 Hyperdrive', // This hits three times + // Occurs 3.1s after C622 Light of Judgment, which is a 5s cast type: 'StartsUsing', - netRegex: { id: 'C24B', source: 'Kefka' }, - suppressSeconds: 5, + netRegex: { id: 'C622', source: 'Kefka', capture: true }, + delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 3, // Gives 5.1s delay response: Responses.tankBuster(), }, - { - id: 'DMU P1 Intemperate Will', - type: 'StartsUsing', - netRegex: { id: 'BAB2', source: 'Graven Image', capture: false }, - response: Responses.goWest(), - }, - { - id: 'DMU P1 Gravitational Wave', - type: 'StartsUsing', - netRegex: { id: 'BAB1', source: 'Graven Image', capture: false }, - response: Responses.goEast(), - }, ], timelineReplace: [ { From fd08bb0a19d471cf5e9113cf82572284a2f72fb0 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 20:11:49 -0400 Subject: [PATCH 07/20] adjust delay --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index de738b059a..c629cd1700 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -315,7 +315,7 @@ const triggerSet: TriggerSet = { // Occurs 3.1s after C622 Light of Judgment, which is a 5s cast type: 'StartsUsing', netRegex: { id: 'C622', source: 'Kefka', capture: true }, - delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 3, // Gives 5.1s delay + delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 2, // Result in ~5.1s warning response: Responses.tankBuster(), }, ], From 2015b82ed24cf62c8a15c4dfd24898e403e19c2d Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 21:03:57 -0400 Subject: [PATCH 08/20] add double-trouble trap triggers --- .../data/07-dt/ultimate/dancing_mad.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index c629cd1700..b39d64dc00 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -20,6 +20,7 @@ export interface Data extends RaidbossData { isFireTrue?: boolean; isIceTrue?: boolean; isThunderTrue?: boolean; + doubleTroubleTrapTargets: string[]; } const headMarkerData = { @@ -128,6 +129,7 @@ const triggerSet: TriggerSet = { initData: () => { return { phase: 'p1', + doubleTroubleTrapTargets: [], }; }, triggers: [ @@ -303,6 +305,143 @@ const triggerSet: TriggerSet = { delete data.fireMarker; }, }, + { + id: 'DMU P1 Double-trouble Trap Collect', + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + run: (data, matches) => data.doubleTroubleTrapTargets.push(matches.target), + }, + { + id: 'DMU P1 Double-trouble Trap Early', + // Times are 5s, 68s, and 49s + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + delaySeconds: 0.1, + suppressSeconds: 1, + infoText: (data, matches, output) => { + // Ignore first set + if (parseFloat(matches.duration) < 6) + return; + const target1 = data.doubleTroubleTrapTargets[0]; + if (data.doubleTroubleTrapTargets.length === 2) { + const target2 = data.doubleTroubleTrapTargets[1]; + + if (target1 === data.me) + return output.trapOnYouPlayer!({ + player: data.party.member(target1), + }); + + if (target2 === data.me) + return output.trapOnYouPlayer!({ + player: data.party.member(target2), + }); + + return output.trapOnPlayers!({ + player1: data.party.member(target1), + player2: data.party.member(target2), + }); + } + + if (target1 === data.me) + return output.trapOnYou!(); + return output.trapOnPlayer!({ + player: data.party.member(target1), + }); + }, + outputStrings: { + trapOnYou: { + en: 'Trap on YOU (later)', + }, + trapOnYouPlayer: { + en: 'Traps on YOU, ${player} (later)', + }, + trapOnPlayer: { + en: 'Trap on ${player} (later)', + }, + trapOnPlayers: { + en: 'Traps on ${player1}, ${player2} (later)', + }, + }, + }, + { + id: 'DMU P1 Double-trouble Trap', + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + delaySeconds: (_data, matches) => { + const duration = parseFloat(matches.duration); + // Giving a 5s warning + // Second Set + if (duration > 67) + return 63; + + // Last set + if (duration > 48) + return 44; + + // First set + return 0.1; + }, + suppressSeconds: 1, + response: (data, _matches, output) => { + // cactbot-builtin-response + output.responseOutputStrings = { + trapOnYou: { + en: 'Trap on YOU', + }, + trapOnYouPlayer: { + en: 'Traps on YOU, ${player}', + }, + trapOnPlayer: { + en: 'Trap on ${player}', + }, + trapOnPlayers: { + en: 'Traps on ${player1}, ${player2}', + }, + }; + + const target1 = data.doubleTroubleTrapTargets[0]; + if (data.doubleTroubleTrapTargets.length === 2) { + const target2 = data.doubleTroubleTrapTargets[1]; + + if (target1 === data.me) + return { + alertText: output.trapOnYouPlayer!({ + player: data.party.member(target1), + }), + }; + + if (target2 === data.me) + return { + alertText: output.trapOnYouPlayer!({ + player: data.party.member(target2), + }), + }; + + return { + infoText: output.trapOnPlayers!({ + player1: data.party.member(target1), + player2: data.party.member(target2), + }), + }; + } + + if (target1 === data.me) + return { alertText: output.trapOnYou!() }; + return { + infoText: output.trapOnPlayer!({ + player: data.party.member(target1), + }), + }; + }, + }, + { + // Debuffs should expire before the new ones come out + id: 'DMU P1 Double-trouble Trap Cleanup', + type: 'LosesEffect', + netRegex: { effectId: '13D6', capture: false }, + suppressSeconds: 1, + run: (data) => data.doubleTroubleTrapTargets = [], + }, { id: 'DMU P1 Light of Judgment', type: 'StartsUsing', From 47cc7f5d01bc2ef682c3f4846baa25f1e4776e44 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 23:49:32 -0400 Subject: [PATCH 09/20] tele-portents, add trap outputs, ice only output --- .../data/07-dt/ultimate/dancing_mad.ts | 428 ++++++++++++++++-- 1 file changed, 385 insertions(+), 43 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index b39d64dc00..cdd7b5145d 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,9 +1,15 @@ +import Conditions from '../../../../../resources/conditions'; import Outputs from '../../../../../resources/outputs'; import { Responses } from '../../../../../resources/responses'; import ZoneId from '../../../../../resources/zone_id'; import { RaidbossData } from '../../../../../types/data'; import { OutputStrings, TriggerSet } from '../../../../../types/trigger'; +// TODO: P1 Tethers +// TODO: P1 Halfroom Cleaves +// TODO: P1 Replace Mystery Magic Ice Only with tether combination +// TODO: P1 Tele-Portent configuration options + type Phase = 'p1' | 'p2'; const phases: { [id: string]: Phase } = { 'C24C': 'p2', // Ultimate Embrace, God Kefka @@ -21,6 +27,8 @@ export interface Data extends RaidbossData { isIceTrue?: boolean; isThunderTrue?: boolean; doubleTroubleTrapTargets: string[]; + myTelePortent1?: 'up' | 'down' | 'right' | 'left'; + myTelePortent2?: 'up' | 'down' | 'right' | 'left'; } const headMarkerData = { @@ -122,6 +130,36 @@ const mysteryMagicOutputStrings: OutputStrings = { }, }; +const trapEarlyOutputStrings: OutputStrings = { + trapOnYou: { + en: 'Trap on YOU (later)', + }, + trapOnYouPlayer: { + en: 'Traps on YOU, ${player} (later)', + }, + trapOnPlayer: { + en: 'Trap on ${player} (later)', + }, + trapOnPlayers: { + en: 'Traps on ${player1}, ${player2} (later)', + }, +}; + +const trapOutputStrings: OutputStrings = { + trapOnYou: { + en: 'Trap on YOU ', + }, + trapOnYouPlayer: { + en: 'Traps on YOU, ${player}', + }, + trapOnPlayer: { + en: 'Trap on ${player}', + }, + trapOnPlayers: { + en: 'Traps on ${player1}, ${player2}', + }, +}; + const triggerSet: TriggerSet = { id: 'DancingMadUltimate', zoneId: ZoneId.DancingMadUltimate, @@ -129,6 +167,7 @@ const triggerSet: TriggerSet = { initData: () => { return { phase: 'p1', + // Phase 1 doubleTroubleTrapTargets: [], }; }, @@ -252,9 +291,30 @@ const triggerSet: TriggerSet = { }, outputStrings: mysteryMagicOutputStrings, }, + { + id: 'DMU P1 Mystery Magic Ice Only', + // Occurs between Set 2 and Set 3 + // BA95 Blizzard Blowout III cast + type: 'StartsUsing', + netRegex: { id: 'BA95', source: 'Kefka', capture: false }, + condition: (data) => { + if ( + data.isIceTrue !== undefined && + data.isThunderTrue === undefined && + data.isFireTrue === undefined + ) + return true; + }, + infoText: (data, _matches, output) => { + return data.isIceTrue + ? output.trueIce!() + : output.fakeIce!(); + }, + outputStrings: mysteryMagicOutputStrings, + }, { id: 'DMU P1 Mystery Magic Fire and Thunder', - // Set 2: Only Ice and Thunder should be set + // Set 3: Only Fire and Thunder should be set type: 'StartsUsing', netRegex: { id: 'BA94', source: 'Kefka', capture: false }, condition: (data) => { @@ -307,21 +367,22 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P1 Double-trouble Trap Collect', + // Times are 5s, 68s, and 49s type: 'GainsEffect', netRegex: { effectId: '13D6', capture: true }, run: (data, matches) => data.doubleTroubleTrapTargets.push(matches.target), }, { - id: 'DMU P1 Double-trouble Trap Early', - // Times are 5s, 68s, and 49s + id: 'DMU P1 Double-trouble Trap 2 Early', type: 'GainsEffect', netRegex: { effectId: '13D6', capture: true }, delaySeconds: 0.1, suppressSeconds: 1, infoText: (data, matches, output) => { - // Ignore first set - if (parseFloat(matches.duration) < 6) + // Ignore first set and third set + if (parseFloat(matches.duration) < 67) return; + const target1 = data.doubleTroubleTrapTargets[0]; if (data.doubleTroubleTrapTargets.length === 2) { const target2 = data.doubleTroubleTrapTargets[1]; @@ -348,56 +409,153 @@ const triggerSet: TriggerSet = { player: data.party.member(target1), }); }, - outputStrings: { - trapOnYou: { - en: 'Trap on YOU (later)', - }, - trapOnYouPlayer: { - en: 'Traps on YOU, ${player} (later)', - }, - trapOnPlayer: { - en: 'Trap on ${player} (later)', - }, - trapOnPlayers: { - en: 'Traps on ${player1}, ${player2} (later)', - }, - }, + outputStrings: trapEarlyOutputStrings, }, { - id: 'DMU P1 Double-trouble Trap', + id: 'DMU P1 Double-trouble Trap 3 Early', type: 'GainsEffect', netRegex: { effectId: '13D6', capture: true }, - delaySeconds: (_data, matches) => { + delaySeconds: 0.1, + suppressSeconds: 1, + infoText: (data, matches, output) => { const duration = parseFloat(matches.duration); - // Giving a 5s warning - // Second Set - if (duration > 67) - return 63; + // Only capture 3rd set + if (duration < 48 || duration > 50) + return; + + const target1 = data.doubleTroubleTrapTargets[0]; + if (data.doubleTroubleTrapTargets.length === 2) { + const target2 = data.doubleTroubleTrapTargets[1]; + + if (target1 === data.me) + return output.trapOnYouPlayer!({ + player: data.party.member(target1), + }); + + if (target2 === data.me) + return output.trapOnYouPlayer!({ + player: data.party.member(target2), + }); - // Last set - if (duration > 48) - return 44; + return output.trapOnPlayers!({ + player1: data.party.member(target1), + player2: data.party.member(target2), + }); + } - // First set - return 0.1; + if (target1 === data.me) + return output.trapOnYou!(); + return output.trapOnPlayer!({ + player: data.party.member(target1), + }); }, + outputStrings: trapEarlyOutputStrings, + }, + { + id: 'DMU P1 Double-trouble Trap 1', + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) < 6, + delaySeconds: 0.1, suppressSeconds: 1, response: (data, _matches, output) => { // cactbot-builtin-response - output.responseOutputStrings = { - trapOnYou: { - en: 'Trap on YOU', - }, - trapOnYouPlayer: { - en: 'Traps on YOU, ${player}', - }, - trapOnPlayer: { - en: 'Trap on ${player}', - }, - trapOnPlayers: { - en: 'Traps on ${player1}, ${player2}', - }, + output.responseOutputStrings = trapOutputStrings; + + const target1 = data.doubleTroubleTrapTargets[0]; + if (data.doubleTroubleTrapTargets.length === 2) { + const target2 = data.doubleTroubleTrapTargets[1]; + + if (target1 === data.me) + return { + alertText: output.trapOnYouPlayer!({ + player: data.party.member(target1), + }), + }; + + if (target2 === data.me) + return { + alertText: output.trapOnYouPlayer!({ + player: data.party.member(target2), + }), + }; + + return { + infoText: output.trapOnPlayers!({ + player1: data.party.member(target1), + player2: data.party.member(target2), + }), + }; + } + + if (target1 === data.me) + return { alertText: output.trapOnYou!() }; + return { + infoText: output.trapOnPlayer!({ + player: data.party.member(target1), + }), }; + }, + }, + { + id: 'DMU P1 Double-trouble Trap 2', + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) > 67, + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 5, + suppressSeconds: 1, + response: (data, _matches, output) => { + // cactbot-builtin-response + output.responseOutputStrings = trapOutputStrings; + + const target1 = data.doubleTroubleTrapTargets[0]; + if (data.doubleTroubleTrapTargets.length === 2) { + const target2 = data.doubleTroubleTrapTargets[1]; + + if (target1 === data.me) + return { + alertText: output.trapOnYouPlayer!({ + player: data.party.member(target1), + }), + }; + + if (target2 === data.me) + return { + alertText: output.trapOnYouPlayer!({ + player: data.party.member(target2), + }), + }; + + return { + infoText: output.trapOnPlayers!({ + player1: data.party.member(target1), + player2: data.party.member(target2), + }), + }; + } + + if (target1 === data.me) + return { alertText: output.trapOnYou!() }; + return { + infoText: output.trapOnPlayer!({ + player: data.party.member(target1), + }), + }; + }, + }, + { + id: 'DMU P1 Double-trouble Trap 3', + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + condition: (_data, matches) => { + const duration = parseFloat(matches.duration); + return duration > 48 && duration < 50; + }, + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 5, + suppressSeconds: 1, + response: (data, _matches, output) => { + // cactbot-builtin-response + output.responseOutputStrings = trapOutputStrings; const target1 = data.doubleTroubleTrapTargets[0]; if (data.doubleTroubleTrapTargets.length === 2) { @@ -457,6 +615,190 @@ const triggerSet: TriggerSet = { delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 2, // Result in ~5.1s warning response: Responses.tankBuster(), }, + { + id: 'DMU P1 Tele-Portent Collect', + // Debuffs distributed to 8 players: + // Players with 2 of the same are always: + // 130F Left (7s) + 130F Left (10s) + // 130E Right (7s) + 130E Right (10s) + // 130D Down (7s) + 130D Down (10s) + // 130C Up (7s) + 130C Up (10s) + // + // The remaining players may have differing patterns: + // Pattern 1: + // 130D Down (7s) + 13DA Left (10s) + // 13D9 Right (7s) + 130C Up (10s) + // 13D8 Down (7s) + 130E Right (10s) + // 130F Left (7s) + 13D7 Up (10s) + // + // Pattern 2: + // 130D Down (7s) + 13DA Left (10s) + // 13D9 Right (7s) + 130C Up (10s) + // 130E Right (7s) + 13D8 Down (10s) + // 13D7 Up (7s) + 130F Left (10s) + // + // Pattern 3: + // 130D Down (7s) + 13DA Left (10s) + // 13D9 Right (7s) + 130C Up (10s) + // 130E Right (7s) + 13D8 Down (10s) + // 130F Left (7s) + 13D7 Up (10s) + // + // Pattern 4: + // 13DA Left (7s) + 130D Down (10s) + // 130C Up (7s) + 13D9 Right (10s) + // 130E Right (7s) + 13D8 Down (10s) + // 130F Left (7s) + 13D7 Up (10s) + // + // Possibly More? + // Varying strategies to resolve + // Players with the same arrows will get a 6s 503 Confused which causes them to target nearest players + // Players with different arrows will cause a 6s 131E Sleep aoe + type: 'GainsEffect', + netRegex: { + effectId: [ + '130C', // Up + '130D', // Down + '130E', // Right + '130F', // Left + '13D7', // Up + '13D8', // Down + '13D9', // Right + '13DA', // Left + ], + capture: true, + }, + condition: Conditions.targetIsYou(), + run: (data, matches) => { + const effectMap: { [effectId: string]: typeof data.myTelePortent1 } = { + '130C': 'up', + '130D': 'down', + '130E': 'right', + '130F': 'left', + '13D7': 'up', + '13D8': 'down', + '13D9': 'right', + '13DA': 'left', + }; + const duration = parseFloat(matches.duration); + if (duration < 8) { + data.myTelePortent1 = effectMap[matches.effectId]; + return; + } + data.myTelePortent2 = effectMap[matches.effectId]; + }, + }, + { + id: 'DMU P1 Tele-Portents', + type: 'GainsEffect', + netRegex: { + effectId: [ + '130C', // Up + '130D', // Down + '130E', // Right + '130F', // Left + '13D7', // Up + '13D8', // Down + '13D9', // Right + '13DA', // Left + ], + capture: true, + }, + condition: Conditions.targetIsYou(), + durationSeconds: 7, + infoText: (data, _matches, output) => { + if (data.myTelePortent1 === undefined || data.myTelePortent2 === undefined) + return; + const portents = data.myTelePortent1 + data.myTelePortent2; + return output[portents]!(); + }, + outputStrings: { + upup: { + en: 'Up Portents', + }, + downdown: { + en: 'Down Portents', + }, + rightright: { + en: 'Right Portents', + }, + leftleft: { + en: 'Left Portents', + }, + downleft: { + en: 'Down => Left Portent', + }, + downright: { + en: 'Down => Right Portent', + }, + rightup: { + en: 'Right => Up Portent', + }, + rightdown: { + en: 'Right => Down Portent', + }, + leftup: { + en: 'Left => Up Portent', + }, + leftdown: { + en: 'Left => Down Portent', + }, + upright: { + en: 'Up => Right Portent', + }, + upleft: { + en: 'Up => Left Portent', + }, + }, + }, + { + id: 'DMU P1 Tele-Portent 2', + // Not enough time to have lengthy TTS, but could configure this to give direction instead of move + type: 'LosesEffect', + netRegex: { + effectId: [ + '130C', // Up + '130D', // Down + '130E', // Right + '130F', // Left + '13D7', // Up + '13D8', // Down + '13D9', // Right + '13DA', // Left + ], + capture: true, + }, + condition: (data, matches) => { + if (data.me === matches.target) + if (data.myTelePortent1 !== undefined) + return true; + return false; + }, + durationSeconds: 3, + response: Responses.moveAway('alert'), + }, + { + id: 'DMU P1 Tele-Portent Cleanup', + type: 'LosesEffect', + netRegex: { + effectId: [ + '130C', // Up + '130D', // Down + '130E', // Right + '130F', // Left + '13D7', // Up + '13D8', // Down + '13D9', // Right + '13DA', // Left + ], + capture: true, + }, + condition: Conditions.targetIsYou(), + suppressSeconds: 1, + run: (data) => { + delete data.myTelePortent1; + delete data.myTelePortent2; + }, + }, ], timelineReplace: [ { From e5bc5f9ffb68796e54f319889c587e312b3fb074 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 3 Jun 2026 23:53:46 -0400 Subject: [PATCH 10/20] lint + missing false condition --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index cdd7b5145d..ff30d0d188 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -303,7 +303,8 @@ const triggerSet: TriggerSet = { data.isThunderTrue === undefined && data.isFireTrue === undefined ) - return true; + return true; + return false; }, infoText: (data, _matches, output) => { return data.isIceTrue From 5cbfd84943c88210116f86b4cc0f4a30eee9cab5 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 03:48:13 -0400 Subject: [PATCH 11/20] Add halfroom triggers, fixup trap tracking --- .../data/07-dt/ultimate/dancing_mad.ts | 109 +++++++++++++++++- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 284df359dd..262c8e8b01 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -23,6 +23,10 @@ export interface Data extends RaidbossData { // General phase: Phase | 'unknown'; // Phase 1 + blueTowerIds: string[]; + yellowTowerIds: string[]; + purpleTowerIds: string[]; + tower?: 'blue' | 'yellow' | 'purple'; fireMarker?: string; isFireTrue?: boolean; isIceTrue?: boolean; @@ -169,6 +173,9 @@ const triggerSet: TriggerSet = { return { phase: 'p1', // Phase 1 + blueTowerIds: [], + yellowTowerIds: [], + purpleTowerIds: [], doubleTroubleTrapTargets: [], }; }, @@ -179,6 +186,63 @@ const triggerSet: TriggerSet = { netRegex: { id: Object.keys(phases) }, run: (data, matches) => data.phase = phases[matches.id] ?? 'unknown', }, + { + id: 'DMU P1 CombatantMemory Tower Tracker', + // 1EBFBB => Wave Cannon entity (blue) + // 1EBFBC => Gravitational Wave entity (purple) + // 1EBFBD => Intemperate Will entity (yellow) + // There are two of each, they are added at start of fight + type: 'CombatantMemory', + netRegex: { + change: 'Add', + pair: [{ key: 'BNpcID', value: ['1EBFBB', '1EBFBC', '1EBFBD'] }], + capture: true, + }, + run: (data, matches) => { + const towerMap = { + '1EBFBB': 'blue', + '1EBFBC': 'purple', + '1EBFBD': 'yellow', + 'unknown': 'unknown', + }; + const bnpcid = matches.pairBNpcID ?? 'unknown'; + const kind = towerMap[bnpcid as keyof typeof towerMap]; + if (kind === 'blue') { + data.blueTowerIds.push(matches.id); + return; + } + if (kind === 'yellow') { + data.yellowTowerIds.push(matches.id); + return; + } + if (kind === 'purple') { + data.purpleTowerIds.push(matches.id); + return; + } + }, + }, + { + id: 'DMU P1 Graven Image Collect', + // Tower entity actions + type: 'ActorControlExtra', + netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, + run: (data, matches) => { + const id = matches.id; + + if (data.yellowTowerIds.indexOf(id) !== -1) { + data.tower = 'yellow'; + return; + } + if (data.purpleTowerIds.indexOf(id) !== -1) { + data.tower = 'purple'; + return; + } + if (data.blueTowerIds.indexOf(id) !== -1) { + data.tower = 'blue'; + return; + } + }, + }, { id: 'DMU P1 Revolting Ruin III', // Tankbuster targets highest enmity then the nearest player that is not the highest enmity @@ -386,6 +450,10 @@ const triggerSet: TriggerSet = { return; const target1 = data.doubleTroubleTrapTargets[0]; + // Check if players died from a knockback + if (target1 === undefined) + return; + if (data.doubleTroubleTrapTargets.length === 2) { const target2 = data.doubleTroubleTrapTargets[1]; @@ -426,6 +494,10 @@ const triggerSet: TriggerSet = { return; const target1 = data.doubleTroubleTrapTargets[0]; + // Check if players died from a knockback + if (target1 === undefined) + return; + if (data.doubleTroubleTrapTargets.length === 2) { const target2 = data.doubleTroubleTrapTargets[1]; @@ -511,6 +583,10 @@ const triggerSet: TriggerSet = { output.responseOutputStrings = trapOutputStrings; const target1 = data.doubleTroubleTrapTargets[0]; + // Check if players died + if (target1 === undefined) + return; + if (data.doubleTroubleTrapTargets.length === 2) { const target2 = data.doubleTroubleTrapTargets[1]; @@ -560,6 +636,10 @@ const triggerSet: TriggerSet = { output.responseOutputStrings = trapOutputStrings; const target1 = data.doubleTroubleTrapTargets[0]; + // Check if players died + if (target1 === undefined) + return; + if (data.doubleTroubleTrapTargets.length === 2) { const target2 = data.doubleTroubleTrapTargets[1]; @@ -595,12 +675,15 @@ const triggerSet: TriggerSet = { }, }, { - // Debuffs should expire before the new ones come out id: 'DMU P1 Double-trouble Trap Cleanup', + // Players dying will also trigger this type: 'LosesEffect', - netRegex: { effectId: '13D6', capture: false }, - suppressSeconds: 1, - run: (data) => data.doubleTroubleTrapTargets = [], + netRegex: { effectId: '13D6', capture: true }, + run: (data, matches) => { + data.doubleTroubleTrapTargets = data.doubleTroubleTrapTargets.filter( + (target) => target !== matches.target + ); + }, }, { id: 'DMU P1 Light of Judgment', @@ -617,6 +700,24 @@ const triggerSet: TriggerSet = { delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 2, // Result in ~5.1s warning response: Responses.tankBuster(), }, + { + id: 'DMU P1 Impertinent Will/Gravitational Wave', + type: 'ActorControlExtra', + netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, + alertText: (data, matches, output) => { + const id = matches.id; + if (data.yellowTowerIds.indexOf(id) !== -1) { + return output.goWest!(); + } + if (data.purpleTowerIds.indexOf(id) !== -1) { + return output.goEast!(); + } + }, + outputStrings: { + goWest: Outputs.getLeftAndWest, + goEast: Outputs.getRightAndEast, + }, + }, { id: 'DMU P1 Tele-Portent Collect', // Debuffs distributed to 8 players: From 30d4d2ee3f511cf207866f8bb46226fc0ebfb8c9 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 03:52:00 -0400 Subject: [PATCH 12/20] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 262c8e8b01..79ff8896bb 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -681,7 +681,7 @@ const triggerSet: TriggerSet = { netRegex: { effectId: '13D6', capture: true }, run: (data, matches) => { data.doubleTroubleTrapTargets = data.doubleTroubleTrapTargets.filter( - (target) => target !== matches.target + (target) => target !== matches.target, ); }, }, From 752d62e829488bc5a6f50895cc70668c76aabaa7 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 18:44:28 -0400 Subject: [PATCH 13/20] revolting ruin tankbuster => tank cleave --- .../data/07-dt/ultimate/dancing_mad.ts | 132 +++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 79ff8896bb..bfabd6a4c9 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -31,6 +31,7 @@ export interface Data extends RaidbossData { isFireTrue?: boolean; isIceTrue?: boolean; isThunderTrue?: boolean; + waveCannonTargets: string[]; doubleTroubleTrapTargets: string[]; myTelePortent1?: 'up' | 'down' | 'right' | 'left'; myTelePortent2?: 'up' | 'down' | 'right' | 'left'; @@ -176,6 +177,7 @@ const triggerSet: TriggerSet = { blueTowerIds: [], yellowTowerIds: [], purpleTowerIds: [], + waveCannonTargets: [], doubleTroubleTrapTargets: [], }; }, @@ -249,7 +251,62 @@ const triggerSet: TriggerSet = { // Offtank can provoke to cause the main tank to take both hits so long as main tank is closest type: 'HeadMarker', netRegex: { id: headMarkerData['tankbuster'], capture: true }, - response: Responses.tankBuster(), + alertText: (data, matches, output) => { + const target = matches.target; + + // Highest entity player can stand wherever, but if they swap threat + // they should be in to either take the hit or prevent party hit + if (target === data.me) + return output.cleaveOnYouDir!({ + cleave: output.cleaveOnYou!(), + dir: output.in!(), + }); + + // Off tank (second highest enmity player) needs to be in for followup + // Or swap with main tank being in + // If both tanks are in, then it's safe for party in either case + if (data.role === 'tank') + return output.cleaveOnPlayerSwapDir!({ + cleave: output.cleaveOnPlayer!({ + player: data.party.member(target), + }), + dir: output.in!(), + }); + + if (data.role === 'healer') + return output.cleaveOnPlayerDir!({ + cleave: output.cleaveOnPlayer!({ + player: data.party.member(target), + }), + dir: output.out!(), + }); + + return output.avoidCleavesDir!({ + cleave: output.avoidCleaves!(), + dir: output.out!(), + }); + }, + outputStrings: { + in: Outputs.in, + out: Outputs.out, + cleaveOnYou: Outputs.tankCleaveOnYou, + avoidCleaves: Outputs.avoidTankCleaves, + cleaveOnPlayer: { + en: 'Tank Cleave on ${player}', + }, + cleaveOnYouDir: { + en: '${cleave} => ${dir}', + }, + cleaveOnPlayerDir: { + en: '${cleave} + ${dir}', + }, + cleaveOnPlayerSwapDir: { + en: '${cleave} => ${dir}', + }, + avoidCleavesDir: { + en: '${cleave} + ${dir}', + }, + }, }, { id: 'DMU P1 Mystery Magic Collect', @@ -431,6 +488,79 @@ const triggerSet: TriggerSet = { delete data.fireMarker; }, }, + { + id: 'DMU P1 Wave Cannon', + // BAA8 Wave Cannon is an instant cast from Graven Image + // This gives a ~5 second warning to spread + type: 'ActorControlExtra', + netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, + alertText: (data, matches, output) => { + if (data.blueTowerIds.indexOf(matches.id) !== -1) + return output.waveCannonLine!(); + }, + outputStrings: { + waveCannonLine: { + en: 'E/W Spread', + }, + }, + }, + { + id: 'DMU P1 Wave Cannon Collect', + // Collect players hit by Wave Cannon to tell who soaks tower followup and who avoids tower + type: 'Ability', + netRegex: { id: 'BAA8', source: 'Graven Image', capture: true }, + run: (data, matches) => data.waveCannonTargets.push(matches.target), + }, + { + id: 'DMU P1 Wave Cannon Explosion Towers', + // Wave Cannon gives a vulnerability which causes death to BAAA Explosion soaks + // Sacraficing a player who clipped to prevent party 90% damage down from + // BAAB Unmitigated Explosion seems ideal, although different clients may + // get different order + // Suprisingly the Unmitigated Explosion doesn't deal damage + type: 'Ability', + netRegex: { id: 'BAA8', source: 'Graven Image', capture: true }, + delaySeconds: 0.1, + suppressSeconds: 1, + response: (data, _matches, output) => { + // cactbot-builtin-response + output.responseOutputStrings = { + soak: { + en: 'Soak tower', + de: 'Türme nehmen', + fr: 'Prenez une tour', + ja: '塔踏み', + cn: '踩塔击飞', + ko: '기둥 들어가기', + tc: '踩塔擊飛', + }, + avoid: { + en: 'Avoid towers', + de: 'Türme vermeiden', + fr: 'Évitez les tours', + ja: '塔回避', + cn: '远离塔', + ko: '기둥 피하기', + tc: '遠離塔', + }, + extra: { + en: 'Extra Tower', + }, + }; + const avoidedCannon = data.waveCannonTargets.indexOf(data.me) !== -1; + + // Option for player to soak the tower for p1 prog? + if (avoidedCannon && data.waveCannonTargets.length > 4) + return { infoText: output.extra!() }; + + // Avoid the tower + if (avoidedCannon) + return { alertText: output.avoid!() }; + + // Player didn't get hit, they will need to soak a tower + return { alertTest: output.soak!() }; + }, + }, { id: 'DMU P1 Double-trouble Trap Collect', // Times are 5s, 68s, and 49s From b8c884589e348fdc2def8c1736c9896a31f26b7b Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 18:48:14 -0400 Subject: [PATCH 14/20] remove unnecessary capture --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index bfabd6a4c9..5783c9ff0b 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -519,7 +519,7 @@ const triggerSet: TriggerSet = { // get different order // Suprisingly the Unmitigated Explosion doesn't deal damage type: 'Ability', - netRegex: { id: 'BAA8', source: 'Graven Image', capture: true }, + netRegex: { id: 'BAA8', source: 'Graven Image', capture: false }, delaySeconds: 0.1, suppressSeconds: 1, response: (data, _matches, output) => { From f6b53c06f62cc0593d6a7a64aa4ba8bd067268b3 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 19:02:17 -0400 Subject: [PATCH 15/20] explicit mystery magic output --- .../data/07-dt/ultimate/dancing_mad.ts | 60 ++++--------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 5783c9ff0b..e0fcd77388 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -63,40 +63,16 @@ const mysteryMagicOutputStrings: OutputStrings = { tc: '集合', }, trueThunder: { - en: 'True Thunder', - de: 'Wahrer Blitz', - fr: 'Vraie foudre', - ja: '真サンダガ', - cn: '真雷', - ko: '진실 선더가', - tc: '真雷', + en: 'In Line', }, fakeThunder: { - en: 'Fake Thunder', - de: 'Falscher Blitz', - fr: 'Fausse foudre', - ja: 'にせサンダガ', - cn: '假雷', - ko: '거짓 선더가', - tc: '假雷', + en: 'Avoid Tell', }, trueIce: { - en: 'True Ice', - de: 'Wahres Eis', - fr: 'Vraie glace', - ja: '真ブリザガ', - cn: '真冰', - ko: '진실 블리자가', - tc: '真冰', + en: 'In Cone', }, fakeIce: { - en: 'Fake Ice', - de: 'Falsches Eis', - fr: 'Fausse glace', - ja: 'にせブリザガ', - cn: '假冰', - ko: '거짓 블리자가', - tc: '假冰', + en: 'Avoid Tell', }, stackTrueIce: { en: '${mech} + ${ice}', @@ -111,16 +87,16 @@ const mysteryMagicOutputStrings: OutputStrings = { en: '${mech} + ${ice}', }, trueIceTrueThunder: { - en: '${ice} + ${thunder}', + en: 'Avoid Tells', }, fakeIceTrueThunder: { - en: '${ice} + ${thunder}', + en: 'Cone (only)', }, trueIceFakeThunder: { - en: '${ice} + ${thunder}', + en: 'Line (only)', }, fakeIceFakeThunder: { - en: '${ice} + ${thunder}', + en: 'Cone + Line', }, stackTrueThunder: { en: '${mech} + ${thunder}', @@ -392,24 +368,12 @@ const triggerSet: TriggerSet = { infoText: (data, _matches, output) => { if (data.isThunderTrue) { return data.isIceTrue - ? output.trueIceTrueThunder!({ - ice: output.trueIce!(), - thunder: output.trueThunder!(), - }) - : output.fakeIceTrueThunder!({ - ice: output.fakeIce!(), - thunder: output.trueThunder!(), - }); + ? output.trueIceTrueThunder!() + : output.fakeIceTrueThunder!(); } return data.isIceTrue - ? output.trueIceTrueThunder!({ - ice: output.trueIce!(), - thunder: output.fakeThunder!(), - }) - : output.fakeIceFakeThunder!({ - ice: output.fakeIce!(), - thunder: output.fakeThunder!(), - }); + ? output.trueIceTrueThunder!() + : output.fakeIceFakeThunder!(); }, outputStrings: mysteryMagicOutputStrings, }, From 2103d4184f85a76d21721ef725a17bfcad2d0798 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 20:56:02 -0400 Subject: [PATCH 16/20] flip fake/true thunder ice --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index e0fcd77388..b2c66e0228 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -63,16 +63,16 @@ const mysteryMagicOutputStrings: OutputStrings = { tc: '集合', }, trueThunder: { - en: 'In Line', + en: 'Avoid Tell', }, fakeThunder: { - en: 'Avoid Tell', + en: 'In Line', }, trueIce: { - en: 'In Cone', + en: 'Avoid Tell', }, fakeIce: { - en: 'Avoid Tell', + en: 'In Cone', }, stackTrueIce: { en: '${mech} + ${ice}', From 63d60cea61bdd03011c9a53f8243b85eacdc1986 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 21:08:40 -0400 Subject: [PATCH 17/20] remove dir outputs from revolting ruin --- .../data/07-dt/ultimate/dancing_mad.ts | 47 ++++--------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index b2c66e0228..3e304c758a 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -223,44 +223,26 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P1 Revolting Ruin III', - // Tankbuster targets highest enmity then the nearest player that is not the highest enmity - // Offtank can provoke to cause the main tank to take both hits so long as main tank is closest + // Tankbuster targets highest enmity then second highest enmity + // A tank swap can happen to have MT take both hits type: 'HeadMarker', netRegex: { id: headMarkerData['tankbuster'], capture: true }, alertText: (data, matches, output) => { const target = matches.target; - - // Highest entity player can stand wherever, but if they swap threat - // they should be in to either take the hit or prevent party hit if (target === data.me) - return output.cleaveOnYouDir!({ - cleave: output.cleaveOnYou!(), - dir: output.in!(), - }); + return output.cleaveOnYou!(); - // Off tank (second highest enmity player) needs to be in for followup - // Or swap with main tank being in - // If both tanks are in, then it's safe for party in either case if (data.role === 'tank') - return output.cleaveOnPlayerSwapDir!({ - cleave: output.cleaveOnPlayer!({ - player: data.party.member(target), - }), - dir: output.in!(), + return output.cleaveSwap!({ + player: data.party.member(target), }); if (data.role === 'healer') - return output.cleaveOnPlayerDir!({ - cleave: output.cleaveOnPlayer!({ - player: data.party.member(target), - }), - dir: output.out!(), + return output.cleaveOnPlayer!({ + player: data.party.member(target), }); - return output.avoidCleavesDir!({ - cleave: output.avoidCleaves!(), - dir: output.out!(), - }); + return output.avoidCleaves!(); }, outputStrings: { in: Outputs.in, @@ -270,17 +252,8 @@ const triggerSet: TriggerSet = { cleaveOnPlayer: { en: 'Tank Cleave on ${player}', }, - cleaveOnYouDir: { - en: '${cleave} => ${dir}', - }, - cleaveOnPlayerDir: { - en: '${cleave} + ${dir}', - }, - cleaveOnPlayerSwapDir: { - en: '${cleave} => ${dir}', - }, - avoidCleavesDir: { - en: '${cleave} + ${dir}', + cleaveSwap: { // Defaulting to same output as cleaveOnPlayer + en: 'Tank Cleave on ${player}', }, }, }, From cad38384578c527da4a0464da488a16f34d940ee Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 22:08:43 -0400 Subject: [PATCH 18/20] double-trouble trap output refactor --- .../data/07-dt/ultimate/dancing_mad.ts | 242 +++++------------- 1 file changed, 60 insertions(+), 182 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 3e304c758a..26eaedb845 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -112,33 +112,12 @@ const mysteryMagicOutputStrings: OutputStrings = { }, }; -const trapEarlyOutputStrings: OutputStrings = { - trapOnYou: { - en: 'Trap on YOU (later)', - }, - trapOnYouPlayer: { - en: 'Traps on YOU, ${player} (later)', - }, - trapOnPlayer: { - en: 'Trap on ${player} (later)', - }, - trapOnPlayers: { - en: 'Traps on ${player1}, ${player2} (later)', - }, -}; - const trapOutputStrings: OutputStrings = { - trapOnYou: { - en: 'Trap on YOU ', - }, - trapOnYouPlayer: { - en: 'Traps on YOU, ${player}', + knockbackFrom: { + en: 'Knockback from ${players}', }, - trapOnPlayer: { - en: 'Trap on ${player}', - }, - trapOnPlayers: { - en: 'Traps on ${player1}, ${player2}', + knockbackFromLater: { + en: 'Knockback from ${players} (later)', }, }; @@ -516,37 +495,21 @@ const triggerSet: TriggerSet = { if (parseFloat(matches.duration) < 67) return; - const target1 = data.doubleTroubleTrapTargets[0]; - // Check if players died from a knockback - if (target1 === undefined) + // Check if players died + if (data.doubleTroubleTrapTargets[0] === undefined) return; - if (data.doubleTroubleTrapTargets.length === 2) { - const target2 = data.doubleTroubleTrapTargets[1]; - - if (target1 === data.me) - return output.trapOnYouPlayer!({ - player: data.party.member(target1), - }); - - if (target2 === data.me) - return output.trapOnYouPlayer!({ - player: data.party.member(target2), - }); - - return output.trapOnPlayers!({ - player1: data.party.member(target1), - player2: data.party.member(target2), - }); - } - - if (target1 === data.me) - return output.trapOnYou!(); - return output.trapOnPlayer!({ - player: data.party.member(target1), - }); + const players = data.doubleTroubleTrapTargets.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + } + ); + const msg = players?.join(', '); + return output.knockbackFromLater!({ players: msg }); }, - outputStrings: trapEarlyOutputStrings, + outputStrings: trapOutputStrings, }, { id: 'DMU P1 Double-trouble Trap 3 Early', @@ -560,37 +523,21 @@ const triggerSet: TriggerSet = { if (duration < 48 || duration > 50) return; - const target1 = data.doubleTroubleTrapTargets[0]; - // Check if players died from a knockback - if (target1 === undefined) + // Check if players died + if (data.doubleTroubleTrapTargets[0] === undefined) return; - if (data.doubleTroubleTrapTargets.length === 2) { - const target2 = data.doubleTroubleTrapTargets[1]; - - if (target1 === data.me) - return output.trapOnYouPlayer!({ - player: data.party.member(target1), - }); - - if (target2 === data.me) - return output.trapOnYouPlayer!({ - player: data.party.member(target2), - }); - - return output.trapOnPlayers!({ - player1: data.party.member(target1), - player2: data.party.member(target2), - }); - } - - if (target1 === data.me) - return output.trapOnYou!(); - return output.trapOnPlayer!({ - player: data.party.member(target1), - }); + const players = data.doubleTroubleTrapTargets.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + } + ); + const msg = players?.join(', '); + return output.knockbackFromLater!({ players: msg }); }, - outputStrings: trapEarlyOutputStrings, + outputStrings: trapOutputStrings, }, { id: 'DMU P1 Double-trouble Trap 1', @@ -603,39 +550,16 @@ const triggerSet: TriggerSet = { // cactbot-builtin-response output.responseOutputStrings = trapOutputStrings; - const target1 = data.doubleTroubleTrapTargets[0]; - if (data.doubleTroubleTrapTargets.length === 2) { - const target2 = data.doubleTroubleTrapTargets[1]; - - if (target1 === data.me) - return { - alertText: output.trapOnYouPlayer!({ - player: data.party.member(target1), - }), - }; - - if (target2 === data.me) - return { - alertText: output.trapOnYouPlayer!({ - player: data.party.member(target2), - }), - }; - - return { - infoText: output.trapOnPlayers!({ - player1: data.party.member(target1), - player2: data.party.member(target2), - }), - }; - } - - if (target1 === data.me) - return { alertText: output.trapOnYou!() }; - return { - infoText: output.trapOnPlayer!({ - player: data.party.member(target1), - }), - }; + const severity = data.doubleTroubleTrapTargets.includes(data.me) ? 'alertText' : 'infoText'; + const players = data.doubleTroubleTrapTargets.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + } + ); + const msg = players?.join(', '); + return { [severity]: output.knockbackFrom!({ players: msg }) }; }, }, { @@ -649,43 +573,20 @@ const triggerSet: TriggerSet = { // cactbot-builtin-response output.responseOutputStrings = trapOutputStrings; - const target1 = data.doubleTroubleTrapTargets[0]; // Check if players died - if (target1 === undefined) + if (data.doubleTroubleTrapTargets[0] === undefined) return; - if (data.doubleTroubleTrapTargets.length === 2) { - const target2 = data.doubleTroubleTrapTargets[1]; - - if (target1 === data.me) - return { - alertText: output.trapOnYouPlayer!({ - player: data.party.member(target1), - }), - }; - - if (target2 === data.me) - return { - alertText: output.trapOnYouPlayer!({ - player: data.party.member(target2), - }), - }; - - return { - infoText: output.trapOnPlayers!({ - player1: data.party.member(target1), - player2: data.party.member(target2), - }), - }; - } - - if (target1 === data.me) - return { alertText: output.trapOnYou!() }; - return { - infoText: output.trapOnPlayer!({ - player: data.party.member(target1), - }), - }; + const severity = data.doubleTroubleTrapTargets.includes(data.me) ? 'alertText' : 'infoText'; + const players = data.doubleTroubleTrapTargets.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + } + ); + const msg = players?.join(', '); + return { [severity]: output.knockbackFrom!({ players: msg }) }; }, }, { @@ -702,43 +603,20 @@ const triggerSet: TriggerSet = { // cactbot-builtin-response output.responseOutputStrings = trapOutputStrings; - const target1 = data.doubleTroubleTrapTargets[0]; // Check if players died - if (target1 === undefined) + if (data.doubleTroubleTrapTargets[0] === undefined) return; - if (data.doubleTroubleTrapTargets.length === 2) { - const target2 = data.doubleTroubleTrapTargets[1]; - - if (target1 === data.me) - return { - alertText: output.trapOnYouPlayer!({ - player: data.party.member(target1), - }), - }; - - if (target2 === data.me) - return { - alertText: output.trapOnYouPlayer!({ - player: data.party.member(target2), - }), - }; - - return { - infoText: output.trapOnPlayers!({ - player1: data.party.member(target1), - player2: data.party.member(target2), - }), - }; - } - - if (target1 === data.me) - return { alertText: output.trapOnYou!() }; - return { - infoText: output.trapOnPlayer!({ - player: data.party.member(target1), - }), - }; + const severity = data.doubleTroubleTrapTargets.includes(data.me) ? 'alertText' : 'infoText'; + const players = data.doubleTroubleTrapTargets.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + } + ); + const msg = players?.join(', '); + return { [severity]: output.knockbackFrom!({ players: msg }) }; }, }, { From 80789139bbcd001c3efe6fdf14f45124596e7499 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 22:11:09 -0400 Subject: [PATCH 19/20] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 26eaedb845..fd56d22e6f 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -504,7 +504,7 @@ const triggerSet: TriggerSet = { if (player === data.me) return 'YOU'; return data.party.member(player); - } + }, ); const msg = players?.join(', '); return output.knockbackFromLater!({ players: msg }); @@ -532,7 +532,7 @@ const triggerSet: TriggerSet = { if (player === data.me) return 'YOU'; return data.party.member(player); - } + }, ); const msg = players?.join(', '); return output.knockbackFromLater!({ players: msg }); @@ -556,7 +556,7 @@ const triggerSet: TriggerSet = { if (player === data.me) return 'YOU'; return data.party.member(player); - } + }, ); const msg = players?.join(', '); return { [severity]: output.knockbackFrom!({ players: msg }) }; @@ -583,7 +583,7 @@ const triggerSet: TriggerSet = { if (player === data.me) return 'YOU'; return data.party.member(player); - } + }, ); const msg = players?.join(', '); return { [severity]: output.knockbackFrom!({ players: msg }) }; @@ -613,7 +613,7 @@ const triggerSet: TriggerSet = { if (player === data.me) return 'YOU'; return data.party.member(player); - } + }, ); const msg = players?.join(', '); return { [severity]: output.knockbackFrom!({ players: msg }) }; From 15a1c918b9c1b103fc2c9e3b7264ae10f74382e5 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 23:19:13 -0400 Subject: [PATCH 20/20] add tether triggers --- .../data/07-dt/ultimate/dancing_mad.ts | 286 +++++++++++++++++- 1 file changed, 283 insertions(+), 3 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index fd56d22e6f..8414453f3d 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -27,6 +27,15 @@ export interface Data extends RaidbossData { yellowTowerIds: string[]; purpleTowerIds: string[]; tower?: 'blue' | 'yellow' | 'purple'; + gravenImageCount: number; + actorPositions: { [id: string]: { x: number; y: number; heading: number } }; + gravenImageTether?: + | 'pulse' + | 'gravitas' + | 'vitrophyre' + | 'indulgent' + | 'idyllic' + | 'unknown'; fireMarker?: string; isFireTrue?: boolean; isIceTrue?: boolean; @@ -49,10 +58,22 @@ const headMarkerData = { 'tankbuster': '00DA', // Revolting Ruin III tankbuster 'dorito': '007F', // spread (real) or stack (fake) 'stack': '0080', // spread (fake) or stack (real) + // Phase 1 Tethers + 'imageTether': '002D', } as const; const mysteryMagicOutputStrings: OutputStrings = { + puddle: { + en: 'Bait Puddle', + de: 'Fläche ködern', + fr: 'Déposez', + ja: 'AOE誘導', + cn: '诱导AOE', + ko: '장판 유도', + tc: '誘導AOE', + }, spread: Outputs.spread, + middle: Outputs.goIntoMiddle, stack: { en: 'Stack', de: 'Stacken', @@ -74,6 +95,12 @@ const mysteryMagicOutputStrings: OutputStrings = { fakeIce: { en: 'In Cone', }, + trueIcePuddle: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + fakeIcePuddle: { + en: '${mech1} + ${mech2} => ${mech3}', + }, stackTrueIce: { en: '${mech} + ${ice}', }, @@ -132,6 +159,8 @@ const triggerSet: TriggerSet = { blueTowerIds: [], yellowTowerIds: [], purpleTowerIds: [], + actorPositions: {}, + gravenImageCount: 0, waveCannonTargets: [], doubleTroubleTrapTargets: [], }; @@ -143,6 +172,248 @@ const triggerSet: TriggerSet = { netRegex: { id: Object.keys(phases) }, run: (data, matches) => data.phase = phases[matches.id] ?? 'unknown', }, + { + id: 'DMU ActorSetPos Tracker', + // Only in use for P1 Graven Image tethers + type: 'ActorSetPos', + netRegex: { id: '4[0-9A-Fa-f]{7}', capture: true }, + run: (data, matches) => + data.actorPositions[matches.id] = { + x: parseFloat(matches.x), + y: parseFloat(matches.y), + heading: parseFloat(matches.heading), + }, + }, + { + id: 'DMU P1 Graven Image Counter', + // Used for timing of tether triggers + type: 'StartsUsing', + netRegex: { id: 'BCF2', source: 'Kefka', capture: false }, + run: (data) => data.gravenImageCount = data.gravenImageCount + 1, + }, + { + id: 'DMU Graven Image Tether Collect', + // 271 ActorSetPos lines indicate where the tether is coming from + // 261 CombatantMemory lines may also indicate this + // Graven Image 1: + // (100, 56, 18.5) Center Tether, Will be target of BAA9 Pulse Wave (knockback) + // Graven Image 2: + // (102.5, 27, 22.5) Center Tether, Will be target of BAAC Gravitas (puddles) + // (126, 41.5, 7) Right Tether, Will be target of BAB0 Vitrophyre (rocks) + // Graven Image 3: + // (95, 25, 27) Left Tether, Will be target of BAB5 Indulgent Will which causes 503 Confused + // (107, 43, 8.5) Right tether, Will be target of BAB6 Idyllic Will which causes 131E Sleep + type: 'Tether', + netRegex: { id: headMarkerData['imageTether'], capture: true }, + condition: Conditions.targetIsYou(), + delaySeconds: 0.1, // Actor position data can come after tether in log + run: (data, matches) => { + const actor = data.actorPositions[matches.sourceId]; + if (actor === undefined) { + data.gravenImageTether = 'unknown'; + return; + } + + const x = actor.x; + // Graven Image 1: Pulse Wave target + if (x < 101 && x > 99) + data.gravenImageTether = 'pulse'; + else if (x < 103 && x > 101) // Graven Image 2: Gravitas target + data.gravenImageTether = 'gravitas'; + else if (x > 125) // Graven Image 2: Vitrophyre target + data.gravenImageTether = 'vitrophyre'; + else if (x < 100) // Graven Image 3: Indulgent Will target + data.gravenImageTether = 'indulgent'; + else if (x < 108 && x > 106) // Graven Image 3: Idyllic Will target + data.gravenImageTether = 'idyllic'; + else + data.gravenImageTether = 'unknown'; + }, + }, + { + id: 'DMU Pulse Wave Tethers', + type: 'Tether', + netRegex: { id: headMarkerData['imageTether'], capture: true }, + condition: (data, matches) => { + return data.me === matches.target && data.gravenImageCount === 1; + }, + delaySeconds: 0.1, // Actor position data can come after tether in log + durationSeconds: 7, + infoText: (data, matches, output) => { + const actor = data.actorPositions[matches.sourceId]; + if (actor === undefined) + return output.tetherOnYou!(); + + const x = actor.x; + // Graven Image 1: Pulse Wave target + if (x < 101 && x > 99) + return output.pulse!(); + return output.tetherOnYou!(); + }, + outputStrings: { + tetherOnYou: { + en: 'Tether on YOU', + de: 'Verbindung auf DIR', + fr: 'Lien sur VOUS', + ja: '線ついた', + cn: '连线点名', + ko: '선 대상자 지정됨', + tc: '連線點名', + }, + pulse: Outputs.knockback, // Cannot be immuned, happens within 6s of tether + }, + }, + { + id: 'DMU Gravitas and Vitrophyre Tethers 2', + type: 'Tether', + netRegex: { id: headMarkerData['imageTether'], capture: true }, + condition: (data, matches) => { + return data.me === matches.target && + data.isIceTrue !== undefined && + data.isThunderTrue === undefined && + data.isFireTrue === undefined; + }, + delaySeconds: 2, + durationSeconds: 6, + infoText: (data, matches, output) => { + const actor = data.actorPositions[matches.sourceId]; + if (actor === undefined) + return output.tetherOnYou!(); + + const x = actor.x; + if (x < 103 && x > 101) // Graven Image 2: Gravitas target + return output.gravitas!({ + mech1: output.puddle!(), + mech2: output.middle!(), + }); + if (x > 125) // Graven Image 2: Vitrophyre target + return output.vitrophyre!({ + mech1: output.puddle!(), + mech2: output.spread!(), + }); + return output.tetherOnYou!(); + }, + outputStrings: { + puddle: { + en: 'Bait Puddle', + de: 'Fläche ködern', + fr: 'Déposez', + ja: 'AOE誘導', + cn: '诱导AOE', + ko: '장판 유도', + tc: '誘導AOE', + }, + middle: Outputs.goIntoMiddle, + spread: Outputs.spread, + tetherOnYou: { + en: 'Tether on YOU', + de: 'Verbindung auf DIR', + fr: 'Lien sur VOUS', + ja: '線ついた', + cn: '连线点名', + ko: '선 대상자 지정됨', + tc: '連線點名', + }, + gravitas: { + en: '${mech1} => ${mech2}', + }, + vitrophyre: { + en: '${mech1} => ${mech2}', + }, + indulgent: { + en: 'Confuse Tether on YOU', + }, + idyllic: { + en: 'Sleep Tether on YOU', + }, + }, + }, + { + id: 'DMU P1 Vitrophyre', + // Trigger on BAAC Gravitas, ~4s to get away + type: 'Ability', + netRegex: { id: 'BAAC', source: 'Graven Image', capture: false }, + suppressSeconds: 1, + alertText: (data, _matches, output) => { + if (data.gravenImageTether === 'vitrophyre') + return output.spread!(); + return output.avoidTethers!(); + }, + outputStrings: { + avoidTethers: 'Avoid Tethered Players', + spread: 'Spread (avoid puddles)', + }, + }, + { + id: 'DMU Indulgent Will and Idyllic Will Tethers', + type: 'Tether', + netRegex: { id: headMarkerData['imageTether'], capture: true }, + condition: (data, matches) => { + return data.me === matches.target && data.gravenImageCount === 3; + }, + infoText: (data, matches, output) => { + const actor = data.actorPositions[matches.sourceId]; + if (actor === undefined) + return output.tetherOnYou!(); + + const x = actor.x; + if (x < 100) // Graven Image 3: Indulgent Will target + return output.indulgent!(); + if (x < 108 && x > 106) // Graven Image 3: Idyllic Will target + return output.idyllic!(); + return output.tetherOnYou!(); + }, + outputStrings: { + tetherOnYou: { + en: 'Tether on YOU', + de: 'Verbindung auf DIR', + fr: 'Lien sur VOUS', + ja: '線ついた', + cn: '连线点名', + ko: '선 대상자 지정됨', + tc: '連線點名', + }, + indulgent: { + en: 'Confuse Tether on YOU', + }, + idyllic: { + en: 'Sleep Tether on YOU', + }, + }, + }, + { + id: 'DMU P1 Graven Image Tether Cleanup', + // Clear on Ability: + // BAA9 Pulse Wave + // BAAC Gravitas + // BAB0 vitrophyre + // BAB5 Indulgent Will + // BAB6 Idyllic Will + type: 'Ability', + netRegex: { + id: ['BAA9', 'BAAC', 'BAB0', 'BAB5', 'BAB6'], + source: 'Graven Image', + capture: true, + }, + suppressSeconds: 1, + run: (data, matches) => { + // Player could die and this ability then not target them + // Need intelligent way to remove once related ability has executed + // Clear data if ability matches our tether + const abilityMap = { + 'pulse': 'BAAC', + 'gravitas': 'BAA9', + 'vitrophyre': 'BAB0', + 'indulgent': 'BAB5', + 'idyllic': 'BAB6', + 'unknown': 'unknown', + }; + const tether = data.gravenImageTether ?? 'unknown'; + const tetherAbilityId = abilityMap[tether]; + if (tetherAbilityId === matches.id || tether === 'unknown') + delete data.gravenImageTether; + }, + }, { id: 'DMU P1 CombatantMemory Tower Tracker', // 1EBFBB => Wave Cannon entity (blue) @@ -330,7 +601,7 @@ const triggerSet: TriggerSet = { outputStrings: mysteryMagicOutputStrings, }, { - id: 'DMU P1 Mystery Magic Ice Only', + id: 'DMU P1 Mystery Magic Ice, and Gravitas and Vitrophyre Tethers 1', // Occurs between Set 2 and Set 3 // BA95 Blizzard Blowout III cast type: 'StartsUsing', @@ -345,9 +616,18 @@ const triggerSet: TriggerSet = { return false; }, infoText: (data, _matches, output) => { + const hasVitrophyre = data.gravenImageTether === 'vitrophyre'; return data.isIceTrue - ? output.trueIce!() - : output.fakeIce!(); + ? output.trueIcePuddle!({ + mech1: output.trueIce!(), + mech2: output.puddle!(), + mech3: hasVitrophyre ? output.spread!() : output.middle!(), + }) + : output.fakeIcePuddle!({ + mech1: output.fakeIce!(), + mech2: output.puddle!(), + mech3: hasVitrophyre ? output.spread!() : output.middle!(), + }); }, outputStrings: mysteryMagicOutputStrings, },