From e2dda9d432484fb9245c5d6d8abccdeb46ebdd19 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 2 Jun 2026 21:29:18 -0400 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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, }, From 5b9411b196d8c44741f78d10a1f2495263126ce5 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 4 Jun 2026 23:49:30 -0400 Subject: [PATCH 21/28] reorder triggers, lint, add trap knockback lines --- .../data/07-dt/ultimate/dancing_mad.ts | 822 +++++++++--------- .../data/07-dt/ultimate/dancing_mad.txt | 7 +- 2 files changed, 416 insertions(+), 413 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 8414453f3d..5b541d474b 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -184,236 +184,6 @@ const triggerSet: TriggerSet = { 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) @@ -507,6 +277,85 @@ const triggerSet: TriggerSet = { }, }, }, + { + 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 P1 Mystery Magic Collect', type: 'HeadMarker', @@ -580,98 +429,6 @@ const triggerSet: TriggerSet = { }, 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!() - : output.fakeIceTrueThunder!(); - } - return data.isIceTrue - ? output.trueIceTrueThunder!() - : output.fakeIceFakeThunder!(); - }, - outputStrings: mysteryMagicOutputStrings, - }, - { - 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', - netRegex: { id: 'BA95', source: 'Kefka', capture: false }, - condition: (data) => { - if ( - data.isIceTrue !== undefined && - data.isThunderTrue === undefined && - data.isFireTrue === undefined - ) - return true; - return false; - }, - infoText: (data, _matches, output) => { - const hasVitrophyre = data.gravenImageTether === 'vitrophyre'; - return data.isIceTrue - ? 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, - }, - { - id: 'DMU P1 Mystery Magic Fire and Thunder', - // Set 3: Only Fire 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 @@ -684,6 +441,39 @@ const triggerSet: TriggerSet = { delete data.fireMarker; }, }, + { + 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 Wave Cannon', // BAA8 Wave Cannon is an instant cast from Graven Image @@ -707,6 +497,13 @@ const triggerSet: TriggerSet = { netRegex: { id: 'BAA8', source: 'Graven Image', capture: true }, run: (data, matches) => data.waveCannonTargets.push(matches.target), }, + { + 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 Wave Cannon Explosion Towers', // Wave Cannon gives a vulnerability which causes death to BAAA Explosion soaks @@ -758,38 +555,147 @@ const triggerSet: TriggerSet = { }, }, { - id: 'DMU P1 Double-trouble Trap Collect', - // Times are 5s, 68s, and 49s + id: 'DMU P1 Double-trouble Trap 1', type: 'GainsEffect', netRegex: { effectId: '13D6', capture: true }, - run: (data, matches) => data.doubleTroubleTrapTargets.push(matches.target), + condition: (_data, matches) => parseFloat(matches.duration) < 6, + delaySeconds: 0.1, + suppressSeconds: 1, + response: (data, _matches, output) => { + // cactbot-builtin-response + output.responseOutputStrings = trapOutputStrings; + + 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 }) }; + }, + }, + { + id: 'DMU P1 Double-trouble Trap Cleanup', + // Players dying will also trigger this + type: 'LosesEffect', + netRegex: { effectId: '13D6', capture: true }, + run: (data, matches) => { + data.doubleTroubleTrapTargets = data.doubleTroubleTrapTargets.filter( + (target) => target !== matches.target, + ); + }, + }, + { + 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 and third set + if (parseFloat(matches.duration) < 67) + return; + + // Check if players died + if (data.doubleTroubleTrapTargets[0] === undefined) + return; + + 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: trapOutputStrings, + }, + { + 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!() + : output.fakeIceTrueThunder!(); + } + return data.isIceTrue + ? output.trueIceTrueThunder!() + : output.fakeIceFakeThunder!(); + }, + outputStrings: mysteryMagicOutputStrings, + }, + { + 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 + // 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) - 2, // Result in ~5.1s warning + response: Responses.tankBuster(), + }, + { + 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', + netRegex: { id: 'BA95', source: 'Kefka', capture: false }, + condition: (data) => { + if ( + data.isIceTrue !== undefined && + data.isThunderTrue === undefined && + data.isFireTrue === undefined + ) + return true; + return false; + }, + infoText: (data, _matches, output) => { + const hasVitrophyre = data.gravenImageTether === 'vitrophyre'; + return data.isIceTrue + ? 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, }, { - id: 'DMU P1 Double-trouble Trap 2 Early', - type: 'GainsEffect', - netRegex: { effectId: '13D6', capture: true }, - delaySeconds: 0.1, + id: 'DMU P1 Vitrophyre', + // Trigger on BAAC Gravitas, ~4s to get away + type: 'Ability', + netRegex: { id: 'BAAC', source: 'Graven Image', capture: false }, suppressSeconds: 1, - infoText: (data, matches, output) => { - // Ignore first set and third set - if (parseFloat(matches.duration) < 67) - return; - - // Check if players died - if (data.doubleTroubleTrapTargets[0] === undefined) - return; - - 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 }); + alertText: (data, _matches, output) => { + if (data.gravenImageTether === 'vitrophyre') + return output.spread!(); + return output.avoidTethers!(); + }, + outputStrings: { + avoidTethers: 'Avoid Tethered Players', + spread: 'Spread (avoid puddles)', }, - outputStrings: trapOutputStrings, }, { id: 'DMU P1 Double-trouble Trap 3 Early', @@ -820,26 +726,86 @@ const triggerSet: TriggerSet = { outputStrings: trapOutputStrings, }, { - 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 = trapOutputStrings; + 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 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 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 }) }; + 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', + }, }, }, { @@ -899,50 +865,6 @@ const triggerSet: TriggerSet = { return { [severity]: output.knockbackFrom!({ players: msg }) }; }, }, - { - id: 'DMU P1 Double-trouble Trap Cleanup', - // Players dying will also trigger this - type: 'LosesEffect', - netRegex: { effectId: '13D6', capture: true }, - run: (data, matches) => { - data.doubleTroubleTrapTargets = data.doubleTroubleTrapTargets.filter( - (target) => target !== matches.target, - ); - }, - }, - { - 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 - // 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) - 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: @@ -1127,6 +1049,84 @@ const triggerSet: TriggerSet = { delete data.myTelePortent2; }, }, + { + 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 Mystery Magic Fire and Thunder', + // Set 3: Only Fire 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, + }, ], timelineReplace: [ { diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt index 40f49ce8a5..2f89a9b1b3 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt @@ -28,7 +28,8 @@ hideall "--sync--" 42.5 "Wave Cannon" Ability { id: "BAA8", source: "Graven Image" } 44.6 "Double-trouble Trap" Ability { id: "BAA6", source: "Kefka" } 46.0 "Explosion" Ability { id: "BAAA", source: "Kefka" } -49.7 "Double-trouble Trap" Ability { id: "BAA7", source: "Kefka" } +49.6 "--knockback--" # From Double-trouble Trap 1 +49.7 "Double-trouble Trap 2" 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" } @@ -48,7 +49,8 @@ hideall "--sync--" 105.8 "Gravitas" Ability { id: "BAAC", source: "Graven Image" } 109.8 "Vitrophyre" Ability { id: "BAB0", source: "Graven Image" } 114.4 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } -118.9 "Double-Trouble Trap" Ability { id: "BAA7", source: "Kefka" } # NOTE: If it was passed after first set. +118.0 "--knockback--" # From Double-trouble Trap 2 +118.1 "Double-Trouble Trap" 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" } @@ -60,6 +62,7 @@ hideall "--sync--" 163.6 "Graven Image 3" Ability { id: "BCF2", source: "Kefka" } 168.7 "--sync--" Ability { id: "C554", source: "Kefka" } +170.6 "--knockback--" # From Double-trouble Trap 3 173.4 "Indulgent Will" Ability { id: "BAB5", source: "Graven Image" } 173.4 "Idyllic Will" #Ability { id: "BAB6", source: "Graven Image" } 177.7 "--sync--" Ability { id: "C555", source: "Kefka" } From fb675b79cf0e832dbb3ec6cf1e7c07f43fcf4aff Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 00:02:27 -0400 Subject: [PATCH 22/28] double-trouble trap and gravity timing adjust --- ui/raidboss/data/07-dt/ultimate/dancing_mad.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt index 2f89a9b1b3..5276443d0c 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt @@ -26,7 +26,7 @@ hideall "--sync--" 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" Ability { id: "BAA8", source: "Graven Image" } -44.6 "Double-trouble Trap" Ability { id: "BAA6", source: "Kefka" } +44.6 "Double-trouble Trap 1" Ability { id: "BAA6", source: "Kefka" } 46.0 "Explosion" Ability { id: "BAAA", source: "Kefka" } 49.6 "--knockback--" # From Double-trouble Trap 1 49.7 "Double-trouble Trap 2" Ability { id: "BAA7", source: "Kefka" } @@ -49,9 +49,9 @@ hideall "--sync--" 105.8 "Gravitas" Ability { id: "BAAC", source: "Graven Image" } 109.8 "Vitrophyre" Ability { id: "BAB0", source: "Graven Image" } 114.4 "Intemperate Will/Gravitational Wave" Ability { id: ["BAB2", "BAB1"], source: "Graven Image" } +117.0 "Gravity III (Pop Window)" #Ability { id: "BAAF", source: "Kefka" } duration 6 # ~1s remaining on debuff and until 45s on next 118.0 "--knockback--" # From Double-trouble Trap 2 -118.1 "Double-Trouble Trap" 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? +118.1 "Double-Trouble Trap 3" Ability { id: "BAA7", source: "Kefka" } # NOTE: If it was passed after first set. 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" } From 763996f24901e988177eb0f5012e287606da13a4 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 00:09:53 -0400 Subject: [PATCH 23/28] fix mystery magic cleanup order --- .../data/07-dt/ultimate/dancing_mad.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 5b541d474b..35c1e0b8c9 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -429,18 +429,6 @@ const triggerSet: TriggerSet = { }, 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 Graven Image Tether Cleanup', // Clear on Ability: @@ -1127,6 +1115,18 @@ const triggerSet: TriggerSet = { }, 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; + }, + }, ], timelineReplace: [ { From acdbfc05ae639190c9bb1360534a1ab73f3d3935 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 00:13:18 -0400 Subject: [PATCH 24/28] remove done TODOs --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 3 --- 1 file changed, 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 35c1e0b8c9..35ada1b520 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -5,9 +5,6 @@ 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' | 'p3'; From 72298a1a4e7068dad8f87415831d14b6b20113b0 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 13:38:27 -0400 Subject: [PATCH 25/28] replace CombatantMemory with OverlayHandler --- .../data/07-dt/ultimate/dancing_mad.ts | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 35ada1b520..da4f551360 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,4 +1,5 @@ import Conditions from '../../../../../resources/conditions'; +import { callOverlayHandler } from '../../../../../resources/overlay_plugin_api'; import Outputs from '../../../../../resources/outputs'; import { Responses } from '../../../../../resources/responses'; import ZoneId from '../../../../../resources/zone_id'; @@ -182,45 +183,42 @@ const triggerSet: TriggerSet = { }, }, { - id: 'DMU P1 CombatantMemory Tower Tracker', - // 1EBFBB => Wave Cannon entity (blue) - // 1EBFBC => Gravitational Wave entity (purple) - // 1EBFBD => Intemperate Will entity (yellow) + id: 'DMU P1 Graven Image Collect', + // Tower entity actions + // The CombatantMemory Add lines are added prior to combat + // OverlayPlugin call used to retrieve the matching BNpcID + // 1EBFBB (2015163) => Wave Cannon entity (blue) + // 1EBFBC (2015164) => Gravitational Wave entity (purple) + // 1EBFBD (2015165) => 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) => { + type: 'ActorControlExtra', + netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, + promise: async (data, matches) => { + const ids = [parseInt((matches.id), 16)]; + const actors = (await callOverlayHandler({ + call: 'getCombatants', + ids: ids, + })).combatants; + const image = actors[0]; + if (image === undefined) + return; + const towerMap = { - '1EBFBB': 'blue', - '1EBFBC': 'purple', - '1EBFBD': 'yellow', + '2015163': 'blue', + '2015164': 'purple', + '2015165': 'yellow', 'unknown': 'unknown', }; - const bnpcid = matches.pairBNpcID ?? 'unknown'; + + const bnpcid = image.BNpcID ?? 'unknown'; const kind = towerMap[bnpcid as keyof typeof towerMap]; - if (kind === 'blue') { + if (kind === 'blue') data.blueTowerIds.push(matches.id); - return; - } - if (kind === 'yellow') { + else if (kind === 'yellow') data.yellowTowerIds.push(matches.id); - return; - } - if (kind === 'purple') { + else 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; From fad1b17f7d457db3493afdc804a0e79e4732bf22 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 13:51:09 -0400 Subject: [PATCH 26/28] change string[] to string + lint --- .../data/07-dt/ultimate/dancing_mad.ts | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index da4f551360..9dd7b913ee 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,6 +1,6 @@ import Conditions from '../../../../../resources/conditions'; -import { callOverlayHandler } from '../../../../../resources/overlay_plugin_api'; import Outputs from '../../../../../resources/outputs'; +import { callOverlayHandler } from '../../../../../resources/overlay_plugin_api'; import { Responses } from '../../../../../resources/responses'; import ZoneId from '../../../../../resources/zone_id'; import { RaidbossData } from '../../../../../types/data'; @@ -21,9 +21,9 @@ export interface Data extends RaidbossData { // General phase: Phase | 'unknown'; // Phase 1 - blueTowerIds: string[]; - yellowTowerIds: string[]; - purpleTowerIds: string[]; + blueTowerId?: string; + yellowTowerId?: string; + purpleTowerId?: string; tower?: 'blue' | 'yellow' | 'purple'; gravenImageCount: number; actorPositions: { [id: string]: { x: number; y: number; heading: number } }; @@ -154,9 +154,6 @@ const triggerSet: TriggerSet = { return { phase: 'p1', // Phase 1 - blueTowerIds: [], - yellowTowerIds: [], - purpleTowerIds: [], actorPositions: {}, gravenImageCount: 0, waveCannonTargets: [], @@ -194,10 +191,10 @@ const triggerSet: TriggerSet = { type: 'ActorControlExtra', netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, promise: async (data, matches) => { - const ids = [parseInt((matches.id), 16)]; + const id = matches.id; const actors = (await callOverlayHandler({ call: 'getCombatants', - ids: ids, + ids: [parseInt(id, 16)], })).combatants; const image = actors[0]; if (image === undefined) @@ -213,24 +210,24 @@ const triggerSet: TriggerSet = { const bnpcid = image.BNpcID ?? 'unknown'; const kind = towerMap[bnpcid as keyof typeof towerMap]; if (kind === 'blue') - data.blueTowerIds.push(matches.id); + data.blueTowerId = id; else if (kind === 'yellow') - data.yellowTowerIds.push(matches.id); + data.yellowTowerId = id; else if (kind === 'purple') - data.purpleTowerIds.push(matches.id); + data.purpleTowerId = id; }, run: (data, matches) => { const id = matches.id; - if (data.yellowTowerIds.indexOf(id) !== -1) { + if (data.yellowTowerId === id) { data.tower = 'yellow'; return; } - if (data.purpleTowerIds.indexOf(id) !== -1) { + if (data.purpleTowerId === id) { data.tower = 'purple'; return; } - if (data.blueTowerIds.indexOf(id) !== -1) { + if (data.blueTowerId === id) { data.tower = 'blue'; return; } @@ -464,7 +461,7 @@ const triggerSet: TriggerSet = { type: 'ActorControlExtra', netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, alertText: (data, matches, output) => { - if (data.blueTowerIds.indexOf(matches.id) !== -1) + if (data.blueTowerId === matches.id) return output.waveCannonLine!(); }, outputStrings: { @@ -714,10 +711,10 @@ const triggerSet: TriggerSet = { netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, alertText: (data, matches, output) => { const id = matches.id; - if (data.yellowTowerIds.indexOf(id) !== -1) { + if (data.yellowTowerId === id) { return output.goWest!(); } - if (data.purpleTowerIds.indexOf(id) !== -1) { + if (data.purpleTowerId === id) { return output.goEast!(); } }, From 52687c9c1f344b9f2ae78d6985e2f06ada9766e1 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 14:05:33 -0400 Subject: [PATCH 27/28] some cleanup --- .../data/07-dt/ultimate/dancing_mad.ts | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 9dd7b913ee..3dcec16c14 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -219,18 +219,12 @@ const triggerSet: TriggerSet = { run: (data, matches) => { const id = matches.id; - if (data.yellowTowerId === id) { + if (data.yellowTowerId === id) data.tower = 'yellow'; - return; - } - if (data.purpleTowerId === id) { + else if (data.purpleTowerId === id) data.tower = 'purple'; - return; - } - if (data.blueTowerId === id) { + else if (data.blueTowerId === id) data.tower = 'blue'; - return; - } }, }, { @@ -277,7 +271,7 @@ const triggerSet: TriggerSet = { run: (data) => data.gravenImageCount = data.gravenImageCount + 1, }, { - id: 'DMU Graven Image Tether Collect', + id: 'DMU P1 Graven Image Tether Collect', // 271 ActorSetPos lines indicate where the tether is coming from // 261 CombatantMemory lines may also indicate this // Graven Image 1: @@ -316,7 +310,7 @@ const triggerSet: TriggerSet = { }, }, { - id: 'DMU Pulse Wave Tethers', + id: 'DMU P1 Pulse Wave Tethers', type: 'Tether', netRegex: { id: headMarkerData['imageTether'], capture: true }, condition: (data, matches) => { @@ -460,10 +454,8 @@ const triggerSet: TriggerSet = { // 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.blueTowerId === matches.id) - return output.waveCannonLine!(); - }, + condition: (data, matches) => data.blueTowerId === matches.id, + alertText: (data, _matches, output) => output.waveCannonLine!(), outputStrings: { waveCannonLine: { en: 'E/W Spread', @@ -706,25 +698,27 @@ const triggerSet: TriggerSet = { outputStrings: trapOutputStrings, }, { - id: 'DMU P1 Impertinent Will/Gravitational Wave', + id: 'DMU P1 Impertinent Will', type: 'ActorControlExtra', netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, - alertText: (data, matches, output) => { - const id = matches.id; - if (data.yellowTowerId === id) { - return output.goWest!(); - } - if (data.purpleTowerId === id) { - return output.goEast!(); - } - }, + condition: (data, matches) => data.yellowTowerId === matches.id, + alertText: (data, _matches, output) => output.goWest!(), outputStrings: { goWest: Outputs.getLeftAndWest, + }, + }, + { + id: 'DMU P1 Gravitational Wave', + type: 'ActorControlExtra', + netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, + condition: (data, matches) => data.purpleTowerId === matches.id, + alertText: (data, _matches, output) => output.goEast!(), + outputStrings: { goEast: Outputs.getRightAndEast, }, }, { - id: 'DMU Gravitas and Vitrophyre Tethers 2', + id: 'DMU P1 Gravitas and Vitrophyre Tethers 2', type: 'Tether', netRegex: { id: headMarkerData['imageTether'], capture: true }, condition: (data, matches) => { @@ -1030,7 +1024,7 @@ const triggerSet: TriggerSet = { }, }, { - id: 'DMU Indulgent Will and Idyllic Will Tethers', + id: 'DMU P1 Indulgent Will and Idyllic Will Tethers', type: 'Tether', netRegex: { id: headMarkerData['imageTether'], capture: true }, condition: (data, matches) => { From 8080fe5f5659e3b2fadbd2fbd68dae6648b0383b Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 5 Jun 2026 14:08:35 -0400 Subject: [PATCH 28/28] unused data --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 6 +++--- 1 file changed, 3 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 3dcec16c14..d196a6a108 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -455,7 +455,7 @@ const triggerSet: TriggerSet = { type: 'ActorControlExtra', netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, condition: (data, matches) => data.blueTowerId === matches.id, - alertText: (data, _matches, output) => output.waveCannonLine!(), + alertText: (_data, _matches, output) => output.waveCannonLine!(), outputStrings: { waveCannonLine: { en: 'E/W Spread', @@ -702,7 +702,7 @@ const triggerSet: TriggerSet = { type: 'ActorControlExtra', netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, condition: (data, matches) => data.yellowTowerId === matches.id, - alertText: (data, _matches, output) => output.goWest!(), + alertText: (_data, _matches, output) => output.goWest!(), outputStrings: { goWest: Outputs.getLeftAndWest, }, @@ -712,7 +712,7 @@ const triggerSet: TriggerSet = { type: 'ActorControlExtra', netRegex: { category: '019D', param1: '40', param2: '80', capture: true }, condition: (data, matches) => data.purpleTowerId === matches.id, - alertText: (data, _matches, output) => output.goEast!(), + alertText: (_data, _matches, output) => output.goEast!(), outputStrings: { goEast: Outputs.getRightAndEast, },