diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 23ed3a8757..35ada1b520 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,6 +1,11 @@ +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 { TriggerSet } from '../../../../../types/trigger'; +import { OutputStrings, TriggerSet } from '../../../../../types/trigger'; + +// TODO: P1 Tele-Portent configuration options type Phase = 'p1' | 'p2' | 'p3'; const phases: { [id: string]: Phase } = { @@ -14,8 +19,132 @@ const phases: { [id: string]: Phase } = { export interface Data extends RaidbossData { // General phase: Phase | 'unknown'; + // Phase 1 + blueTowerIds: string[]; + 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; + isThunderTrue?: boolean; + waveCannonTargets: string[]; + doubleTroubleTrapTargets: string[]; + myTelePortent1?: 'up' | 'down' | 'right' | 'left'; + myTelePortent2?: 'up' | 'down' | 'right' | 'left'; } +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) + // 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', + fr: 'Packez-vous', + ja: 'スタック', + cn: '集合', + ko: '집합', + tc: '集合', + }, + trueThunder: { + en: 'Avoid Tell', + }, + fakeThunder: { + en: 'In Line', + }, + trueIce: { + en: 'Avoid Tell', + }, + fakeIce: { + en: 'In Cone', + }, + trueIcePuddle: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + fakeIcePuddle: { + en: '${mech1} + ${mech2} => ${mech3}', + }, + stackTrueIce: { + en: '${mech} + ${ice}', + }, + stackFakeIce: { + en: '${mech} + ${ice}', + }, + spreadTrueIce: { + en: '${mech} + ${ice}', + }, + spreadFakeIce: { + en: '${mech} + ${ice}', + }, + trueIceTrueThunder: { + en: 'Avoid Tells', + }, + fakeIceTrueThunder: { + en: 'Cone (only)', + }, + trueIceFakeThunder: { + en: 'Line (only)', + }, + fakeIceFakeThunder: { + en: 'Cone + Line', + }, + stackTrueThunder: { + en: '${mech} + ${thunder}', + }, + stackFakeThunder: { + en: '${mech} + ${thunder}', + }, + spreadTrueThunder: { + en: '${mech} + ${thunder}', + }, + spreadFakeThunder: { + en: '${mech} + ${thunder}', + }, +}; + +const trapOutputStrings: OutputStrings = { + knockbackFrom: { + en: 'Knockback from ${players}', + }, + knockbackFromLater: { + en: 'Knockback from ${players} (later)', + }, +}; + const triggerSet: TriggerSet = { id: 'DancingMadUltimate', zoneId: ZoneId.DancingMadUltimate, @@ -23,6 +152,14 @@ const triggerSet: TriggerSet = { initData: () => { return { phase: 'p1', + // Phase 1 + blueTowerIds: [], + yellowTowerIds: [], + purpleTowerIds: [], + actorPositions: {}, + gravenImageCount: 0, + waveCannonTargets: [], + doubleTroubleTrapTargets: [], }; }, triggers: [ @@ -32,6 +169,961 @@ 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 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 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; + if (target === data.me) + return output.cleaveOnYou!(); + + if (data.role === 'tank') + return output.cleaveSwap!({ + player: data.party.member(target), + }); + + if (data.role === 'healer') + return output.cleaveOnPlayer!({ + player: data.party.member(target), + }); + + return output.avoidCleaves!(); + }, + outputStrings: { + in: Outputs.in, + out: Outputs.out, + cleaveOnYou: Outputs.tankCleaveOnYou, + avoidCleaves: Outputs.avoidTankCleaves, + cleaveOnPlayer: { + en: 'Tank Cleave on ${player}', + }, + cleaveSwap: { // Defaulting to same output as cleaveOnPlayer + en: 'Tank Cleave on ${player}', + }, + }, + }, + { + 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', + 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 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 + // 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 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 + // 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: false }, + 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 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; + + 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 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 P1 Double-trouble Trap 3 Early', + type: 'GainsEffect', + netRegex: { effectId: '13D6', capture: true }, + delaySeconds: 0.1, + suppressSeconds: 1, + infoText: (data, matches, output) => { + const duration = parseFloat(matches.duration); + // Only capture 3rd set + if (duration < 48 || duration > 50) + 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 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 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 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; + + // Check if players died + if (data.doubleTroubleTrapTargets[0] === undefined) + return; + + 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 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; + + // Check if players died + if (data.doubleTroubleTrapTargets[0] === undefined) + return; + + 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 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; + }, + }, + { + 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, + }, + { + 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: [ { diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt index 97fe98c9a0..5276443d0c 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.txt @@ -26,9 +26,10 @@ 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.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,8 +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" } -118.9 "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? +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 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" } @@ -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" } @@ -176,7 +179,7 @@ hideall "--sync--" # Phase 2 Enrage Sequence 10376.2 label "p2-enrage" -10381.2 "Light of Judgment (Enrage)" #Ability { id: "BAE1", source: "Kefka" } # Kefka > 0% HP +10381.2 "Light of Judgment (Enrage)" Ability { id: "BAE1", source: "Kefka" } # Kefka > 0% HP # IGNORED ABILITIES # C252 Attack: P1 Kefka attack and P3 Chaos attack