From 3bdc726da5783cbef3ceb625615fbedb909a71cc Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sat, 27 Jun 2026 08:28:40 -0400 Subject: [PATCH 1/4] initial draft --- .../data/07-dt/ultimate/dancing_mad.ts | 653 ++++++++++++++++++ 1 file changed, 653 insertions(+) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index b21cd17c07..c1364f83e3 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -7,6 +7,7 @@ import { RaidbossData } from '../../../../../types/data'; import { OutputStrings, TriggerSet } from '../../../../../types/trigger'; // TODO: P2 Old AAAABBBB plan was found at https://raidplan.io/plan/kj2d734d36es2ugs, would like to find replacement +// TODO: P4 Add detection for Fake Chaos and Fake Neo Exdeath debuffs // TODO: Earlier phase tracking for P5 (counting the jumps to middle?) type Phase = 'p1' | 'p2' | 'p3' | 'p4' | 'p5'; @@ -68,6 +69,18 @@ export interface Data extends RaidbossData { forsakenGroupB: string[]; // List of players in Group B trineDirNums: number[]; middleTrineFacing?: 'east' | 'west'; + // Phase 4 + grandCrossCount: number; + shortShriekPlayers: string[]; + longShriekPlayers: string[]; + shortForkedPlayers: string[]; + longForkedPlayers: string[]; + shortCompressedPlayers: string[]; + longCompressedPlayers: string[]; + shortBombPlayers: string[]; + longBombPlayers: string[]; + deathOrField?: 'death' | 'field'; + wound?: 'white' | 'black'; } const headMarkerData = { @@ -587,6 +600,16 @@ const triggerSet: TriggerSet = { forsakenGroupA: [], forsakenGroupB: [], trineDirNums: [], + // Phase 4 + grandCrossCount: 0, + shortShriekPlayers: [], + longShriekPlayers: [], + shortForkedPlayers: [], + longForkedPlayers: [], + shortCompressedPlayers: [], + longCompressedPlayers: [], + shortBombPlayers: [], + longBombPlayers: [], }; }, triggers: [ @@ -3839,6 +3862,636 @@ const triggerSet: TriggerSet = { }, }, }, + { + id: 'DMU P4 Tsunami/Inferno', + // BB14 Grand Cross AoE also happens ~4s after this cast starts + // BB20 Inferno / BB21 Tsunami are 9s castTime + type: 'StartsUsing', + netRegex: { id: ['BB20', 'BB21'], source: 'Chaos', capture: true }, + delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 6, + response: Responses.aoe(), + }, + { + id: 'DMU P4 Grand Cross Counter', + type: 'StartsUsing', + netRegex: { id: 'BB14', source: 'Neo Exdeath', capture: false }, + run: (data) => data.grandCrossCount = data.grandCrossCount + 1, + }, + { + id: 'DMU P4 Grand Cross', + // 9s castTime + type: 'StartsUsing', + netRegex: { id: 'BB14', source: 'Neo Exdeath', capture: true }, + delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 6, + response: Responses.aoe(), + }, + { + id: 'DMU P4 Debuff Collect', + // Neo Exdeath Debuffs Cast 1: (12:28.672) + // 15A7 Cursed Shriek x2 60s => 13:28.672 + // 15A8 Forked Lightning x2 76s => 13:44.672 + // 15A9 Compressed Water x2 76s => 13:44.672 + // 15AA Acceleration Bomb (2 Long 76s, 2 Short 51s) => 13:19.672 13:44.672 + // Chaos Debuffs 1: (12:34.341) + // 15AC Dynamic Fluid x8 84s => 13:48.672 + // Neo Exdeath Debuffs Cast 2: (12:43.578) + // 15A8 Forked Lightning x2 36s => 13:19.578 + // 15A7 Cursed Shriek x2 69s => 13:52.578 + // 15A9 Compressed Water x2 36s => 13:19.578 + // 15AA Acceleration Bomb (2 Long 61s, 2 Short 36s) => 13:19.578 13:44.578 + // Chaos Debuffs 2: 12:50.485 + // 15AB Entroy x8 45s => 13:35.485 + // Neo Exdeath Debuffs Cast 3: (13:00.002) + // 1317 White Wound + // 1318 Black Wound + // 1558 Beyond Death x4 15s => 13:15.002 + // 1C6 Allagan Field x4 15s => 13:15.002 + // Neo Exdeath Final: (13:11.383) + // 1317 or 15A5 White Wound + // 1318 or 15A6 Black Wound + // + // For Neo Exdeath, after the second Grand Cross cast there will be: + // 1 Support, 1 DPS with Short 3-Person Stack (Compressed Water and/or Fake Forked Lightning) + // 1 Support, 1 DPS with Long 3-Person Stack (Compressed Water and/or Fake Forked Lightning) + // 1 Support, 1 DPS with Short Gaze (Cursed Shriek or Fake Cursed Shriek) + // 1 Support, 1 DPS with Long Gaze (Cursed Shriek or Fake Cursed Shriek) + // 2 Support, 2 DPS with Short Stillness (Acceleration Bomb or Fake Acceleration Bomb) + // 2 Support, 2 DPS with Long Stillness (Acceleration Bomb or Fake Acceleration Bomb) + // For 3rd set of debuffs we do not need to know real/fake: + // Allagan Field players swap their color by getting hit with opposite color Angilight + // Beyond Death field players keep their color by getting hit with same colore Antilight + // Entropy and Dynamic Fluids will be handled seperately + type: 'GainsEffect', + netRegex: { + effectId: [ + '15A7', + '15A8', + '15A9', + '15AA', + '1317', + '1318', + '1558', + '1C6' + ], + capture: true, + }, + run: (data, matches) => { + const target = matches.target; + const id = matches.effectId; + const duration = parseFloat(matches.duration); + + // Cursed Shriek + if (id === '15A7') { + if (duration < 61) + data.shortShriekPlayers.push(target); + else + data.longShriekPlayers.push(target); + } else if (id === '15A8') { + // Forked Lightning + if (duration < 37) + data.shortForkedPlayers.push(target); + else + data.longForkedPlayers.push(target); + } else if (id === '15A9') { + // Compressed Water + if (duration < 37) + data.shortCompressedPlayers.push(target); + else + data.longCompressedPlayers.push(target); + } else if (id === '15AA') { + // Acceleration Bomb + if (duration < 52) + data.shortBombPlayers.push(target); + else + data.longBombPlayers.push(target); + } else if (data.me === target) { + // Cast 5 / 6 Debuffs + if (id === '1558') + data.deathOrField = 'death'; + else if (id === '1C6') + data.deathOrField = 'field'; + else if (id === '1317' || id === '15A5') + data.wound = 'white'; + else if (id === '1318' || id === '15A6') + data.wound = 'black'; + } + }, + }, + { + id: 'DMU P4 First Debuffs (Early)', + // Cast 1: + // 15A7 Cursed Shriek x2 60s + // 15A8 Forked Lightning x2 76s + // 15A9 Compressed Water x2 76s + // 15AA Acceleration Bomb (2 Long 76s, 2 Short 51s) + // TODO: Detect if fake + type: 'GainsEffect', + netRegex: { effectId: ['15A7', '15A8', '15A9', '15AA'], capture: true }, + condition: Conditions.targetIsYou(), + delaySeconds: 0.1, + suppressSeconds: 99999, + infoText: (data, _matches, output) => { + const hasShreik = data.shortShriekPlayers.includes(data.me); + const hasFork = data.longForkedPlayers.includes(data.me); + const hasCompressed = data.longCompressedPlayers.includes(data.me); + const hasBomb = data.longBombPlayers.includes(data.me); + + // Shriek players always are short bomb + // This will be two players + if (hasShreik) + return output.firstGazeAndBomb!({ + gaze: output.gaze!(), + bomb: output.bomb!(), + }); + + // Remaing 6 players will get one debuff + if (hasFork) + return output.spreadSecond!({ mech: output.spread!() }); + if (hasCompressed) + return output.stackSecond!({ mech: output.stack!() }); + if (hasBomb) + return output.bombSecond!({ mech: output.bomb!() }); + }, + outputStrings: { + firstGazeAndBomb: { + en: '${gaze} + ${bomb} on YOU First', + }, + gaze: { + en: 'Look Away', + }, + fakeGaze: { + en: 'Look At', + }, + spreadSecond: { + en: '${mech} on YOU Second', + }, + stackSecond: { + en: '${mech} on YOU Second', + }, + bombSecond: { + en: '${mech} on YOU Second', + }, + stack: Outputs.stackMarker, + fakeStack: Outputs.spread, + spread: Outputs.spread, + fakeSpread: Outputs.stackMarker, + bomb: { + en: 'Stillness', + }, + fakeBomb: { + en: 'Motion', + }, + }, + }, + { + id: 'DMU P4 Dynamic Fluid (Early)', + // TODO: Detect if fake + type: 'GainsEffect', + netRegex: { effectId: '15AC', capture: false }, + delaySeconds: 0.1, + suppressSeconds: 99999, + infoText: (_data, _matches, output) => output.text!(), + outputStrings: { + text: { + en: 'Donuts or Twisters (later)', + }, + }, + }, + { + id: 'DMU P4 Second Debuffs (Early)', + // Cast 2: + // 15A8 Forked Lightning x2 36s + // 15A7 Cursed Shriek x2 69s + // 15A9 Compressed Water x2 36s + // 15AA Acceleration Bomb (2 Long 61s, 2 Short 36s) + // TODO: Detect if fake + type: 'GainsEffect', + netRegex: { effectId: ['15A7', '15A8', '15A9', '15AA'], capture: true }, + condition: (data, matches) => { + return data.me === matches.target && data.grandCrossCount === 2; + }, + delaySeconds: 0.1, + suppressSeconds: 99999, + infoText: (data, _matches, output) => { + const hasShreik = data.longShriekPlayers.includes(data.me); + const hasFork = data.shortForkedPlayers.includes(data.me); + const hasCompressed = data.shortCompressedPlayers.includes(data.me); + const hasBomb = data.shortBombPlayers.includes(data.me); + + // Shriek players always are short bomb + // This will be two players + if (hasShreik) + return output.secondGazeAndBomb!({ + gaze: output.gaze!(), + bomb: output.bomb!(), + }); + + // Remaing 6 players will get one debuff + if (hasFork) + return output.spreadFirst!({ mech: output.spread!() }); + if (hasCompressed) + return output.stackFirst!({ mech: output.stack!() }); + if (hasBomb) + return output.bombFirst!({ mech: output.bomb!() }); + }, + outputStrings: { + secondGazeAndBomb: { + en: '${gaze} + ${bomb} on YOU Second', + }, + gaze: { + en: 'Look Away', + }, + fakeGaze: { + en: 'Look At', + }, + spreadFirst: { + en: '${mech} on YOU First', + }, + stackFirst: { + en: '${mech} on YOU First', + }, + bombFirst: { + en: '${mech} on YOU First', + }, + stack: Outputs.stackMarker, + fakeStack: Outputs.spread, + spread: Outputs.spread, + fakeSpread: Outputs.stackMarker, + bomb: { + en: 'Stillness', + }, + fakeBomb: { + en: 'Motion', + }, + }, + }, + { + id: 'DMU P4 Entropy (Early)', + // TODO: Detect if fake + type: 'GainsEffect', + netRegex: { effectId: '15AB', capture: false }, + delaySeconds: 0.1, + suppressSeconds: 99999, + infoText: (_data, _matches, output) => output.text!(), + outputStrings: { + text: { + en: 'Twisters or Donuts (later)', + }, + }, + }, + { + id: 'DMU P4 Third Debuffs', + // Neo Exdeath Debuffs Cast 3 + // 1317 White Wound + // 1318 Black Wound + // 1558 Beyond Death x4 15s + // 1C6 Allagan Field x4 15s + // TODO: Detect if short debuffs fake + type: 'GainsEffect', + netRegex: { effectId: ['1317', '1318', '1558', '1C6'], capture: true }, + condition: Conditions.targetIsYou(), + delaySeconds: 0.1, + durationSeconds: 9, + suppressSeconds: 99999, + infoText: (data, _matches, output) => { + const wound = data.wound; + const deathOrField = data.deathOrField; + + if (wound === undefined || deathOrField === undefined) + return; + const laser = output[deathOrField]!({ + color: deathOrField === 'death' + ? output[wound]!() + : wound === 'white' + ? output.black!() + : output.white!(), + }); + + const hasFork = data.shortForkedPlayers.includes(data.me); + const hasCompressed = data.shortCompressedPlayers.includes(data.me); + const hasBomb = data.shortBombPlayers.includes(data.me); + + // Handle 2 Mechs + if (hasFork && hasBomb) + return output.laserThenForkBomb!({ + mech1: laser, + mech2: output.spread!(), + mech3: output.bomb!(), + }); + if (hasCompressed && hasBomb) + return output.laserThenCompressedBomb!({ + mech1: laser, + mech2: output.stack!(), + mech3: output.bomb!(), + }); + if (hasFork) + return output.laserThenSpread!({ + mech1: laser, + mech2: output.spread!(), + }); + if (hasCompressed) + return output.laserThenStack!({ + mech1: laser, + mech2: output.stack!(), + }); + if (hasBomb) + return output.laserThenBomb!({ + mech1: laser, + mech2: output.bomb!(), + mech3: output.stack!(), + }); + }, + outputStrings: { + death: { + en: 'Stand in ${color}', + }, + field: { + en: 'Stand in ${color}', + }, + white: { + en: 'Purple', + }, + black: { + en: 'Blue', + }, + laserThenSpread: { + en: '${mech1} => ${mech2}', + }, + laserThenStack: { + en: '${mech1} => ${mech2}', + }, + laserThenBomb: { + en: '${mech1} => ${mech2} + ${mech3}', + }, + laserThenForkBomb: { + en: '${mech1} => ${mech2} + ${mech3}', + }, + laserThenCompressedBomb: { + en: '${mech1} => ${mech2} + ${mech3}', + }, + stack: Outputs.stackMarker, + fakeStack: Outputs.spread, + spread: Outputs.spread, + fakeSpread: Outputs.stackMarker, + bomb: { + en: 'Stillness', + }, + fakeBomb: { + en: 'Motion', + }, + }, + }, + { + id: 'DMU P4 Short Debuffs', + // Using the following as possible matches: + // 1558 Beyond Death x4 15s + // 1C6 Allagan Field x4 15s + // The spells/abilities or losesEffect could be triggered from early deaths + // TODO: Detect if short debuffs fake + type: 'GainsEffect', + netRegex: { effectId: ['1558', '1C6'], capture: true }, + delaySeconds: (_data, matches) => parseFloat(matches.duration), + suppressSeconds: 99999, + alertText: (data, _matches, output) => { + const hasFork = data.shortForkedPlayers.includes(data.me); + const hasCompressed = data.shortCompressedPlayers.includes(data.me); + const hasBomb = data.shortBombPlayers.includes(data.me); + + const players = data.shortShriekPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Handle 2 Mechs + if (hasFork && hasBomb) + return output.forkBombThenGaze!({ + mech1: output.spread!(), + mech2: output.bomb!(), + mech3: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasCompressed && hasBomb) + return output.compressedBombThenGaze!({ + mech1: output.stack!(), + mech2: output.bomb!(), + mech3: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasFork) + return output.spreadThenGaze!({ + mech1: output.spread!(), + mech2: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasCompressed) + return output.stackThenGaze!({ + mech1: output.stack!(), + mech2: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasBomb) + return output.bombThenGaze!({ + mech1: output.bomb!(), + mech2: output.stack!(), + mech3: output.lookAwayFromPlayers!({ players: msg }), + }); + }, + outputStrings: { + you: { + en: 'YOU', + }, + spreadThenGaze: { + en: '${mech1} => ${mech2}', + }, + stackThenGaze: { + en: '${mech1} => ${mech2}', + }, + bombThenGaze: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + forkBombThenGaze: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + compressedBombThenGaze: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + stack: Outputs.stackMarker, + fakeStack: Outputs.spread, + spread: Outputs.spread, + fakeSpread: Outputs.stackMarker, + bomb: { + en: 'Stillness', + }, + fakeBomb: { + en: 'Motion', + }, + lookAtPlayers: { + en: 'Face ${players}', + }, + lookAwayFromPlayers: { + en: 'Look Away from ${players}', + }, + }, + }, + { + id: 'DMU P4 First Cursed Shriek', + // TODO: Detect if short debuffs fake + // TODO: Merge this with Mana Charge (stored fake/real thunder) + type: 'GainsEffect', + netRegex: { effectId: '15A7', capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) < 61, + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 6, + suppressSeconds: 99999, + infoText: (data, _matches, output) => { + const players = data.shortShriekPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + return output.shriekThenEntropy!({ + mech1: msg, + mech2: output.twistersOrDonuts!(), + }); + }, + outputStrings: { + you: { + en: 'YOU', + }, + shriekThenEntropy: { + en: '${mech1} => ${mech2}', + }, + lookAtPlayers: { + en: 'Face ${players}', + }, + lookAwayFromPlayers: { + en: 'Look Away from ${players}', + }, + twistersOrDonuts: { + en: 'Twisters or Donuts', + }, + }, + }, + { + id: 'DMU P4 Entropy', + // Using the following as possible matches: + // 15A7 Cursed Shriek x2 60s + // TODO: Detect if shriek debuffs fake + // TODO: Merge this with Mana Charge (stored fake/real thunder) + type: 'GainsEffect', + netRegex: { effectId: '15A7', capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) < 61, + delaySeconds: (_data, matches) => parseFloat(matches.duration), + suppressSeconds: 99999, + alertText: (_data, _matches, output) => output.twistersOrDonuts!(), + outputStrings: { + twistersOrDonuts: { + en: 'Twisters or Donuts', + }, + }, + }, + { + id: 'DMU P4/P5 Ultima Upsurge', + type: 'StartsUsing', + netRegex: { id: 'C24A', source: 'Kefka', capture: false }, + response: Responses.bigAoe(), + }, + { + id: 'DMU P4 Long Debuffs', + // Using the following as possible matches: + // 15A8 Forked Lightning x2 76s + // 15A9 Compressed Water x2 76s + // 15AA Acceleration Bomb (2 Long 76s) + // TODO: Detect if long debuffs fake + type: 'GainsEffect', + netRegex: { effectId: ['15A8', '15A9', '15AA'], capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) > 75, + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 6, + suppressSeconds: 99999, + alertText: (data, _matches, output) => { + const hasFork = data.longForkedPlayers.includes(data.me); + const hasCompressed = data.longCompressedPlayers.includes(data.me); + const hasBomb = data.longBombPlayers.includes(data.me); + + const players = data.longShriekPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Handle 2 Mechs + if (hasFork && hasBomb) + return output.forkBombThenGaze!({ + mech1: output.spread!(), + mech2: output.bomb!(), + mech3: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasCompressed && hasBomb) + return output.compressedBombThenGaze!({ + mech1: output.stack!(), + mech2: output.bomb!(), + mech3: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasFork) + return output.spreadThenGaze!({ + mech1: output.spread!(), + mech2: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasCompressed) + return output.stackThenGaze!({ + mech1: output.stack!(), + mech2: output.lookAwayFromPlayers!({ players: msg }), + }); + if (hasBomb) + return output.bombThenGaze!({ + mech1: output.bomb!(), + mech2: output.stack!(), + mech3: output.lookAwayFromPlayers!({ players: msg }), + }); + }, + outputStrings: { + you: { + en: 'YOU', + }, + spreadThenGaze: { + en: '${mech1} => ${mech2}', + }, + stackThenGaze: { + en: '${mech1} => ${mech2}', + }, + bombThenGaze: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + forkBombThenGaze: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + compressedBombThenGaze: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + stack: Outputs.stackMarker, + fakeStack: Outputs.spread, + spread: Outputs.spread, + fakeSpread: Outputs.stackMarker, + bomb: { + en: 'Stillness', + }, + fakeBomb: { + en: 'Motion', + }, + lookAtPlayers: { + en: 'Face ${players}', + }, + lookAwayFromPlayers: { + en: 'Look Away from ${players}', + }, + }, + }, ], timelineReplace: [ { From 3517017cb684b40bece86dc04d76e41a8c27eecb Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sat, 27 Jun 2026 09:34:22 -0400 Subject: [PATCH 2/4] add template for checking true casts Still need to determine how the casts are done, if itss a headmarker, etc --- .../data/07-dt/ultimate/dancing_mad.ts | 341 ++++++++++++++---- 1 file changed, 268 insertions(+), 73 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index c1364f83e3..edda9001ba 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -79,6 +79,10 @@ export interface Data extends RaidbossData { longCompressedPlayers: string[]; shortBombPlayers: string[]; longBombPlayers: string[]; + areFirstDebuffsTrue?: boolean; + isEntropyTrue?: boolean; + areSecondDebuffsTrue?: boolean; + isDynamicFluidTrue?: boolean; deathOrField?: 'death' | 'field'; wound?: 'white' | 'black'; } @@ -3871,6 +3875,15 @@ const triggerSet: TriggerSet = { delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 6, response: Responses.aoe(), }, + { + id: 'DMU P4 Tsunami/Inferno', + // BB14 Grand Cross AoE also happens ~4s after this cast starts + // BB20 Inferno / BB21 Tsunami are 9s castTime + type: 'StartsUsing', + netRegex: { id: ['BB20', 'BB21'], source: 'Chaos', capture: true }, + delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 6, + response: Responses.aoe(), + }, { id: 'DMU P4 Grand Cross Counter', type: 'StartsUsing', @@ -3984,7 +3997,6 @@ const triggerSet: TriggerSet = { // 15A8 Forked Lightning x2 76s // 15A9 Compressed Water x2 76s // 15AA Acceleration Bomb (2 Long 76s, 2 Short 51s) - // TODO: Detect if fake type: 'GainsEffect', netRegex: { effectId: ['15A7', '15A8', '15A9', '15AA'], capture: true }, condition: Conditions.targetIsYou(), @@ -3995,22 +4007,28 @@ const triggerSet: TriggerSet = { const hasFork = data.longForkedPlayers.includes(data.me); const hasCompressed = data.longCompressedPlayers.includes(data.me); const hasBomb = data.longBombPlayers.includes(data.me); + const isTrue = data.areFirstDebuffsTrue; + + if (isTrue === undefined) + return; // Shriek players always are short bomb // This will be two players if (hasShreik) return output.firstGazeAndBomb!({ - gaze: output.gaze!(), - bomb: output.bomb!(), + gaze: isTrue ? output.gaze!() : output.fakeGaze!(), + bomb: isTrue ? output.bomb!() : output.fakeBomb!(), }); // Remaing 6 players will get one debuff - if (hasFork) + if ((hasFork && isTrue) || (hasCompressed && !isTrue)) return output.spreadSecond!({ mech: output.spread!() }); - if (hasCompressed) + if ((hasFork && !isTrue) || (hasCompressed && isTrue)) return output.stackSecond!({ mech: output.stack!() }); if (hasBomb) - return output.bombSecond!({ mech: output.bomb!() }); + return output.bombSecond!({ + mech: isTrue ? output.bomb!() : output.fakeBomb!(), + }); }, outputStrings: { firstGazeAndBomb: { @@ -4032,9 +4050,7 @@ const triggerSet: TriggerSet = { en: '${mech} on YOU Second', }, stack: Outputs.stackMarker, - fakeStack: Outputs.spread, spread: Outputs.spread, - fakeSpread: Outputs.stackMarker, bomb: { en: 'Stillness', }, @@ -4045,15 +4061,24 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P4 Dynamic Fluid (Early)', - // TODO: Detect if fake type: 'GainsEffect', netRegex: { effectId: '15AC', capture: false }, delaySeconds: 0.1, suppressSeconds: 99999, - infoText: (_data, _matches, output) => output.text!(), + infoText: (data, _matches, output) => { + const isFluidTrue = data.isDynamicFluidTrue; + if (isFluidTrue === undefined) + return; + return isFluidTrue + ? output.donutsSecond!() + : output.twistersSecond!(); + }, outputStrings: { - text: { - en: 'Donuts or Twisters (later)', + twistersSecond: { + en: 'Twisters Second', + }, + donutsSecond: { + en: 'Donuts Second', }, }, }, @@ -4064,7 +4089,6 @@ const triggerSet: TriggerSet = { // 15A7 Cursed Shriek x2 69s // 15A9 Compressed Water x2 36s // 15AA Acceleration Bomb (2 Long 61s, 2 Short 36s) - // TODO: Detect if fake type: 'GainsEffect', netRegex: { effectId: ['15A7', '15A8', '15A9', '15AA'], capture: true }, condition: (data, matches) => { @@ -4077,22 +4101,28 @@ const triggerSet: TriggerSet = { const hasFork = data.shortForkedPlayers.includes(data.me); const hasCompressed = data.shortCompressedPlayers.includes(data.me); const hasBomb = data.shortBombPlayers.includes(data.me); + const isTrue = data.areSecondDebuffsTrue; + + if (isTrue === undefined) + return; // Shriek players always are short bomb // This will be two players if (hasShreik) return output.secondGazeAndBomb!({ - gaze: output.gaze!(), - bomb: output.bomb!(), + gaze: isTrue ? output.gaze!() : output.fakeGaze!(), + bomb: isTrue ? output.bomb!() : output.fakeBomb!(), }); // Remaing 6 players will get one debuff - if (hasFork) + if ((hasFork && isTrue) || (hasCompressed && !isTrue)) return output.spreadFirst!({ mech: output.spread!() }); - if (hasCompressed) + if ((hasFork && !isTrue) || (hasCompressed && isTrue)) return output.stackFirst!({ mech: output.stack!() }); if (hasBomb) - return output.bombFirst!({ mech: output.bomb!() }); + return output.bombFirst!({ + mech: isTrue ? output.bomb!() : output.fakeBomb!(), + }); }, outputStrings: { secondGazeAndBomb: { @@ -4114,9 +4144,7 @@ const triggerSet: TriggerSet = { en: '${mech} on YOU First', }, stack: Outputs.stackMarker, - fakeStack: Outputs.spread, spread: Outputs.spread, - fakeSpread: Outputs.stackMarker, bomb: { en: 'Stillness', }, @@ -4127,15 +4155,24 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P4 Entropy (Early)', - // TODO: Detect if fake type: 'GainsEffect', netRegex: { effectId: '15AB', capture: false }, delaySeconds: 0.1, suppressSeconds: 99999, - infoText: (_data, _matches, output) => output.text!(), + infoText: (data, _matches, output) => { + const isEntropyTrue = data.isEntropyTrue; + if (isEntropyTrue === undefined) + return; + return isEntropyTrue + ? output.twistersFirst!() + : output.donutsFirst!(); + }, outputStrings: { - text: { - en: 'Twisters or Donuts (later)', + twistersFirst: { + en: 'Twisters First', + }, + donutsFirst: { + en: 'Donuts First', }, }, }, @@ -4146,7 +4183,6 @@ const triggerSet: TriggerSet = { // 1318 Black Wound // 1558 Beyond Death x4 15s // 1C6 Allagan Field x4 15s - // TODO: Detect if short debuffs fake type: 'GainsEffect', netRegex: { effectId: ['1317', '1318', '1558', '1C6'], capture: true }, condition: Conditions.targetIsYou(), @@ -4170,26 +4206,33 @@ const triggerSet: TriggerSet = { const hasFork = data.shortForkedPlayers.includes(data.me); const hasCompressed = data.shortCompressedPlayers.includes(data.me); const hasBomb = data.shortBombPlayers.includes(data.me); + const isTrue = data.areSecondDebuffsTrue; + + if (isTrue === undefined) + return laser; + + const isSpread = (hasFork && isTrue) || (hasCompressed && !isTrue); + const isStack = (hasFork && !isTrue) || (hasCompressed && isTrue); // Handle 2 Mechs - if (hasFork && hasBomb) + if (isSpread && hasBomb) return output.laserThenForkBomb!({ mech1: laser, mech2: output.spread!(), - mech3: output.bomb!(), + mech3: isTrue ? output.bomb!() : output.fakeBomb!(), }); - if (hasCompressed && hasBomb) + if (isStack && hasBomb) return output.laserThenCompressedBomb!({ mech1: laser, mech2: output.stack!(), - mech3: output.bomb!(), + mech3: isTrue ? output.bomb!() : output.fakeBomb!(), }); - if (hasFork) + if (isSpread) return output.laserThenSpread!({ mech1: laser, mech2: output.spread!(), }); - if (hasCompressed) + if (isStack) return output.laserThenStack!({ mech1: laser, mech2: output.stack!(), @@ -4197,7 +4240,7 @@ const triggerSet: TriggerSet = { if (hasBomb) return output.laserThenBomb!({ mech1: laser, - mech2: output.bomb!(), + mech2: isTrue ? output.bomb!() : output.fakeBomb!(), mech3: output.stack!(), }); }, @@ -4230,9 +4273,7 @@ const triggerSet: TriggerSet = { en: '${mech1} => ${mech2} + ${mech3}', }, stack: Outputs.stackMarker, - fakeStack: Outputs.spread, spread: Outputs.spread, - fakeSpread: Outputs.stackMarker, bomb: { en: 'Stillness', }, @@ -4247,7 +4288,6 @@ const triggerSet: TriggerSet = { // 1558 Beyond Death x4 15s // 1C6 Allagan Field x4 15s // The spells/abilities or losesEffect could be triggered from early deaths - // TODO: Detect if short debuffs fake type: 'GainsEffect', netRegex: { effectId: ['1558', '1C6'], capture: true }, delaySeconds: (_data, matches) => parseFloat(matches.duration), @@ -4256,6 +4296,11 @@ const triggerSet: TriggerSet = { const hasFork = data.shortForkedPlayers.includes(data.me); const hasCompressed = data.shortCompressedPlayers.includes(data.me); const hasBomb = data.shortBombPlayers.includes(data.me); + const is2ndTrue = data.areSecondDebuffsTrue; + const is1stTrue = data.areFirstDebuffsTrue; + + if (is2ndTrue === undefined || is1stTrue === undefined) + return; const players = data.shortShriekPlayers.map( (player) => { @@ -4266,34 +4311,40 @@ const triggerSet: TriggerSet = { ); const msg = players?.join(', '); + const gaze = is1stTrue + ? output.lookAwayFromPlayers!({ players: msg }) + : output.lookAtPlayers!({ players: msg }); + const isSpread = (hasFork && is2ndTrue) || (hasCompressed && !is2ndTrue); + const isStack = (hasFork && !is2ndTrue) || (hasCompressed && is2ndTrue); + // Handle 2 Mechs - if (hasFork && hasBomb) + if (isSpread && hasBomb) return output.forkBombThenGaze!({ mech1: output.spread!(), - mech2: output.bomb!(), - mech3: output.lookAwayFromPlayers!({ players: msg }), + mech2: is2ndTrue ? output.bomb!() : output.fakeBomb!(), + mech3: gaze, }); - if (hasCompressed && hasBomb) + if (isStack && hasBomb) return output.compressedBombThenGaze!({ mech1: output.stack!(), - mech2: output.bomb!(), - mech3: output.lookAwayFromPlayers!({ players: msg }), + mech2: is2ndTrue ? output.bomb!() : output.fakeBomb!(), + mech3: gaze, }); - if (hasFork) + if (isSpread) return output.spreadThenGaze!({ mech1: output.spread!(), - mech2: output.lookAwayFromPlayers!({ players: msg }), + mech2: gaze, }); - if (hasCompressed) + if (isStack) return output.stackThenGaze!({ mech1: output.stack!(), - mech2: output.lookAwayFromPlayers!({ players: msg }), + mech2: gaze, }); if (hasBomb) return output.bombThenGaze!({ - mech1: output.bomb!(), + mech1: is2ndTrue ? output.bomb!() : output.fakeBomb!(), mech2: output.stack!(), - mech3: output.lookAwayFromPlayers!({ players: msg }), + mech3: gaze, }); }, outputStrings: { @@ -4316,9 +4367,7 @@ const triggerSet: TriggerSet = { en: '${mech1} + ${mech2} => ${mech3}', }, stack: Outputs.stackMarker, - fakeStack: Outputs.spread, spread: Outputs.spread, - fakeSpread: Outputs.stackMarker, bomb: { en: 'Stillness', }, @@ -4335,7 +4384,6 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P4 First Cursed Shriek', - // TODO: Detect if short debuffs fake // TODO: Merge this with Mana Charge (stored fake/real thunder) type: 'GainsEffect', netRegex: { effectId: '15A7', capture: true }, @@ -4343,6 +4391,12 @@ const triggerSet: TriggerSet = { delaySeconds: (_data, matches) => parseFloat(matches.duration) - 6, suppressSeconds: 99999, infoText: (data, _matches, output) => { + const is1stTrue = data.areFirstDebuffsTrue; + const isEntropyTrue = data.isEntropyTrue; + + if (is1stTrue === undefined || isEntropyTrue === undefined) + return; + const players = data.shortShriekPlayers.map( (player) => { if (player === data.me) @@ -4353,8 +4407,10 @@ const triggerSet: TriggerSet = { const msg = players?.join(', '); return output.shriekThenEntropy!({ - mech1: msg, - mech2: output.twistersOrDonuts!(), + mech1: is1stTrue + ? output.lookAwayFromPlayers!({ players: msg }) + : output.lookAtPlayers!({ players: msg }), + mech2: isEntropyTrue ? output.twisters!() : output.donuts!(), }); }, outputStrings: { @@ -4370,8 +4426,22 @@ const triggerSet: TriggerSet = { lookAwayFromPlayers: { en: 'Look Away from ${players}', }, - twistersOrDonuts: { - en: 'Twisters or Donuts', + donuts: { + en: 'Stack for Donuts', + de: 'Für Donuts sammeln', + fr: 'Packez-vous pour les donuts', + cn: '集合放月环', + ko: '모여서 도넛장판 피하기', + tc: '集合放月環', + }, + twisters: { + en: 'Twisters', + de: 'Wirbelstürme', + fr: 'Tornades', + ja: '大竜巻', + cn: '旋风', + ko: '회오리', + tc: '旋風', }, }, }, @@ -4379,17 +4449,36 @@ const triggerSet: TriggerSet = { id: 'DMU P4 Entropy', // Using the following as possible matches: // 15A7 Cursed Shriek x2 60s - // TODO: Detect if shriek debuffs fake // TODO: Merge this with Mana Charge (stored fake/real thunder) type: 'GainsEffect', netRegex: { effectId: '15A7', capture: true }, condition: (_data, matches) => parseFloat(matches.duration) < 61, delaySeconds: (_data, matches) => parseFloat(matches.duration), suppressSeconds: 99999, - alertText: (_data, _matches, output) => output.twistersOrDonuts!(), + alertText: (data, _matches, output) => { + const isEntropyTrue = data.isEntropyTrue; + if (isEntropyTrue === undefined) + return; + + return isEntropyTrue ? output.twisters!() : output.donuts!(); + }, outputStrings: { - twistersOrDonuts: { - en: 'Twisters or Donuts', + donuts: { + en: 'Stack for Donuts', + de: 'Für Donuts sammeln', + fr: 'Packez-vous pour les donuts', + cn: '集合放月环', + ko: '모여서 도넛장판 피하기', + tc: '集合放月環', + }, + twisters: { + en: 'Twisters', + de: 'Wirbelstürme', + fr: 'Tornades', + ja: '大竜巻', + cn: '旋风', + ko: '회오리', + tc: '旋風', }, }, }, @@ -4405,7 +4494,6 @@ const triggerSet: TriggerSet = { // 15A8 Forked Lightning x2 76s // 15A9 Compressed Water x2 76s // 15AA Acceleration Bomb (2 Long 76s) - // TODO: Detect if long debuffs fake type: 'GainsEffect', netRegex: { effectId: ['15A8', '15A9', '15AA'], capture: true }, condition: (_data, matches) => parseFloat(matches.duration) > 75, @@ -4415,6 +4503,11 @@ const triggerSet: TriggerSet = { const hasFork = data.longForkedPlayers.includes(data.me); const hasCompressed = data.longCompressedPlayers.includes(data.me); const hasBomb = data.longBombPlayers.includes(data.me); + const is1stTrue = data.areFirstDebuffsTrue; + const is2ndTrue = data.areSecondDebuffsTrue; + + if (is2ndTrue === undefined || is1stTrue === undefined) + return; const players = data.longShriekPlayers.map( (player) => { @@ -4425,34 +4518,40 @@ const triggerSet: TriggerSet = { ); const msg = players?.join(', '); + const gaze = is2ndTrue + ? output.lookAwayFromPlayers!({ players: msg }) + : output.lookAtPlayers!({ players: msg }); + const isSpread = (hasFork && is1stTrue) || (hasCompressed && !is1stTrue); + const isStack = (hasFork && !is1stTrue) || (hasCompressed && is1stTrue); + // Handle 2 Mechs - if (hasFork && hasBomb) + if (isSpread && hasBomb) return output.forkBombThenGaze!({ mech1: output.spread!(), - mech2: output.bomb!(), - mech3: output.lookAwayFromPlayers!({ players: msg }), + mech2: is1stTrue ? output.bomb!() : output.fakeBomb!(), + mech3: gaze, }); - if (hasCompressed && hasBomb) + if (isStack && hasBomb) return output.compressedBombThenGaze!({ mech1: output.stack!(), - mech2: output.bomb!(), - mech3: output.lookAwayFromPlayers!({ players: msg }), + mech2: is1stTrue ? output.bomb!() : output.fakeBomb!(), + mech3: gaze, }); - if (hasFork) + if (isSpread) return output.spreadThenGaze!({ mech1: output.spread!(), - mech2: output.lookAwayFromPlayers!({ players: msg }), + mech2: gaze, }); - if (hasCompressed) + if (isStack) return output.stackThenGaze!({ mech1: output.stack!(), - mech2: output.lookAwayFromPlayers!({ players: msg }), + mech2: gaze, }); if (hasBomb) return output.bombThenGaze!({ - mech1: output.bomb!(), + mech1: is1stTrue ? output.bomb!() : output.fakeBomb!(), mech2: output.stack!(), - mech3: output.lookAwayFromPlayers!({ players: msg }), + mech3: gaze, }); }, outputStrings: { @@ -4475,9 +4574,7 @@ const triggerSet: TriggerSet = { en: '${mech1} + ${mech2} => ${mech3}', }, stack: Outputs.stackMarker, - fakeStack: Outputs.spread, spread: Outputs.spread, - fakeSpread: Outputs.stackMarker, bomb: { en: 'Stillness', }, @@ -4492,6 +4589,104 @@ const triggerSet: TriggerSet = { }, }, }, + { + id: 'DMU P4 Second Cursed Shriek', + type: 'GainsEffect', + netRegex: { effectId: '15A7', capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) > 68, + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 6, + suppressSeconds: 99999, + infoText: (data, _matches, output) => { + const is2ndTrue = data.areSecondDebuffsTrue; + const isFluidTrue = data.isDynamicFluidTrue; + + if (is2ndTrue === undefined || isFluidTrue === undefined) + return; + + const players = data.shortShriekPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + return output.shriekThenFluid!({ + mech1: is2ndTrue + ? output.lookAwayFromPlayers!({ players: msg }) + : output.lookAtPlayers!({ players: msg }), + mech2: isFluidTrue ? output.donuts!() : output.twisters!(), + }); + }, + outputStrings: { + you: { + en: 'YOU', + }, + shriekThenFluid: { + en: '${mech1} => ${mech2}', + }, + lookAtPlayers: { + en: 'Face ${players}', + }, + lookAwayFromPlayers: { + en: 'Look Away from ${players}', + }, + donuts: { + en: 'Stack for Donuts', + de: 'Für Donuts sammeln', + fr: 'Packez-vous pour les donuts', + cn: '集合放月环', + ko: '모여서 도넛장판 피하기', + tc: '集合放月環', + }, + twisters: { + en: 'Twisters', + de: 'Wirbelstürme', + fr: 'Tornades', + ja: '大竜巻', + cn: '旋风', + ko: '회오리', + tc: '旋風', + }, + }, + }, + { + id: 'DMU P4 Dynamic Fluid', + // Using the following as possible matches: + // 15A7 Cursed Shriek x2 69s + type: 'GainsEffect', + netRegex: { effectId: '15A7', capture: true }, + condition: (_data, matches) => parseFloat(matches.duration) > 68, + delaySeconds: (_data, matches) => parseFloat(matches.duration), + suppressSeconds: 99999, + alertText: (data, _matches, output) => { + const isFluidTrue = data.isDynamicFluidTrue; + if (isFluidTrue === undefined) + return; + + return isFluidTrue ? output.donuts!() : output.twisters!(); + }, + outputStrings: { + donuts: { + en: 'Stack for Donuts', + de: 'Für Donuts sammeln', + fr: 'Packez-vous pour les donuts', + cn: '集合放月环', + ko: '모여서 도넛장판 피하기', + tc: '集合放月環', + }, + twisters: { + en: 'Twisters', + de: 'Wirbelstürme', + fr: 'Tornades', + ja: '大竜巻', + cn: '旋风', + ko: '회오리', + tc: '旋風', + }, + }, + }, ], timelineReplace: [ { From dc42a0c5d67fc3a6c727d524eb4c13aef8a3f57a Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sat, 27 Jun 2026 09:42:55 -0400 Subject: [PATCH 3/4] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index edda9001ba..f51de7c28c 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -3937,14 +3937,14 @@ const triggerSet: TriggerSet = { type: 'GainsEffect', netRegex: { effectId: [ - '15A7', - '15A8', - '15A9', - '15AA', - '1317', - '1318', - '1558', - '1C6' + '15A7', + '15A8', + '15A9', + '15AA', + '1317', + '1318', + '1558', + '1C6', ], capture: true, }, From a81d5166469b55bc3b1e4db3837274cf9082d713 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sat, 27 Jun 2026 09:51:22 -0400 Subject: [PATCH 4/4] fix paste error --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index f51de7c28c..c65fc5bb5f 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -3875,15 +3875,6 @@ const triggerSet: TriggerSet = { delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 6, response: Responses.aoe(), }, - { - id: 'DMU P4 Tsunami/Inferno', - // BB14 Grand Cross AoE also happens ~4s after this cast starts - // BB20 Inferno / BB21 Tsunami are 9s castTime - type: 'StartsUsing', - netRegex: { id: ['BB20', 'BB21'], source: 'Chaos', capture: true }, - delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 6, - response: Responses.aoe(), - }, { id: 'DMU P4 Grand Cross Counter', type: 'StartsUsing',