From bff2b1add60cb250bb56faadef62cdcc9a8423d3 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 02:51:31 -0400 Subject: [PATCH 01/49] p2-initial-triggers --- .../data/07-dt/ultimate/dancing_mad.ts | 843 +++++++++++++++++- 1 file changed, 842 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 23ed3a87577..450521c0c9d 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,6 +1,9 @@ +import Outputs from '../../../../../resources/outputs'; +import { Responses } from '../../../../../resources/responses'; +import Util from '../../../../../resources/util'; 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' | 'p3'; const phases: { [id: string]: Phase } = { @@ -12,17 +15,102 @@ const phases: { [id: string]: Phase } = { // const centerY = 100; export interface Data extends RaidbossData { + readonly triggerSetConfig: { + teleportent: 'clockwise' | 'filipino' | 'none'; + forsaken: 'kroxy-rinon' | 'none'; + }; // General phase: Phase | 'unknown'; + // Phase 2 + pathOfLightCounter: number; + pathOfLightStackPlayers: string[]; + pathOfLightConePlayers: string[]; + pathOfLightSpreadPlayers: string[]; + myPathOfLights: string[]; } +const headMarkerData = { + // Phase 2 + 'sharedBuster': '0103', // Ultimate Embrace shared tankbuster + 'stackPath': '02CB', // When standing in Path of Light tower, causes BAC0 Spelldriver (3-person stack) + 'conePath': '02CD', // When standing in Path of Light tower, causes BAC2 Spellwave (cone targetting nearest player) + 'spreadPath': '02CC', // When standing in Path of Light tower, causes BAC1 Spellscatter (small aoe on the player) +} as const; + +const forsakenOutputStrings: OutputStrings = { + tower: Outputs.getTowers, + leftTower: { + en: 'Left Tower', + }, + rightTower: { + en: 'Right Tower', + }, + stackOnYou: Outputs.stackOnYou, + cone: { + en: 'Cone on YOU', + }, + spread: { + en: 'AOE on YOU', + }, + stackOnYouTower: { + en: '${tower} + ${marker}', + }, + stackOnPlayer: { + en: 'Stack is on ${player}', + }, + stacksOnPlayers: { + en: 'Stacks on ${players}', + }, + markerOnYouStacksOnPlayers: { + en: '${marker} + ${stacks}', + }, + markerOnYouTower: { + en: '${marker} + ${tower}', + }, + leftStack: { + en: 'Left Stack/Cone', + }, + rightStack: { + en: 'Right Stack' + }, + groupBTowers: { + en: 'Group B Towers', + }, +}; + const triggerSet: TriggerSet = { id: 'DancingMadUltimate', zoneId: ZoneId.DancingMadUltimate, + config: [ + { + id: 'forsaken', + comment: { + en: + `Kroxy-Rinon 3/4/1: Kefka Bin`, + }, + name: { + en: 'P2 Forsaken Strategy', + }, + type: 'select', + options: { + en: { + 'Group soak order: AAABBBBA. Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.': 'kroxy-rinon', + 'Generic calls.': 'none', + }, + }, + default: 'none', + }, + ], timelineFile: 'dancing_mad.txt', initData: () => { return { phase: 'p1', + // Phase 2 + pathOfLightCounter: 1, + myPathOfLights: [], + pathOfLightStackPlayers: [], + pathOfLightConePlayers: [], + pathOfLightSpreadPlayers: [], }; }, triggers: [ @@ -32,6 +120,759 @@ const triggerSet: TriggerSet = { netRegex: { id: Object.keys(phases) }, run: (data, matches) => data.phase = phases[matches.id] ?? 'unknown', }, + { + id: 'DMU P2 Ultimate Embrace', + type: 'StartsUsing', + netRegex: { id: 'C24C', source: 'Kefka', capture: true }, + response: Responses.sharedTankBuster(), + }, + { + id: 'DMU P2 Forsaken', + // 7s cast + type: 'StartsUsing', + netRegex: { id: 'BABC', source: 'Kefka', capture: false }, + durationSeconds: 6.7, + response: Responses.bigAoe('alert'), + }, + { + id: 'DMU P2 Path of Light Headmarker Tracker', + // When standing in Path of Light tower, causes BAC0 Spelldriver (3-person stack) + // When standing in Path of Light tower, causes BAC2 Spellwave (cone targetting nearest player) + // When standing in Path of Light tower, causes BAC1 Spellscatter (small aoe on the player) + // Headmarkers update ~2.5s prior to 13DB Spell's Trouble debuff count decrementing + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: true, + }, + run: (data, matches) => { + const id = matches.id; + const target = matches.target; + type markerMap = { + [key: string]: string; + }; + const markers: markerMap = { + '02CB': 'stack', + '02CD': 'cone', + '02CC': 'spread', + }; + + // Storing self for simple lookups later + // This can also be used to track how many towers have been soaked + // and what was soaked before to handle who baits where on evens + if (data.me === target) + data.myPathOfLights.push(markers[id] ?? 'unknown'); + + // Clear previous Headmarker if set + data.pathOfLightStackPlayers.filter((t) => t !== target); + data.pathOfLightConePlayers.filter((t) => t !== target); + data.pathOfLightSpreadPlayers.filter((t) => t !== target); + + if (id === headMarkerData['stackPath']) + data.pathOfLightStackPlayers.push(target); + else if (id === headMarkerData['conePath']) + data.pathOfLightConePlayers.push(target); + else + data.pathOfLightSpreadPlayers.push(target); + }, + }, + { + id: 'DMU P2 Path of Light Towers 1', + // First Tower: + // 2 Soak markers + // 3 Cone markers (same role) + // 3 Spread markers (same role) + // If not marked for soak, check role of soak marked players, if matches + // player, add to output. Player will then know if they need to soak + // Unfortunately we do not know partners until the first tower is taken + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: true, + }, + condition: (data, matches) => { + return data.me === matches.target && data.pathOfLightCounter === 1; + }, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + infoText: (data, matches, output) => { + const id = matches.id; + type markerMap = { + [key: string]: 'stack' | 'cone' | 'spread'; + }; + const markers: markerMap = { + '02CB': 'stack', + '02CD': 'cone', + '02CC': 'spread', + }; + const marker = markers[id]; + if (marker === undefined) + return; + + if (marker === 'stack') { + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + if (data.role === 'healer' || data.role === 'tank') + return output.stackOnYouTower!({ + tower: output.leftTower!(), + marker: output.stackOnYou!(), + }); + return output.stackOnYouTower!({ + tower: output.rightTower!(), + marker: output.stackOnYou!(), + }); + } + return output.stackOnYouTower!({ + tower: output.tower!(), + marker: output.stackOnYou!(), + }); + } + + const stack1 = data.pathOfLightStackPlayers[0] ?? 'unknown'; + const stack2 = data.pathOfLightStackPlayers[1] ?? 'unknown'; + const stack1IsDPS = data.party.isDPS(stack1); + const stack2IsDPS = data.party.isDPS(stack2); + const myRoleIsDPS = data.party.isDPS(data.me); + + // If both stack players are the same role, output both players + if (myRoleIsDPS === stack1IsDPS && myRoleIsDPS === stack2IsDPS) { + const players = data.pathOfLightStackPlayers.map( + (player) => { + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + return output.markerOnYouStacksOnPlayers!({ + marker: output[marker]!(), + stacks: output.stacksOnPlayers!({ players: msg }), + }); + } + + // Our partner will be the role that matches us + const possiblePartner = data.party.member(myRoleIsDPS === stack1IsDPS ? stack1 : stack2); + return output.markerOnYouStacksOnPlayers!({ + marker: output[marker]!(), + stacks: output.stackOnPlayer!({ player: possiblePartner }), + }); + }, + outputStrings: forsakenOutputStrings, + }, + { + id: 'DMU P2 Path of Light Counter', + // Used to track which step of the paths we are own + // 4 Players soak Odd Towers, 4 Players soak Even Towers + // Headmarkers get applied to those hit ~0.5s after + type: 'Ability', + netRegex: { id: 'BABE', source: 'Kefka', capture: false }, + suppressSeconds: 1, + run: (data) => data.pathOfLightCounter = data.pathOfLightCounter + 1, + }, + { + id: 'DMU P2 Path of Light Towers 2', + // This set should not contain stack markers + // If stacks exist, they came from first set + // 2 Cones and 2 Spreads will soak towers + // + // Headmarkers come out ~2s before Future's/Past's End + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: true, + }, + condition: (data, matches) => { + return data.me === matches.target && data.pathOfLightCounter === 2; + }, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + infoText: (data, matches, output) => { + const id = matches.id; + type markerMap = { + [key: string]: 'stack' | 'cone' | 'spread'; + }; + const markers: markerMap = { + '02CB': 'stack', + '02CD': 'cone', + '02CC': 'spread', + }; + const marker = markers[id]; + if (marker === undefined) + return; + + // Unsure that this could happen, unless more than 4 players soaked? + if (marker === 'stack') + return; + + // Ignoring stack players that didn't soak tower 1 + // Check our previous headmarker + if (data.myPathOfLights[0] === 'cone') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beFar!(), + }); + if (data.myPathOfLights[0] === 'spread') + return output.mechs!({ + mech1: output.swapTowers!(), + mech2: output.beFar!(), + }); + }, + outputStrings: { + tower: Outputs.getTowers, + swapTowers: { + en: 'Swap Towers', + }, + beNear: { + en: 'Be Near', + de: 'Sei Nahe', + cn: '站近', + ko: '가까이 있기', + }, + beFar: { + en: 'Be Far', + de: 'Sei Fern', + cn: '站远', + ko: '멀리 있기', + }, + mechs: { + en: '${mech1} + ${mech2}', + }, + }, + }, + { + id: 'DMU P2 Path of Light Towers 2 Baits', + // Players that still have the first headmarker + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: false, + }, + condition: (data) => data.pathOfLightCounter === 2, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + suppressSeconds: 1, + infoText: (data, _matches, output) => { + // Ignoring stack players that didn't soak tower 1 + if (data.myPathOfLights.length !== 1 || data.myPathOfLights[0] === 'stack') + return; + + return output.bait!(); + }, + outputStrings: { + bait: { + en: 'Bait cone Left/Right or clone far', + }, + }, + }, + { + id: 'DMU P2 Future\'s End/Past\'s End Baits', + // There are four end casts + // 10s apart + // BAD2 and BAD3 are the castbar, damage doesn't go out until later + // TODO: Get Tower Locations + type: 'Ability', + netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, + delaySeconds: 1.2, // Time until headmarker damage + alertText: (_data, matches, output) => { + return matches.id === 'BAD2' ? output.future!() : output.past!(); + }, + outputStrings: { + future: { + en: 'Bait Ending opposite Towers', + }, + past: { + en: 'Bait Ending between Towers', + }, + }, + }, + { + id: 'DMU P2 Path of Light Towers 3', + // BADC All Things Ending (Future) + // BADD All Things Ending (Past) + // There should be two stacks, a cone and an aoe + type: 'StartsUsing', + netRegex: { id: ['BADC', 'BADD'], source: 'Kefka', capture: false }, + condition: (data) => data.pathOfLightCounter === 3, + suppressSeconds: 1, + alertText: (data, _matches, output) => { + // Tower soak group A will be at 3 + if (data.myPathOfLights.length === 3) { + const marker = data.myPathOfLights[2] ?? 'unknown'; + if (marker === 'stack') { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + return output.markerOnYouTower!({ + marker: output.stacksOnPlayers!({ players: msg }), + tower: output.tower!(), + }); + } + + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + return output.markerOnYouTower!({ + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + } + return output.markerOnYouTower!({ + marker: output[marker]!(), + tower: output.tower!(), + }); + } + + // No tower has been soaked + if (data.myPathOfLights.length === 1) { + if (data.role === 'healer' || data.role === 'tank') + return output.leftStack!(); + return output.rightStack!(); + } + }, + outputStrings: forsakenOutputStrings, + }, + { + id: 'DMU P2 Path of Light Towers 4', + // This set should not contain stack markers + // If stacks exist, they came from first set + // 2 Cones and 2 Spreads will soak towers + // + // Headmarkers come out ~2s before Future's/Past's End + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: false, + }, + condition: (data) => data.pathOfLightCounter === 4, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + suppressSeconds: 1, + infoText: (data, _matches, output) => { + // Handle second group's first towers + if (data.myPathOfLights.length === 1) { + const marker = data.myPathOfLights[0]; + // If someone has stack from beginning + if (marker === 'stack' || marker === 'unknown') + return; + + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + const tower = data.role === 'tank' || Util.isMeleeDpsJob(data.job) + ? 'rightTower' + : 'towerTower'; + if (marker === 'cone') + return output.mechs!({ + mech1: output[tower]!(), + mech2: output.beNear!(), + }); + if (marker === 'spread') + return output.mechs!({ + mech1: output[tower]!(), + mech2: output.beFar!(), + }); + } + if (marker === 'cone') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beNear!(), + }); + if (marker === 'spread') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beFar!(), + }); + } + return output.bait!(); + }, + outputStrings: { + tower: Outputs.getTowers, + leftTower: { + en: 'Left Tower', + }, + rightTower: { + en: 'Right Tower', + }, + beNear: { + en: 'Be Near', + de: 'Sei Nahe', + cn: '站近', + ko: '가까이 있기', + }, + beFar: { + en: 'Be Far', + de: 'Sei Fern', + cn: '站远', + ko: '멀리 있기', + }, + mechs: { + en: '${mech1} + ${mech2}', + }, + bait: { + en: 'Bait cone Left/Right or clone far', + }, + }, + }, + { + id: 'DMU P2 Path of Light Towers 5', + // BADC All Things Ending (Future) + // BADD All Things Ending (Past) + // There should be two stacks, a cone and an aoe + type: 'StartsUsing', + netRegex: { id: ['BADC', 'BADD'], source: 'Kefka', capture: false }, + condition: (data) => data.pathOfLightCounter === 5, + suppressSeconds: 1, + alertText: (data, _matches, output) => { + // Tower soak group B will be at 2 + if (data.myPathOfLights.length === 2) { + const marker = data.myPathOfLights[1] ?? 'unknown'; + if (marker === 'stack') { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + return output.markerOnYouTower!({ + marker: output.stacksOnPlayers!({ players: msg }), + tower: output.tower!(), + }); + } + + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + return output.markerOnYouTower!({ + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + } + return output.markerOnYouTower!({ + marker: output[marker]!(), + tower: output.tower!(), + }); + } + + // Players that have soaked 3 towers + if (data.myPathOfLights.length === 4) { + if (data.role === 'healer' || data.role === 'tank') + return output.leftStack!(); + return output.rightStack!(); + } + }, + outputStrings: forsakenOutputStrings, + }, + { + id: 'DMU P2 Path of Light Towers 6', + // This set should not contain stack markers + // If stacks exist, they came from first set + // 2 Cones and 2 Spreads will soak towers + // + // Headmarkers come out ~2s before Future's/Past's End + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: true, + }, + condition: (data, matches) => { + return data.me === matches.target && data.pathOfLightCounter === 6; + }, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + infoText: (data, matches, output) => { + // If a player from Group A accidentally soaks + if (data.myPathOfLights.length !== 3) + return; + const id = matches.id; + type markerMap = { + [key: string]: 'stack' | 'cone' | 'spread'; + }; + const markers: markerMap = { + '02CB': 'stack', + '02CD': 'cone', + '02CC': 'spread', + }; + const marker = markers[id]; + if (marker === undefined) + return; + + // Unsure that this could happen, unless more than 4 players soaked? + if (marker === 'stack') + return; + + if (marker === 'cone') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beNear!(), + }); + if (marker === 'spread') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beFar!(), + }); + }, + outputStrings: { + tower: Outputs.getTowers, + beNear: { + en: 'Be Near', + de: 'Sei Nahe', + cn: '站近', + ko: '가까이 있기', + }, + beFar: { + en: 'Be Far', + de: 'Sei Fern', + cn: '站远', + ko: '멀리 있기', + }, + mechs: { + en: '${mech1} + ${mech2}', + }, + }, + }, + { + id: 'DMU P2 Path of Light Towers 6 Baits', + // Players that still have the first headmarker + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: false, + }, + condition: (data) => data.pathOfLightCounter === 6, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + suppressSeconds: 1, + infoText: (data, _matches, output) => { + if (data.myPathOfLights.length !== 4) + return; + + return output.bait!(); + }, + outputStrings: { + bait: { + en: 'Bait cone Left/Right or clone far', + }, + }, + }, + { + id: 'DMU P2 Path of Light Towers 7', + // This set should not contain stack markers + // If stacks exist, they came from first set + // There should be two stacks, a cone and an aoe + // + // Headmarkers come out ~2s before Future's/Past's End + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: false, + }, + condition: (data) => data.pathOfLightCounter === 7, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + infoText: (data, _matches, output) => { + // Both groups will be on their last soak + // Group B will have two stacks + const marker = data.myPathOfLights[4] ?? 'unknown'; + if (marker === 'stack') { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return 'YOU'; + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + return output.markerOnYouTower!({ + marker: output.stacksOnPlayers!({ players: msg }), + tower: output.tower!(), + }); + } + return output.groupBTowers!(); + }, + outputStrings: forsakenOutputStrings, + }, + { + id: 'DMU P2 Path of Light Towers 8', + // This set should not contain stack markers + // If stacks exist, they came from first set + // 2 Cones and 2 Spreads will soak towers + // + // Headmarkers come out ~2s before Future's/Past's End + type: 'HeadMarker', + netRegex: { + id: [ + headMarkerData['stackPath'], + headMarkerData['conePath'], + headMarkerData['spreadPath'], + ], + capture: false, + }, + condition: (data) => data.pathOfLightCounter === 8, + delaySeconds: 0.1, // Delay for party headmarker collect + durationSeconds: 9, + infoText: (data, _matches, output) => { + // Handle first group's last towers + if (data.myPathOfLights.length === 4) { + const marker = data.myPathOfLights[3]; + + if (marker === 'stack' || marker === 'unknown') + return; + + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + const tower = data.role === 'tank' || Util.isMeleeDpsJob(data.job) + ? 'rightTower' + : 'towerTower'; + if (marker === 'cone') + return output.mechs!({ + mech1: output[tower]!(), + mech2: output.beNear!(), + }); + if (marker === 'spread') + return output.mechs!({ + mech1: output[tower]!(), + mech2: output.beFar!(), + }); + } + if (marker === 'cone') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beNear!(), + }); + if (marker === 'spread') + return output.mechs!({ + mech1: output.tower!(), + mech2: output.beFar!(), + }); + } + return output.bait!(); + }, + outputStrings: { + tower: Outputs.getTowers, + leftTower: { + en: 'Left Tower', + }, + rightTower: { + en: 'Right Tower', + }, + beNear: { + en: 'Be Near', + de: 'Sei Nahe', + cn: '站近', + ko: '가까이 있기', + }, + beFar: { + en: 'Be Far', + de: 'Sei Fern', + cn: '站远', + ko: '멀리 있기', + }, + mechs: { + en: '${mech1} + ${mech2}', + }, + bait: { + en: 'Bait cone Left/Right or clone far', + }, + }, + }, + { + id: 'DMU P2 Light of Judgment', + type: 'StartsUsing', + netRegex: { id: 'BABD', source: 'Kefka', capture: false }, + response: Responses.bigAoe('alert'), + }, + { + id: 'DMU P2 Single Wing of Destruction', + // BACD Wings of Destruction, Left wing highlight + // BACE Wingso of Desctruction, Right wing highlight + // Halfroom cleaves + type: 'StartsUsing', + netRegex: { id: ['BACD', 'BACE'], source: 'Kefka', capture: true }, + infoText: (_data, matches, output) => { + if (matches.id === 'BACD') + return output.right!(); + return output.left!(); + }, + outputStrings: { + right: Outputs.right, + left: Outputs.left, + }, + }, + { + id: 'DMU P2 Wings of Destruction', + type: 'StartsUsing', + netRegex: { id: 'C487', source: 'Kefka', capture: false }, + response: (data, _matches, output) => { + // cactbot-builtin-response + output.responseOutputStrings = { + maxMeleeAvoidTanks: { + en: 'Max Melee: Avoid Tanks', + de: 'Max Nahkampf: Weg von den Tanks', + fr: 'Max mêlée : éloignez-vous des tanks', + ja: '近接最大レンジ タンクから離れる', + cn: '最大近战距离,避开坦克', + ko: '칼끝딜: 탱커 피하기', + tc: '最大近戰距離,避開坦克', + }, + wingsBeNearFar: { + en: 'Wings: Be Near/Far', + de: 'Schwingen: Nah/Fern', + fr: 'Ailes : Placez-vous près/loin', + ja: '翼: めり込む/離れる', + cn: '双翅膀:近或远', + ko: '양날개: 가까이/멀리', + tc: '雙翅膀:近或遠', + }, + }; + if (data.role === 'tank') + return { alertText: output.wingsBeNearFar!() }; + return { infoText: output.maxMeleeAvoidTanks!() }; + }, + }, + { + id: 'DMU P2 Aero III Assault', + // Knockback from boss that can't be resisted + // Applies 306 Down for the Count + type: 'StartsUsing', + netRegex: { id: 'C3F7', source: 'Kefka', capture: false }, + response: Responses.getUnder('alert'), + }, ], timelineReplace: [ { From 921b52cea4cee900549a18004a91470e263244fb Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 02:57:39 -0400 Subject: [PATCH 02/49] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 7 ++++--- 1 file changed, 4 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 450521c0c9d..f80ce163040 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -94,7 +94,8 @@ const triggerSet: TriggerSet = { type: 'select', options: { en: { - 'Group soak order: AAABBBBA. Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.': 'kroxy-rinon', + 'Group soak order: AAABBBBA. Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.': + 'kroxy-rinon', 'Generic calls.': 'none', }, }, @@ -369,7 +370,7 @@ const triggerSet: TriggerSet = { if (data.myPathOfLights.length !== 1 || data.myPathOfLights[0] === 'stack') return; - return output.bait!(); + return output.bait!(); }, outputStrings: { bait: { @@ -678,7 +679,7 @@ const triggerSet: TriggerSet = { if (data.myPathOfLights.length !== 4) return; - return output.bait!(); + return output.bait!(); }, outputStrings: { bait: { From 8dce14579b3d172bacf732bf79c9cb042fcaef94 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 02:58:10 -0400 Subject: [PATCH 03/49] missed 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 f80ce163040..4156289ce75 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -71,7 +71,7 @@ const forsakenOutputStrings: OutputStrings = { en: 'Left Stack/Cone', }, rightStack: { - en: 'Right Stack' + en: 'Right Stack', }, groupBTowers: { en: 'Group B Towers', From 8b55b6068a33b10e28c43b9af5d906cf4352bede Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 04:10:40 -0400 Subject: [PATCH 04/49] Update ui/raidboss/data/07-dt/ultimate/dancing_mad.ts Co-authored-by: Dowon --- 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 4156289ce75..5708f63475f 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -482,7 +482,7 @@ const triggerSet: TriggerSet = { if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { const tower = data.role === 'tank' || Util.isMeleeDpsJob(data.job) ? 'rightTower' - : 'towerTower'; + : 'lefttower'; if (marker === 'cone') return output.mechs!({ mech1: output[tower]!(), From 00193d52982eb25eb94c6d0027ab4aa6b2402a75 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 18:04:00 -0400 Subject: [PATCH 05/49] replace literal YOU with output. --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 9 ++++++--- 1 file changed, 6 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 5708f63475f..592430ac99d 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -38,6 +38,9 @@ const headMarkerData = { } as const; const forsakenOutputStrings: OutputStrings = { + you: { + en: 'YOU', + }, tower: Outputs.getTowers, leftTower: { en: 'Left Tower', @@ -417,7 +420,7 @@ const triggerSet: TriggerSet = { const players = data.pathOfLightStackPlayers.map( (player) => { if (player === data.me) - return 'YOU'; + return output.you!(); return data.party.member(player); }, ); @@ -553,7 +556,7 @@ const triggerSet: TriggerSet = { const players = data.pathOfLightStackPlayers.map( (player) => { if (player === data.me) - return 'YOU'; + return output.you!(); return data.party.member(player); }, ); @@ -715,7 +718,7 @@ const triggerSet: TriggerSet = { const players = data.pathOfLightStackPlayers.map( (player) => { if (player === data.me) - return 'YOU'; + return output.you!(); return data.party.member(player); }, ); From 0206d36dd709dc2adb40df5d24b5dfd0697da8a6 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 21:27:13 -0400 Subject: [PATCH 06/49] check roles with towers 2 --- .../data/07-dt/ultimate/dancing_mad.ts | 63 +++++++++++++------ 1 file changed, 45 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 592430ac99d..17a2f191534 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -172,9 +172,9 @@ const triggerSet: TriggerSet = { data.myPathOfLights.push(markers[id] ?? 'unknown'); // Clear previous Headmarker if set - data.pathOfLightStackPlayers.filter((t) => t !== target); - data.pathOfLightConePlayers.filter((t) => t !== target); - data.pathOfLightSpreadPlayers.filter((t) => t !== target); + data.pathOfLightStackPlayers = data.pathOfLightStackPlayers.filter((t) => t !== target); + data.pathOfLightConePlayers = data.pathOfLightConePlayers.filter((t) => t !== target); + data.pathOfLightSpreadPlayers = data.pathOfLightSpreadPlayers.filter((t) => t !== target); if (id === headMarkerData['stackPath']) data.pathOfLightStackPlayers.push(target); @@ -313,22 +313,49 @@ const triggerSet: TriggerSet = { if (marker === undefined) return; - // Unsure that this could happen, unless more than 4 players soaked? - if (marker === 'stack') + // Stack shouldn't be possible here + if (marker === 'stack' || data.myPathOfLights[1] === undefined) return; - // Ignoring stack players that didn't soak tower 1 - // Check our previous headmarker - if (data.myPathOfLights[0] === 'cone') - return output.mechs!({ - mech1: output.tower!(), - mech2: output.beFar!(), - }); - if (data.myPathOfLights[0] === 'spread') - return output.mechs!({ - mech1: output.swapTowers!(), - mech2: output.beFar!(), - }); + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = data.myPathOfLights[1] === 'spread' + ? output.beFar!() + : output.beNear!(); + + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + // Check our previous headmarker + // Supports Left, DPS Right + if (data.role === 'healer' || data.role === 'tank') { + // Support had cone in left tower 1, moves up in tower + if (data.myPathOfLights[0] === 'cone') + return output.mechs!({ + mech1: output.tower!(), + mech2: nearFar, + }); + // Support with spread on right tower 1 changes to left + if (data.myPathOfLights[0] === 'spread') + return output.mechs!({ + mech1: output.swapTowers!(), + mech2: nearFar, + }); + } + if (data.myPathOfLights[0] === 'cone') + return output.mechs!({ + mech1: output.swapTowers!(), + mech2: nearFar, + }); + if (data.myPathOfLights[0] === 'spread') + return output.mechs!({ + mech1: output.getTowers!(), + mech2: nearFar, + }); + } + + // No strategy just say the cone/spread difference + return output.mechs!({ + mech1: output.getTowers!(), + mech2: nearFar, + }); }, outputStrings: { tower: Outputs.getTowers, @@ -762,7 +789,7 @@ const triggerSet: TriggerSet = { if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { const tower = data.role === 'tank' || Util.isMeleeDpsJob(data.job) ? 'rightTower' - : 'towerTower'; + : 'leftTower'; if (marker === 'cone') return output.mechs!({ mech1: output[tower]!(), From e0d4bf39ac4ca1b2d33f8ab4565adb9b51056a79 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 21:31:23 -0400 Subject: [PATCH 07/49] fix wrong output name --- 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 17a2f191534..9f9b5a426df 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -346,7 +346,7 @@ const triggerSet: TriggerSet = { }); if (data.myPathOfLights[0] === 'spread') return output.mechs!({ - mech1: output.getTowers!(), + mech1: output.tower!(), mech2: nearFar, }); } From accd39d2f37f043c1a60eed70f9fcebda13e9e5e Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 21:39:01 -0400 Subject: [PATCH 08/49] missed the second output --- 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 9f9b5a426df..e3e80ff8c7f 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -353,7 +353,7 @@ const triggerSet: TriggerSet = { // No strategy just say the cone/spread difference return output.mechs!({ - mech1: output.getTowers!(), + mech1: output.tower!(), mech2: nearFar, }); }, From a39fcb22ccd1914e2613f788e7f26b5f0276329b Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 22:10:10 -0400 Subject: [PATCH 09/49] rename/clarify trigger, expand forsakenOutputStrings --- .../data/07-dt/ultimate/dancing_mad.ts | 158 ++++-------------- 1 file changed, 29 insertions(+), 129 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index e3e80ff8c7f..5e99f1b9ded 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -357,27 +357,7 @@ const triggerSet: TriggerSet = { mech2: nearFar, }); }, - outputStrings: { - tower: Outputs.getTowers, - swapTowers: { - en: 'Swap Towers', - }, - beNear: { - en: 'Be Near', - de: 'Sei Nahe', - cn: '站近', - ko: '가까이 있기', - }, - beFar: { - en: 'Be Far', - de: 'Sei Fern', - cn: '站远', - ko: '멀리 있기', - }, - mechs: { - en: '${mech1} + ${mech2}', - }, - }, + outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Path of Light Towers 2 Baits', @@ -402,21 +382,19 @@ const triggerSet: TriggerSet = { return output.bait!(); }, - outputStrings: { - bait: { - en: 'Bait cone Left/Right or clone far', - }, - }, + outputStrings: forsakenOutputStrings, }, { - id: 'DMU P2 Future\'s End/Past\'s End Baits', - // There are four end casts - // 10s apart + id: 'DMU P2 All Things Ending Baits', + // Using the following spells for timing: + // BAD2 Future's End => Need to bait BACD All Things Ending + // BAD3 Past's End => Need to bait BADD All Things Ending + // There are four end casts, each 10s apart // BAD2 and BAD3 are the castbar, damage doesn't go out until later // TODO: Get Tower Locations type: 'Ability', netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, - delaySeconds: 1.2, // Time until headmarker damage + delaySeconds: 1.2, // Time until headmarker and future/past damage alertText: (_data, matches, output) => { return matches.id === 'BAD2' ? output.future!() : output.past!(); }, @@ -509,61 +487,30 @@ const triggerSet: TriggerSet = { if (marker === 'stack' || marker === 'unknown') return; + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = data.myPathOfLights[1] === 'spread' + ? output.beFar!() + : output.beNear!(); + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - const tower = data.role === 'tank' || Util.isMeleeDpsJob(data.job) - ? 'rightTower' - : 'lefttower'; - if (marker === 'cone') - return output.mechs!({ - mech1: output[tower]!(), - mech2: output.beNear!(), - }); - if (marker === 'spread') - return output.mechs!({ - mech1: output[tower]!(), - mech2: output.beFar!(), - }); - } - if (marker === 'cone') return output.mechs!({ - mech1: output.tower!(), - mech2: output.beNear!(), - }); - if (marker === 'spread') - return output.mechs!({ - mech1: output.tower!(), - mech2: output.beFar!(), + mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) + ? output.rightTower!() + : output.leftTower!(), + mech2: nearFar, }); + } + + return output.mechs!({ + mech1: output.tower!(), + mech2: nearFar, + }); } + + // Group A return output.bait!(); }, - outputStrings: { - tower: Outputs.getTowers, - leftTower: { - en: 'Left Tower', - }, - rightTower: { - en: 'Right Tower', - }, - beNear: { - en: 'Be Near', - de: 'Sei Nahe', - cn: '站近', - ko: '가까이 있기', - }, - beFar: { - en: 'Be Far', - de: 'Sei Fern', - cn: '站远', - ko: '멀리 있기', - }, - mechs: { - en: '${mech1} + ${mech2}', - }, - bait: { - en: 'Bait cone Left/Right or clone far', - }, - }, + outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Path of Light Towers 5', @@ -670,24 +617,7 @@ const triggerSet: TriggerSet = { mech2: output.beFar!(), }); }, - outputStrings: { - tower: Outputs.getTowers, - beNear: { - en: 'Be Near', - de: 'Sei Nahe', - cn: '站近', - ko: '가까이 있기', - }, - beFar: { - en: 'Be Far', - de: 'Sei Fern', - cn: '站远', - ko: '멀리 있기', - }, - mechs: { - en: '${mech1} + ${mech2}', - }, - }, + outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Path of Light Towers 6 Baits', @@ -711,11 +641,7 @@ const triggerSet: TriggerSet = { return output.bait!(); }, - outputStrings: { - bait: { - en: 'Bait cone Left/Right or clone far', - }, - }, + outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Path of Light Towers 7', @@ -814,33 +740,7 @@ const triggerSet: TriggerSet = { } return output.bait!(); }, - outputStrings: { - tower: Outputs.getTowers, - leftTower: { - en: 'Left Tower', - }, - rightTower: { - en: 'Right Tower', - }, - beNear: { - en: 'Be Near', - de: 'Sei Nahe', - cn: '站近', - ko: '가까이 있기', - }, - beFar: { - en: 'Be Far', - de: 'Sei Fern', - cn: '站远', - ko: '멀리 있기', - }, - mechs: { - en: '${mech1} + ${mech2}', - }, - bait: { - en: 'Bait cone Left/Right or clone far', - }, - }, + outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Light of Judgment', From f3398595066f3654e41cbcb79cbd385770a7de5d Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 22:12:10 -0400 Subject: [PATCH 10/49] missed copy of outputStrings --- .../data/07-dt/ultimate/dancing_mad.ts | 29 ++++++++++++++++--- 1 file changed, 25 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 5e99f1b9ded..3fe95777698 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -38,17 +38,23 @@ const headMarkerData = { } as const; const forsakenOutputStrings: OutputStrings = { + tower: Outputs.getTowers, + stackOnYou: Outputs.stackOnYou, you: { en: 'YOU', }, - tower: Outputs.getTowers, + swapTowers: { + en: 'Swap Towers', + }, leftTower: { en: 'Left Tower', }, rightTower: { en: 'Right Tower', }, - stackOnYou: Outputs.stackOnYou, + groupBTowers: { + en: 'Group B Towers', + }, cone: { en: 'Cone on YOU', }, @@ -76,8 +82,23 @@ const forsakenOutputStrings: OutputStrings = { rightStack: { en: 'Right Stack', }, - groupBTowers: { - en: 'Group B Towers', + beNear: { + en: 'Be Near', + de: 'Sei Nahe', + cn: '站近', + ko: '가까이 있기', + }, + beFar: { + en: 'Be Far', + de: 'Sei Fern', + cn: '站远', + ko: '멀리 있기', + }, + mechs: { + en: '${mech1} + ${mech2}', + }, + bait: { + en: 'Bait cone Left/Right or clone far', }, }; From 054e644641b1f16c158fed156990015a65aec11f Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 22:44:56 -0400 Subject: [PATCH 11/49] tower fixes roles missing in tower 6, remove groupbtowers (this was just me trying to commit something that had output before stopping for the day), refactor the rest of the towers with even/odd code. Some of the baits still needed separate triggers due to overlaps within current checks between groups. Also to note, this would not work all the way if it is not AAABBBBA. --- .../data/07-dt/ultimate/dancing_mad.ts | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 3fe95777698..082f31d68ac 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -52,9 +52,6 @@ const forsakenOutputStrings: OutputStrings = { rightTower: { en: 'Right Tower', }, - groupBTowers: { - en: 'Group B Towers', - }, cone: { en: 'Cone on YOU', }, @@ -627,16 +624,24 @@ const triggerSet: TriggerSet = { if (marker === 'stack') return; - if (marker === 'cone') - return output.mechs!({ - mech1: output.tower!(), - mech2: output.beNear!(), - }); - if (marker === 'spread') + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = data.myPathOfLights[2] === 'spread' + ? output.beFar!() + : output.beNear!(); + + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.mechs!({ - mech1: output.tower!(), - mech2: output.beFar!(), + mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) + ? output.rightTower!() + : output.leftTower!(), + mech2: nearFar, }); + } + + return output.mechs!({ + mech1: output.tower!(), + mech2: nearFar, + }); }, outputStrings: forsakenOutputStrings, }, @@ -702,7 +707,18 @@ const triggerSet: TriggerSet = { tower: output.tower!(), }); } - return output.groupBTowers!(); + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + return output.markerOnYouTower!({ + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + } + return output.markerOnYouTower!({ + marker: output[marker]!(), + tower: output.tower!(), + }); }, outputStrings: forsakenOutputStrings, }, @@ -733,32 +749,27 @@ const triggerSet: TriggerSet = { if (marker === 'stack' || marker === 'unknown') return; + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = data.myPathOfLights[3] === 'spread' + ? output.beFar!() + : output.beNear!(); + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - const tower = data.role === 'tank' || Util.isMeleeDpsJob(data.job) - ? 'rightTower' - : 'leftTower'; - if (marker === 'cone') - return output.mechs!({ - mech1: output[tower]!(), - mech2: output.beNear!(), - }); - if (marker === 'spread') - return output.mechs!({ - mech1: output[tower]!(), - mech2: output.beFar!(), - }); - } - if (marker === 'cone') - return output.mechs!({ - mech1: output.tower!(), - mech2: output.beNear!(), - }); - if (marker === 'spread') return output.mechs!({ - mech1: output.tower!(), - mech2: output.beFar!(), + mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) + ? output.rightTower!() + : output.leftTower!(), + mech2: nearFar, }); + } + + return output.mechs!({ + mech1: output.tower!(), + mech2: nearFar, + }); } + + // Group A return output.bait!(); }, outputStrings: forsakenOutputStrings, From a9b4a46d83379e2e046e81c12c0645f99f752d44 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 8 Jun 2026 22:47:47 -0400 Subject: [PATCH 12/49] rename triggers that call towers and/or baits --- 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 082f31d68ac..6191acd6f8a 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -478,7 +478,7 @@ const triggerSet: TriggerSet = { outputStrings: forsakenOutputStrings, }, { - id: 'DMU P2 Path of Light Towers 4', + id: 'DMU P2 Path of Light Towers and Baits 4', // This set should not contain stack markers // If stacks exist, they came from first set // 2 Cones and 2 Spreads will soak towers @@ -723,7 +723,7 @@ const triggerSet: TriggerSet = { outputStrings: forsakenOutputStrings, }, { - id: 'DMU P2 Path of Light Towers 8', + id: 'DMU P2 Path of Light Towers and Baits 8', // This set should not contain stack markers // If stacks exist, they came from first set // 2 Cones and 2 Spreads will soak towers @@ -769,7 +769,7 @@ const triggerSet: TriggerSet = { }); } - // Group A + // Group B return output.bait!(); }, outputStrings: forsakenOutputStrings, From 5aff79e7d9c79c09605aa7adf3a80b5829b6cb94 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 9 Jun 2026 00:25:07 -0400 Subject: [PATCH 13/49] Split leftStack call between roles. At least for standard comps, assuming tanks are not ever baiting cones and having melee downtime... This just makes it so you would not have to alter the output. --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 15 ++++++++++++--- 1 file changed, 12 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 6191acd6f8a..ba72af5b03f 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -74,7 +74,10 @@ const forsakenOutputStrings: OutputStrings = { en: '${marker} + ${tower}', }, leftStack: { - en: 'Left Stack/Cone', + en: 'Left Stack', + }, + baitLeftCone: { + en: 'Bait Left Cone', }, rightStack: { en: 'Right Stack', @@ -470,8 +473,11 @@ const triggerSet: TriggerSet = { // No tower has been soaked if (data.myPathOfLights.length === 1) { - if (data.role === 'healer' || data.role === 'tank') + // So long as it is standard party composition... + if (data.role === 'tank') return output.leftStack!(); + if (data.role === 'healer') + return output.baitLeftCone!(); return output.rightStack!(); } }, @@ -575,8 +581,11 @@ const triggerSet: TriggerSet = { // Players that have soaked 3 towers if (data.myPathOfLights.length === 4) { - if (data.role === 'healer' || data.role === 'tank') + // So long as it is standard party composition... + if (data.role === 'tank') return output.leftStack!(); + if (data.role === 'healer') + return output.baitLeftCone!(); return output.rightStack!(); } }, From c990de40b65b0364b797a910017404b2149f3f69 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 9 Jun 2026 04:14:27 -0400 Subject: [PATCH 14/49] refactoring of forsaken + ending after forsaken --- .../data/07-dt/ultimate/dancing_mad.ts | 560 ++++++++++-------- 1 file changed, 314 insertions(+), 246 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index ba72af5b03f..4a65c4bcb20 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -27,6 +27,7 @@ export interface Data extends RaidbossData { pathOfLightConePlayers: string[]; pathOfLightSpreadPlayers: string[]; myPathOfLights: string[]; + isForsakenGroupA: boolean; } const headMarkerData = { @@ -37,20 +38,39 @@ const headMarkerData = { 'spreadPath': '02CC', // When standing in Path of Light tower, causes BAC1 Spellscatter (small aoe on the player) } as const; + const forsakenOutputStrings: OutputStrings = { tower: Outputs.getTowers, + avoid: { + en: 'Avoid towers', + de: 'Türme vermeiden', + fr: 'Évitez les tours', + ja: '塔回避', + cn: '远离塔', + ko: '기둥 피하기', + tc: '遠離塔', + }, stackOnYou: Outputs.stackOnYou, + num: { + en: '${num}: ', + de: '${num}: ', + fr: '${num}: ', + ja: '${num}: ', + cn: '${num}: ', + ko: '${num}: ', + tc: '${num}: ', + }, you: { en: 'YOU', }, swapTowers: { - en: 'Swap Towers', + en: '${num}Swap Towers', }, leftTower: { - en: 'Left Tower', + en: '${num}Left Tower', }, rightTower: { - en: 'Right Tower', + en: '${num}Right Tower', }, cone: { en: 'Cone on YOU', @@ -68,19 +88,22 @@ const forsakenOutputStrings: OutputStrings = { en: 'Stacks on ${players}', }, markerOnYouStacksOnPlayers: { - en: '${marker} + ${stacks}', + en: '${num}${marker} + ${stacks}', }, markerOnYouTower: { - en: '${marker} + ${tower}', + en: '${num}${marker} + ${tower}', }, - leftStack: { - en: 'Left Stack', + baitLeftConeOutOdds: { + en: '${num}Bait Left Cone Out', + }, + baitLeftConeEvens: { + en: '${num}Bait Left Cone Left', }, - baitLeftCone: { - en: 'Bait Left Cone', + leftStack: { + en: '${num}Left Stack + ${avoid}', }, rightStack: { - en: 'Right Stack', + en: '${num}Right Stack + ${avoid}', }, beNear: { en: 'Be Near', @@ -95,10 +118,13 @@ const forsakenOutputStrings: OutputStrings = { ko: '멀리 있기', }, mechs: { - en: '${mech1} + ${mech2}', + en: '${num}${mech1} + ${mech2}', }, bait: { - en: 'Bait cone Left/Right or clone far', + en: '${num}Bait Cone Right or Clone Far', + }, + baitCloneFar: { + en: '${num}Bait Clone Far', }, }; @@ -136,6 +162,7 @@ const triggerSet: TriggerSet = { pathOfLightStackPlayers: [], pathOfLightConePlayers: [], pathOfLightSpreadPlayers: [], + isForsakenGroupA: false, }; }, triggers: [ @@ -197,6 +224,10 @@ const triggerSet: TriggerSet = { data.pathOfLightConePlayers = data.pathOfLightConePlayers.filter((t) => t !== target); data.pathOfLightSpreadPlayers = data.pathOfLightSpreadPlayers.filter((t) => t !== target); + // To track "groups" + if (data.pathOfLightCounter === 2 && data.me === matches.target) + data.isForsakenGroupA = true; + if (id === headMarkerData['stackPath']) data.pathOfLightStackPlayers.push(target); else if (id === headMarkerData['conePath']) @@ -241,20 +272,24 @@ const triggerSet: TriggerSet = { const marker = markers[id]; if (marker === undefined) return; + const num = data.pathOfLightCounter; if (marker === 'stack') { if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { if (data.role === 'healer' || data.role === 'tank') return output.stackOnYouTower!({ + num: output.num!({ num: num }), tower: output.leftTower!(), marker: output.stackOnYou!(), }); return output.stackOnYouTower!({ + num: output.num!({ num: num }), tower: output.rightTower!(), marker: output.stackOnYou!(), }); } return output.stackOnYouTower!({ + num: output.num!({ num: num }), tower: output.tower!(), marker: output.stackOnYou!(), }); @@ -275,6 +310,7 @@ const triggerSet: TriggerSet = { ); const msg = players?.join(', '); return output.markerOnYouStacksOnPlayers!({ + num: output.num!({ num: num }), marker: output[marker]!(), stacks: output.stacksOnPlayers!({ players: msg }), }); @@ -283,6 +319,7 @@ const triggerSet: TriggerSet = { // Our partner will be the role that matches us const possiblePartner = data.party.member(myRoleIsDPS === stack1IsDPS ? stack1 : stack2); return output.markerOnYouStacksOnPlayers!({ + num: output.num!({ num: num }), marker: output[marker]!(), stacks: output.stackOnPlayer!({ player: possiblePartner }), }); @@ -313,98 +350,80 @@ const triggerSet: TriggerSet = { headMarkerData['conePath'], headMarkerData['spreadPath'], ], - capture: true, - }, - condition: (data, matches) => { - return data.me === matches.target && data.pathOfLightCounter === 2; + capture: false, }, + condition: (data) => data.pathOfLightCounter === 2, delaySeconds: 0.1, // Delay for party headmarker collect durationSeconds: 9, - infoText: (data, matches, output) => { - const id = matches.id; - type markerMap = { - [key: string]: 'stack' | 'cone' | 'spread'; - }; - const markers: markerMap = { - '02CB': 'stack', - '02CD': 'cone', - '02CC': 'spread', - }; - const marker = markers[id]; - if (marker === undefined) - return; - - // Stack shouldn't be possible here - if (marker === 'stack' || data.myPathOfLights[1] === undefined) - return; + suppressSeconds: 1, + infoText: (data, _matches, output) => { + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker - // Spread Players have to be far in the tower, cones need to bait end - const nearFar = data.myPathOfLights[1] === 'spread' - ? output.beFar!() - : output.beNear!(); + // Group A + if (data.isForsakenGroupA) { + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = marker === 'spread' + ? output.beFar!() + : output.beNear!(); - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - // Check our previous headmarker - // Supports Left, DPS Right - if (data.role === 'healer' || data.role === 'tank') { - // Support had cone in left tower 1, moves up in tower + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + // Check our previous headmarker + // Supports Left, DPS Right + if (data.role === 'healer' || data.role === 'tank') { + // Support had cone in left tower 1, moves up in tower + if (data.myPathOfLights[0] === 'cone') + return output.mechs!({ + num: output.num!({ num: num }), + mech1: output.tower!(), + mech2: nearFar, + }); + // Support with spread on right tower 1 changes to left + if (data.myPathOfLights[0] === 'spread') + return output.mechs!({ + num: output.num!({ num: num }), + mech1: output.swapTowers!(), + mech2: nearFar, + }); + } if (data.myPathOfLights[0] === 'cone') return output.mechs!({ - mech1: output.tower!(), + num: output.num!({ num: num }), + mech1: output.swapTowers!(), mech2: nearFar, }); - // Support with spread on right tower 1 changes to left if (data.myPathOfLights[0] === 'spread') return output.mechs!({ - mech1: output.swapTowers!(), + num: output.num!({ num: num }), + mech1: output.tower!(), mech2: nearFar, }); } - if (data.myPathOfLights[0] === 'cone') - return output.mechs!({ - mech1: output.swapTowers!(), - mech2: nearFar, - }); - if (data.myPathOfLights[0] === 'spread') - return output.mechs!({ - mech1: output.tower!(), - mech2: nearFar, - }); + + // No strategy just say the cone/spread difference + return output.mechs!({ + num: output.num!({ num: num }), + mech1: output.tower!(), + mech2: nearFar, + }); } - // No strategy just say the cone/spread difference - return output.mechs!({ - mech1: output.tower!(), - mech2: nearFar, + // Group B + if (data.role === 'healer') + return output.baitLeftConeEvens!({ + num: output.num!({ num: num }), + }); + if (data.role === 'tank') + return output.baitCloneFar!({ + num: output.num!({ num: num }), + }); + // DPS Unkmown party composition + return output.bait!({ + num: output.num!({ num: num }), }); }, outputStrings: forsakenOutputStrings, }, - { - id: 'DMU P2 Path of Light Towers 2 Baits', - // Players that still have the first headmarker - type: 'HeadMarker', - netRegex: { - id: [ - headMarkerData['stackPath'], - headMarkerData['conePath'], - headMarkerData['spreadPath'], - ], - capture: false, - }, - condition: (data) => data.pathOfLightCounter === 2, - delaySeconds: 0.1, // Delay for party headmarker collect - durationSeconds: 9, - suppressSeconds: 1, - infoText: (data, _matches, output) => { - // Ignoring stack players that didn't soak tower 1 - if (data.myPathOfLights.length !== 1 || data.myPathOfLights[0] === 'stack') - return; - - return output.bait!(); - }, - outputStrings: forsakenOutputStrings, - }, { id: 'DMU P2 All Things Ending Baits', // Using the following spells for timing: @@ -416,16 +435,37 @@ const triggerSet: TriggerSet = { type: 'Ability', netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, delaySeconds: 1.2, // Time until headmarker and future/past damage - alertText: (_data, matches, output) => { - return matches.id === 'BAD2' ? output.future!() : output.past!(); + alertText: (data, matches, output) => { + const isFuture = matches.id === 'BAD2'; + if (data.pathOfLightCounter !== 9) + return isFuture ? output.future!() : output.past!(); + + return isFuture + ? output.lastFuture!({ action: output.behind!() }) + : output.lastPast!({ action: output.stay!() }); }, outputStrings: { + behind: Outputs.getBehind, + stay: { + en: 'Stay', + de: 'Bleib stehen', + fr: 'Restez', + cn: '停', + ko: '대기', + tc: '停', + }, future: { en: 'Bait Ending opposite Towers', }, past: { en: 'Bait Ending between Towers', }, + lastFuture: { + en: 'Bait Ending => ${action}', + }, + lastPast: { + en: 'Bait Ending => ${action}', + }, }, }, { @@ -438,9 +478,11 @@ const triggerSet: TriggerSet = { condition: (data) => data.pathOfLightCounter === 3, suppressSeconds: 1, alertText: (data, _matches, output) => { - // Tower soak group A will be at 3 - if (data.myPathOfLights.length === 3) { - const marker = data.myPathOfLights[2] ?? 'unknown'; + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + + // Group A + if (data.isForsakenGroupA) { if (marker === 'stack') { // Need to know for priority const players = data.pathOfLightStackPlayers.map( @@ -452,6 +494,7 @@ const triggerSet: TriggerSet = { ); const msg = players?.join(', '); return output.markerOnYouTower!({ + num: output.num!({ num: num }), marker: output.stacksOnPlayers!({ players: msg }), tower: output.tower!(), }); @@ -459,6 +502,7 @@ const triggerSet: TriggerSet = { if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.markerOnYouTower!({ + num: output.num!({ num: num }), marker: output[marker]!(), tower: marker === 'cone' ? output.leftTower!() @@ -466,25 +510,33 @@ const triggerSet: TriggerSet = { }); } return output.markerOnYouTower!({ + num: output.num!({ num: num }), marker: output[marker]!(), tower: output.tower!(), }); } - // No tower has been soaked - if (data.myPathOfLights.length === 1) { - // So long as it is standard party composition... - if (data.role === 'tank') - return output.leftStack!(); - if (data.role === 'healer') - return output.baitLeftCone!(); - return output.rightStack!(); - } + // Group B + // So long as it is standard party composition... + if (data.role === 'tank') + return output.leftStack!({ + num: output.num!({ num: num }), + avoid: output.avoid!(), + }); + if (data.role === 'healer') + return output.baitLeftConeOutOdds!({ + num: output.num!({ num: num }), + }); + // 2 DPS in stack + return output.rightStack!({ + num: output.num!({ num: num }), + avoid: output.avoid!(), + }); }, outputStrings: forsakenOutputStrings, }, { - id: 'DMU P2 Path of Light Towers and Baits 4', + id: 'DMU P2 Path of Light Towers 4', // This set should not contain stack markers // If stacks exist, they came from first set // 2 Cones and 2 Spreads will soak towers @@ -504,35 +556,49 @@ const triggerSet: TriggerSet = { durationSeconds: 9, suppressSeconds: 1, infoText: (data, _matches, output) => { - // Handle second group's first towers - if (data.myPathOfLights.length === 1) { - const marker = data.myPathOfLights[0]; - // If someone has stack from beginning - if (marker === 'stack' || marker === 'unknown') - return; - - // Spread Players have to be far in the tower, cones need to bait end - const nearFar = data.myPathOfLights[1] === 'spread' - ? output.beFar!() - : output.beNear!(); + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.mechs!({ - mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) - ? output.rightTower!() - : output.leftTower!(), - mech2: nearFar, + // Group A + if (data.isForsakenGroupA) { + if (data.role === 'healer') + return output.baitLeftConeEvens!({ + num: output.num!({ num: num }), }); - } + if (data.role === 'tank') + return output.baitCloneFar!({ + num: output.num!({ num: num }), + }); + // DPS Unkmown party composition + return output.bait!({ + num: output.num!({ num: num }), + }); + } + + // Group B + // If someone has stack from beginning + if (marker === 'stack' || marker === 'unknown') + return; + + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = marker === 'spread' + ? output.beFar!() + : output.beNear!(); + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.mechs!({ - mech1: output.tower!(), + mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) + ? output.rightTower!() + : output.leftTower!(), mech2: nearFar, }); } - // Group A - return output.bait!(); + return output.mechs!({ + num: output.num!({ num: num }), + mech1: output.tower!(), + mech2: nearFar, + }); }, outputStrings: forsakenOutputStrings, }, @@ -546,48 +612,59 @@ const triggerSet: TriggerSet = { condition: (data) => data.pathOfLightCounter === 5, suppressSeconds: 1, alertText: (data, _matches, output) => { - // Tower soak group B will be at 2 - if (data.myPathOfLights.length === 2) { - const marker = data.myPathOfLights[1] ?? 'unknown'; - if (marker === 'stack') { - // Need to know for priority - const players = data.pathOfLightStackPlayers.map( - (player) => { - if (player === data.me) - return output.you!(); - return data.party.member(player); - }, - ); - const msg = players?.join(', '); - return output.markerOnYouTower!({ - marker: output.stacksOnPlayers!({ players: msg }), - tower: output.tower!(), - }); - } + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.markerOnYouTower!({ - marker: output[marker]!(), - tower: marker === 'cone' - ? output.leftTower!() - : output.rightTower!(), + // Group A + if (data.isForsakenGroupA) { + // So long as it is standard party composition... + if (data.role === 'tank') + return output.leftStack!({ + num: output.num!({ num: num }), + avoid: output.avoid!(), }); - } + if (data.role === 'healer') + return output.baitLeftConeOutOdds!({ + num: output.num!({ num: num }), + }); + return output.rightStack!({ + num: output.num!({ num: num }), + avoid: output.avoid!(), + }); + } + + // Group B + if (marker === 'stack') { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); return output.markerOnYouTower!({ - marker: output[marker]!(), + num: output.num!({ num: num }), + marker: output.stacksOnPlayers!({ players: msg }), tower: output.tower!(), }); } - // Players that have soaked 3 towers - if (data.myPathOfLights.length === 4) { - // So long as it is standard party composition... - if (data.role === 'tank') - return output.leftStack!(); - if (data.role === 'healer') - return output.baitLeftCone!(); - return output.rightStack!(); + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + return output.markerOnYouTower!({ + num: output.num!({ num: num }), + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); } + return output.markerOnYouTower!({ + num: output.num!({ num: num }), + marker: output[marker]!(), + tower: output.tower!(), + }); }, outputStrings: forsakenOutputStrings, }, @@ -605,41 +682,41 @@ const triggerSet: TriggerSet = { headMarkerData['conePath'], headMarkerData['spreadPath'], ], - capture: true, - }, - condition: (data, matches) => { - return data.me === matches.target && data.pathOfLightCounter === 6; + capture: false, }, + condition: (data) => data.pathOfLightCounter === 6, delaySeconds: 0.1, // Delay for party headmarker collect durationSeconds: 9, - infoText: (data, matches, output) => { - // If a player from Group A accidentally soaks - if (data.myPathOfLights.length !== 3) - return; - const id = matches.id; - type markerMap = { - [key: string]: 'stack' | 'cone' | 'spread'; - }; - const markers: markerMap = { - '02CB': 'stack', - '02CD': 'cone', - '02CC': 'spread', - }; - const marker = markers[id]; - if (marker === undefined) - return; + suppressSeconds: 1, + infoText: (data, _matches, output) => { + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker - // Unsure that this could happen, unless more than 4 players soaked? - if (marker === 'stack') - return; + // Group A + if (data.isForsakenGroupA) { + if (data.role === 'healer') + return output.baitLeftConeEvens!({ + num: output.num!({ num: num }), + }); + if (data.role === 'tank') + return output.baitCloneFar!({ + num: output.num!({ num: num }), + }); + // DPS Unknown party composition + return output.bait!({ + num: output.num!({ num: num }), + }); + } + // Group B // Spread Players have to be far in the tower, cones need to bait end - const nearFar = data.myPathOfLights[2] === 'spread' + const nearFar = marker === 'spread' ? output.beFar!() : output.beNear!(); if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.mechs!({ + num: output.num!({ num: num }), mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) ? output.rightTower!() : output.leftTower!(), @@ -648,59 +725,45 @@ const triggerSet: TriggerSet = { } return output.mechs!({ + num: output.num!({ num: num }), mech1: output.tower!(), mech2: nearFar, }); }, outputStrings: forsakenOutputStrings, }, - { - id: 'DMU P2 Path of Light Towers 6 Baits', - // Players that still have the first headmarker - type: 'HeadMarker', - netRegex: { - id: [ - headMarkerData['stackPath'], - headMarkerData['conePath'], - headMarkerData['spreadPath'], - ], - capture: false, - }, - condition: (data) => data.pathOfLightCounter === 6, - delaySeconds: 0.1, // Delay for party headmarker collect - durationSeconds: 9, - suppressSeconds: 1, - infoText: (data, _matches, output) => { - if (data.myPathOfLights.length !== 4) - return; - - return output.bait!(); - }, - outputStrings: forsakenOutputStrings, - }, { id: 'DMU P2 Path of Light Towers 7', - // This set should not contain stack markers - // If stacks exist, they came from first set + // BADC All Things Ending (Future) + // BADD All Things Ending (Past) // There should be two stacks, a cone and an aoe - // - // Headmarkers come out ~2s before Future's/Past's End - type: 'HeadMarker', - netRegex: { - id: [ - headMarkerData['stackPath'], - headMarkerData['conePath'], - headMarkerData['spreadPath'], - ], - capture: false, - }, + type: 'StartsUsing', + netRegex: { id: ['BADC', 'BADD'], source: 'Kefka', capture: false }, condition: (data) => data.pathOfLightCounter === 7, - delaySeconds: 0.1, // Delay for party headmarker collect - durationSeconds: 9, - infoText: (data, _matches, output) => { - // Both groups will be on their last soak - // Group B will have two stacks - const marker = data.myPathOfLights[4] ?? 'unknown'; + suppressSeconds: 1, + alertText: (data, _matches, output) => { + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + + // Group A + if (data.isForsakenGroupA) { + if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + return output.markerOnYouTower!({ + num: output.num!({ num: num }), + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + } + return output.markerOnYouTower!({ + num: output.num!({ num: num }), + marker: output[marker]!(), + tower: output.tower!(), + }); + } + + // Group B if (marker === 'stack') { // Need to know for priority const players = data.pathOfLightStackPlayers.map( @@ -712,59 +775,53 @@ const triggerSet: TriggerSet = { ); const msg = players?.join(', '); return output.markerOnYouTower!({ + num: output.num!({ num: num }), marker: output.stacksOnPlayers!({ players: msg }), tower: output.tower!(), }); } - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.markerOnYouTower!({ - marker: output[marker]!(), - tower: marker === 'cone' - ? output.leftTower!() - : output.rightTower!(), - }); - } - return output.markerOnYouTower!({ - marker: output[marker]!(), - tower: output.tower!(), - }); }, outputStrings: forsakenOutputStrings, }, { - id: 'DMU P2 Path of Light Towers and Baits 8', + id: 'DMU P2 Path of Light Towers 8', + // There won't be headmarkers for this set in AAABBBBA or ABBAABBA // This set should not contain stack markers // If stacks exist, they came from first set // 2 Cones and 2 Spreads will soak towers // - // Headmarkers come out ~2s before Future's/Past's End - type: 'HeadMarker', + // Track based on tower soak or fail + // BABF The River of Light + // BAC0 Spelldriver + // BAC1 Spellscatter + // BAC2 Spellwave + type: 'Ability', netRegex: { - id: [ - headMarkerData['stackPath'], - headMarkerData['conePath'], - headMarkerData['spreadPath'], - ], + id: ['BABF', 'BAC0', 'BAC1', 'BAC2'], + source: 'Kefka', capture: false, }, condition: (data) => data.pathOfLightCounter === 8, delaySeconds: 0.1, // Delay for party headmarker collect durationSeconds: 9, + suppressSeconds: 9999, infoText: (data, _matches, output) => { - // Handle first group's last towers - if (data.myPathOfLights.length === 4) { - const marker = data.myPathOfLights[3]; + const num = data.pathOfLightCounter; + const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + // Group A + if (data.isForsakenGroupA) { if (marker === 'stack' || marker === 'unknown') return; // Spread Players have to be far in the tower, cones need to bait end - const nearFar = data.myPathOfLights[3] === 'spread' + const nearFar = marker === 'spread' ? output.beFar!() : output.beNear!(); if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.mechs!({ + num: output.num!({ num: num }), mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) ? output.rightTower!() : output.leftTower!(), @@ -773,13 +830,24 @@ const triggerSet: TriggerSet = { } return output.mechs!({ + num: output.num!({ num: num }), mech1: output.tower!(), mech2: nearFar, }); } // Group B - return output.bait!(); + if (data.role === 'healer') + return output.baitLeftConeEvens!({ + num: output.num!({ num: num }), + }); + if (data.role === 'tank') + return output.baitCloneFar!({ + num: output.num!({ num: num }), + }); + return output.bait!({ + num: output.num!({ num: num }), + }); }, outputStrings: forsakenOutputStrings, }, From 6c7b777a66328de7b1f0124ba4402221d0252c29 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 9 Jun 2026 04:23:49 -0400 Subject: [PATCH 15/49] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 4a65c4bcb20..9e57fce3591 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -745,7 +745,7 @@ const triggerSet: TriggerSet = { const num = data.pathOfLightCounter; const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker - // Group A + // Group A if (data.isForsakenGroupA) { if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.markerOnYouTower!({ @@ -763,7 +763,7 @@ const triggerSet: TriggerSet = { }); } - // Group B + // Group B if (marker === 'stack') { // Need to know for priority const players = data.pathOfLightStackPlayers.map( From e904e934f586a30994861dff2f1bd298528030fe Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 9 Jun 2026 04:35:42 -0400 Subject: [PATCH 16/49] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 9e57fce3591..a35db31920a 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -745,8 +745,8 @@ const triggerSet: TriggerSet = { const num = data.pathOfLightCounter; const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker - // Group A - if (data.isForsakenGroupA) { + // Group A + if (data.isForsakenGroupA) { if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { return output.markerOnYouTower!({ num: output.num!({ num: num }), From 63f3dafac9093bc10cadcd9ede5dd0e918c215c6 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 10 Jun 2026 07:29:51 -0400 Subject: [PATCH 17/49] more refactoring of towers This now also includes config options. Redesigned to rely more on player selection than trying to intuit what should be done. Could be possible to change some of the no strategy to be less guesswork and/or change the default to something not none. --- .../data/07-dt/ultimate/dancing_mad.ts | 1193 +++++++++++++---- 1 file changed, 915 insertions(+), 278 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index a35db31920a..1cc50930886 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -14,20 +14,28 @@ const phases: { [id: string]: Phase } = { // const centerX = 100; // const centerY = 100; +type forsakenHeadmarker = 'cone' | 'spread' | 'stack' | 'unknown'; +type forsakenHeadmarkerMap = { [key: string]: forsakenHeadmarker }; +const forsakenHeadmarkerIdToName: forsakenHeadmarkerMap = { + '02CB': 'stack', + '02CD': 'cone', + '02CC': 'spread', +} as const; + export interface Data extends RaidbossData { readonly triggerSetConfig: { - teleportent: 'clockwise' | 'filipino' | 'none'; - forsaken: 'kroxy-rinon' | 'none'; + forsaken: 'kroxy-rinon' | 'abba' | 'bowtie' | 'none'; }; // General phase: Phase | 'unknown'; // Phase 2 pathOfLightCounter: number; - pathOfLightStackPlayers: string[]; - pathOfLightConePlayers: string[]; - pathOfLightSpreadPlayers: string[]; - myPathOfLights: string[]; - isForsakenGroupA: boolean; + pathOfLightStackPlayers: string[]; // Quick lookup/listing of players with stacks + forsakenPlayerHeadmarkers: { [id: string]: forsakenHeadmarker }; // Quickly check player's headmarker + myPathOfLights: string[]; // History of your markers + isForsakenGroupA: boolean; // Quick lookup for group check + forsakenGroupA: string[]; // List of players in Group A + forsakenGroupB: string[]; // List of players in Group B } const headMarkerData = { @@ -38,9 +46,18 @@ const headMarkerData = { 'spreadPath': '02CC', // When standing in Path of Light tower, causes BAC1 Spellscatter (small aoe on the player) } as const; - const forsakenOutputStrings: OutputStrings = { + spreadBowtie: Outputs.spread, tower: Outputs.getTowers, + leftTower: { + en: 'Left Tower', + }, + rightTower: { + en: 'Right Tower', + }, + towerOrBeNear: { // Used in even towers with no strategy + en: '${tower} / ${near}', + }, avoid: { en: 'Avoid towers', de: 'Türme vermeiden', @@ -50,7 +67,16 @@ const forsakenOutputStrings: OutputStrings = { ko: '기둥 피하기', tc: '遠離塔', }, - stackOnYou: Outputs.stackOnYou, + outOfHitbox: Outputs.outOfHitbox, + cone: { + en: 'Cone on YOU', + }, + spread: { + en: 'Spread on YOU', + }, + stack: { // This generally won't get called unless there is a wrong config or missed tower + en: 'Stack stored on YOU', + }, num: { en: '${num}: ', de: '${num}: ', @@ -63,40 +89,44 @@ const forsakenOutputStrings: OutputStrings = { you: { en: 'YOU', }, - swapTowers: { - en: '${num}Swap Towers', - }, - leftTower: { - en: '${num}Left Tower', - }, - rightTower: { - en: '${num}Right Tower', - }, - cone: { - en: 'Cone on YOU', - }, - spread: { - en: 'AOE on YOU', + beNear: { + en: 'Be Near', + de: 'Sei Nahe', + cn: '站近', + ko: '가까이 있기', }, - stackOnYouTower: { - en: '${tower} + ${marker}', + beFar: { + en: 'Be Far', + de: 'Sei Fern', + cn: '站远', + ko: '멀리 있기', }, - stackOnPlayer: { + stackOnYou: Outputs.stackOnYou, + stackOnPlayer: { // Used only in first tower (role-based) en: 'Stack is on ${player}', }, stacksOnPlayers: { en: 'Stacks on ${players}', }, - markerOnYouStacksOnPlayers: { + stacksOnPlayersTower: { // Used after first tower + en: '${num}${stack} + ${tower}', + }, + stackOnYouTower: { // Used in first tower only + en: '${num}${tower} + ${marker}', + }, + swapTowers: { // Used in second tower only + en: '${num}Swap Towers', + }, + markerOnYouStacksOnPlayers: { // Used only for first tower en: '${num}${marker} + ${stacks}', }, - markerOnYouTower: { + markerOnYouTower: { // Used for Cone or Spread en: '${num}${marker} + ${tower}', }, baitLeftConeOutOdds: { en: '${num}Bait Left Cone Out', }, - baitLeftConeEvens: { + baitLeftConeLeftEvens: { en: '${num}Bait Left Cone Left', }, leftStack: { @@ -105,26 +135,50 @@ const forsakenOutputStrings: OutputStrings = { rightStack: { en: '${num}Right Stack + ${avoid}', }, - beNear: { - en: 'Be Near', - de: 'Sei Nahe', - cn: '站近', - ko: '가까이 있기', - }, - beFar: { - en: 'Be Far', - de: 'Sei Fern', - cn: '站远', - ko: '멀리 있기', - }, mechs: { en: '${num}${mech1} + ${mech2}', }, + mechs3: { + en: '${num}${mech1} + ${mech2} + ${mech3}', + }, bait: { - en: '${num}Bait Cone Right or Clone Far', + en: '${num}Bait Cone Right or Clone Near', + }, + baitConeFromPlayer: { + en: 'Bait Cone from ${player}', + }, + spreadWithPlayer: { + en: 'Spread with ${player}', + }, + baitCloneOppositeTowers: { + en: '${num}Bait Clone Opposite Towers Near', + }, + numBeNearSpreadBowtie: { + en: '${num}${near} + ${spread}', }, - baitCloneFar: { - en: '${num}Bait Clone Far', + baitLeftConeOutBowtie: { + en: '${num}Bait Left Cone Out', + }, + baitLeftConeLeftBowtie: { + en: '${num}Bait Left Cone Left', + }, + getHitBySpreadRightBowtie: { // Used only in 5th tower for AAAABBBB + en: '${num}Get Right + Hit by Spread', + }, + spreadTowersBowtie: { // Used only in last tower for AAAABBBB + en: '${num}${tower} + ${spread}', + }, + markerOnYouNoStrategy: { // Odd Towers + en: '${num}${marker}', + }, + mechsNoStrategy: { + en: '${num}${marker} + ${mechs}', + }, + baitNoStrategy: { // No marker and no strategy was selected + en: '${num}Bait Cone or Clone Near', + }, + baitConeOrStackNoStrategy: { + en: '${num}Bait Cone or Stack', }, }; @@ -136,7 +190,11 @@ const triggerSet: TriggerSet = { id: 'forsaken', comment: { en: - `Kroxy-Rinon 3/4/1: Kefka Bin`, + `There should be two groups of four players, choose tower soak order. + Kroxy-Rinon 3/4/1: Kefka Bin
+ Modified ABBA: Raidplan
+ Bowtie: Raidplan (Will require Tank LB3)
+ Default will be Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.`, }, name: { en: 'P2 Forsaken Strategy', @@ -144,8 +202,9 @@ const triggerSet: TriggerSet = { type: 'select', options: { en: { - 'Group soak order: AAABBBBA. Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.': - 'kroxy-rinon', + 'AAABBBBA (3/4/1), Kroxy-Rinon': 'kroxy-rinon', + 'ABBAABBA (1/2/2/2/1) Modified': 'abba', + 'AAAABBBB (4/4) Bowtie': 'bowtie', 'Generic calls.': 'none', }, }, @@ -160,9 +219,10 @@ const triggerSet: TriggerSet = { pathOfLightCounter: 1, myPathOfLights: [], pathOfLightStackPlayers: [], - pathOfLightConePlayers: [], - pathOfLightSpreadPlayers: [], + forsakenPlayerHeadmarkers: {}, isForsakenGroupA: false, + forsakenGroupA: [], + forsakenGroupB: [], }; }, triggers: [ @@ -186,12 +246,31 @@ const triggerSet: TriggerSet = { durationSeconds: 6.7, response: Responses.bigAoe('alert'), }, + { + id: 'DMU P2 Spell\'s Trouble Clear Current Headmarker', + // Each player gets 4 of these, using this to track when to clear from + // Track when last one is lost + type: 'LosesEffect', + netRegex: { effectId: '13DB', capture: true }, + run: (data, matches) => { + delete data.forsakenPlayerHeadmarkers[matches.target]; + }, + }, { id: 'DMU P2 Path of Light Headmarker Tracker', // When standing in Path of Light tower, causes BAC0 Spelldriver (3-person stack) // When standing in Path of Light tower, causes BAC2 Spellwave (cone targetting nearest player) // When standing in Path of Light tower, causes BAC1 Spellscatter (small aoe on the player) // Headmarkers update ~2.5s prior to 13DB Spell's Trouble debuff count decrementing + // + // Stacks cannot exist with Even towers, there isn't enough players for near Baits + // However, it is still possible to do an odd tower without having stacks + // This seems to be treated as a special case as we find tower 7 give 4 stacks + // + // Possible Group solutions: + // AAABBBBA + // ABBAABBA + // AAAABBBB, requires Tank LB3 due to forced 4 stacks from tower 7 type: 'HeadMarker', netRegex: { id: [ @@ -204,36 +283,34 @@ const triggerSet: TriggerSet = { run: (data, matches) => { const id = matches.id; const target = matches.target; - type markerMap = { - [key: string]: string; - }; - const markers: markerMap = { - '02CB': 'stack', - '02CD': 'cone', - '02CC': 'spread', - }; // Storing self for simple lookups later // This can also be used to track how many towers have been soaked // and what was soaked before to handle who baits where on evens if (data.me === target) - data.myPathOfLights.push(markers[id] ?? 'unknown'); + data.myPathOfLights.push(forsakenHeadmarkerIdToName[id] ?? 'unknown'); // Clear previous Headmarker if set data.pathOfLightStackPlayers = data.pathOfLightStackPlayers.filter((t) => t !== target); - data.pathOfLightConePlayers = data.pathOfLightConePlayers.filter((t) => t !== target); - data.pathOfLightSpreadPlayers = data.pathOfLightSpreadPlayers.filter((t) => t !== target); + data.forsakenPlayerHeadmarkers[matches.target] = forsakenHeadmarkerIdToName[id] ?? 'unknown'; - // To track "groups" - if (data.pathOfLightCounter === 2 && data.me === matches.target) - data.isForsakenGroupA = true; + // On first headmarker, start everyone in same group + // Excluding self as this reduces number of lookups to find partner + if (data.pathOfLightCounter === 1 && data.me !== matches.target) + data.forsakenGroupB.push(matches.target); + + // If the groups are uneven a tower was missed and it's probably a wipe + if (data.pathOfLightCounter === 2) { + // Remove from Group B + data.forsakenGroupB = data.forsakenGroupB.filter((t) => t !== target); + if (data.me === matches.target) + data.isForsakenGroupA = true; + else + data.forsakenGroupA.push(matches.target); + } if (id === headMarkerData['stackPath']) data.pathOfLightStackPlayers.push(target); - else if (id === headMarkerData['conePath']) - data.pathOfLightConePlayers.push(target); - else - data.pathOfLightSpreadPlayers.push(target); }, }, { @@ -261,35 +338,31 @@ const triggerSet: TriggerSet = { durationSeconds: 9, infoText: (data, matches, output) => { const id = matches.id; - type markerMap = { - [key: string]: 'stack' | 'cone' | 'spread'; - }; - const markers: markerMap = { - '02CB': 'stack', - '02CD': 'cone', - '02CC': 'spread', - }; - const marker = markers[id]; + const marker = forsakenHeadmarkerIdToName[id]; if (marker === undefined) return; - const num = data.pathOfLightCounter; + const num = output.num!({ num: data.pathOfLightCounter }); + const config = data.triggerSetConfig.forsaken; if (marker === 'stack') { - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + // These players must get a tower + if (config !== 'none') { if (data.role === 'healer' || data.role === 'tank') return output.stackOnYouTower!({ - num: output.num!({ num: num }), + num: num, tower: output.leftTower!(), marker: output.stackOnYou!(), }); return output.stackOnYouTower!({ - num: output.num!({ num: num }), + num: num, tower: output.rightTower!(), marker: output.stackOnYou!(), }); } + + // Assuming no strategy avoids stack soaking tower in first set return output.stackOnYouTower!({ - num: output.num!({ num: num }), + num: num, tower: output.tower!(), marker: output.stackOnYou!(), }); @@ -302,6 +375,7 @@ const triggerSet: TriggerSet = { const myRoleIsDPS = data.party.isDPS(data.me); // If both stack players are the same role, output both players + // This would be a non-standard composition if (myRoleIsDPS === stack1IsDPS && myRoleIsDPS === stack2IsDPS) { const players = data.pathOfLightStackPlayers.map( (player) => { @@ -310,16 +384,17 @@ const triggerSet: TriggerSet = { ); const msg = players?.join(', '); return output.markerOnYouStacksOnPlayers!({ - num: output.num!({ num: num }), + num: num, marker: output[marker]!(), stacks: output.stacksOnPlayers!({ players: msg }), }); } // Our partner will be the role that matches us + // If not, then assuredly the strategy used something like conga line for each role const possiblePartner = data.party.member(myRoleIsDPS === stack1IsDPS ? stack1 : stack2); return output.markerOnYouStacksOnPlayers!({ - num: output.num!({ num: num }), + num: num, marker: output[marker]!(), stacks: output.stackOnPlayer!({ player: possiblePartner }), }); @@ -338,9 +413,7 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P2 Path of Light Towers 2', - // This set should not contain stack markers - // If stacks exist, they came from first set - // 2 Cones and 2 Spreads will soak towers + // Expecting 2 Cones and 2 Spreads soak towers // // Headmarkers come out ~2s before Future's/Past's End type: 'HeadMarker', @@ -357,69 +430,128 @@ const triggerSet: TriggerSet = { durationSeconds: 9, suppressSeconds: 1, infoText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; + const isForsakenGroupA = data.isForsakenGroupA; - // Group A - if (data.isForsakenGroupA) { + // Modified ABBA and Kroxy-Rinon Baits + if ( + (!isForsakenGroupA && config === 'kroxy-rinon') || + (isForsakenGroupA && config === 'abba') + ) { + if (data.role === 'healer') + return output.baitLeftConeLeftEvens!({ + num: num, + }); + if (data.role === 'tank') + return output.baitCloneOppositeTowers!({ + num: num, + }); + // DPS Unknown party composition + return output.bait!({ + num: num, + }); + } + + // ABBA (unmodified) and AAAABBBB, Baits + if (config === 'bowtie' && !data.isForsakenGroupA) { + // Group A Avoids Towers (ABBA) + // Group B Avoids Towers (AAAABBBB) + return output.mechs!({ + num: num, + mech1: output.beNear!(), + mech2: output.avoid!(), + }); + } + + // If someone has stack from beginning + if ( + (config !== 'none') && + (marker === 'stack' || marker === 'unknown') + ) + return; + + // Modified ABBA and Kroxy-Rinon Tower Soaks + if ( + (isForsakenGroupA && config === 'kroxy-rinon') || + (!isForsakenGroupA && config === 'abba') + ) { // Spread Players have to be far in the tower, cones need to bait end const nearFar = marker === 'spread' ? output.beFar!() : output.beNear!(); - - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - // Check our previous headmarker - // Supports Left, DPS Right - if (data.role === 'healer' || data.role === 'tank') { - // Support had cone in left tower 1, moves up in tower - if (data.myPathOfLights[0] === 'cone') - return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.tower!(), - mech2: nearFar, - }); - // Support with spread on right tower 1 changes to left - if (data.myPathOfLights[0] === 'spread') - return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.swapTowers!(), - mech2: nearFar, - }); - } + // Check our previous headmarker + // Supports Left, DPS Right + if (data.role === 'healer' || data.role === 'tank') { + // Support had cone in left tower 1, moves up in tower if (data.myPathOfLights[0] === 'cone') return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.swapTowers!(), + num: num, + mech1: output.tower!(), mech2: nearFar, }); + // Support with spread on right tower 1 changes to left if (data.myPathOfLights[0] === 'spread') return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.tower!(), + num: num, + mech1: output.swapTowers!(), mech2: nearFar, }); } + if (data.myPathOfLights[0] === 'cone') + return output.mechs!({ + num: num, + mech1: output.swapTowers!(), + mech2: nearFar, + }); + if (data.myPathOfLights[0] === 'spread') + return output.mechs!({ + num: num, + mech1: output.tower!(), + mech2: nearFar, + }); + } - // No strategy just say the cone/spread difference - return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.tower!(), - mech2: nearFar, - }); + // ABBA (unmodified) and AAAABBBB, Soaks + if (config === 'bowtie' && isForsakenGroupA) { + // Tower soakers don't bait ends + // Group B Soaks Towers (ABBA) + // Group A Soaks Towers (AAAA) + const group = data.forsakenGroupA; + // Partner is whoever has the same marker + const partner = playerHeadmarkers[group[0] ?? 0] === marker + ? group[0] + : playerHeadmarkers[group[1] ?? 0] === marker + ? group[1] + : group[2]; // Or unknown matched + const name = data.party.member(partner); + if (marker === 'spread') + return output.mechs3!({ + num: num, + mech1: output.rightTower!(), + mech2: output.spreadWithPlayer!({ player: name }), + mech3: output.outOfHitbox!(), + }); + if (marker === 'cone') + return output.mechs3!({ + num: num, + mech1: output.leftTower!(), + mech2: output.baitConeFromPlayer!({ player: name }), + mech3: output.outOfHitbox!(), + }); } - // Group B - if (data.role === 'healer') - return output.baitLeftConeEvens!({ - num: output.num!({ num: num }), - }); - if (data.role === 'tank') - return output.baitCloneFar!({ - num: output.num!({ num: num }), - }); - // DPS Unkmown party composition - return output.bait!({ - num: output.num!({ num: num }), + // No strategy selected + // Many options: Tower, Bait Cone, Share Stack? + return output.mechsNoStrategy!({ + num: num, + marker: output[marker]!(), + mechs: output.towerOrBeNear!({ + tower: output.tower!(), + near: output.beNear!(), + }), }); }, outputStrings: forsakenOutputStrings, @@ -472,18 +604,27 @@ const triggerSet: TriggerSet = { id: 'DMU P2 Path of Light Towers 3', // BADC All Things Ending (Future) // BADD All Things Ending (Past) - // There should be two stacks, a cone and an aoe + // Expecting 2 Stacks, 1 Cone, and 1 Spread soak towers type: 'StartsUsing', netRegex: { id: ['BADC', 'BADD'], source: 'Kefka', capture: false }, condition: (data) => data.pathOfLightCounter === 3, suppressSeconds: 1, alertText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; + const isForsakenGroupA = data.isForsakenGroupA; - // Group A - if (data.isForsakenGroupA) { - if (marker === 'stack') { + // Stacks should soak towers + if (marker === 'stack') { + if ( + ( + isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') + ) || + (!isForsakenGroupA && config === 'abba') || + (config === 'none') + ) { // Need to know for priority const players = data.pathOfLightStackPlayers.map( (player) => { @@ -493,44 +634,60 @@ const triggerSet: TriggerSet = { }, ); const msg = players?.join(', '); - return output.markerOnYouTower!({ - num: output.num!({ num: num }), - marker: output.stacksOnPlayers!({ players: msg }), + + // Assuming none config soaks + return output.stacksOnPlayersTower!({ + num: num, + stack: output.stacksOnPlayers!({ players: msg }), tower: output.tower!(), }); } + } - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.markerOnYouTower!({ - num: output.num!({ num: num }), - marker: output[marker]!(), - tower: marker === 'cone' - ? output.leftTower!() - : output.rightTower!(), - }); - } + // Tower soakers, non stack markers + if ( + ( + isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') + ) || + (!isForsakenGroupA && config === 'abba') + ) { return output.markerOnYouTower!({ - num: output.num!({ num: num }), + num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), }); } - // Group B - // So long as it is standard party composition... - if (data.role === 'tank') - return output.leftStack!({ - num: output.num!({ num: num }), + // Baits and Stacks + if ( + ( + !isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') + ) || + (isForsakenGroupA && config === 'abba') + ) { + // So long as it is standard party composition... + if (data.role === 'tank') + return output.leftStack!({ + num: num, + avoid: output.avoid!(), + }); + if (data.role === 'healer') + return output.baitLeftConeOutOdds!({ + num: num, + }); + // 2 DPS in stack + return output.rightStack!({ + num: num, avoid: output.avoid!(), }); - if (data.role === 'healer') - return output.baitLeftConeOutOdds!({ - num: output.num!({ num: num }), - }); - // 2 DPS in stack - return output.rightStack!({ - num: output.num!({ num: num }), - avoid: output.avoid!(), + } + + // No strategy selected + return output.markerOnYouNoStrategy!({ + num: num, + marker: output[marker]!(), }); }, outputStrings: forsakenOutputStrings, @@ -539,7 +696,7 @@ const triggerSet: TriggerSet = { id: 'DMU P2 Path of Light Towers 4', // This set should not contain stack markers // If stacks exist, they came from first set - // 2 Cones and 2 Spreads will soak towers + // Expecting 2 Cones and 2 Spreads soak towers // // Headmarkers come out ~2s before Future's/Past's End type: 'HeadMarker', @@ -556,48 +713,227 @@ const triggerSet: TriggerSet = { durationSeconds: 9, suppressSeconds: 1, infoText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; + const isForsakenGroupA = data.isForsakenGroupA; - // Group A - if (data.isForsakenGroupA) { + // Baits + if ( + (isForsakenGroupA && config === 'kroxy-rinon') || + (!isForsakenGroupA && config === 'abba') + ) { if (data.role === 'healer') - return output.baitLeftConeEvens!({ - num: output.num!({ num: num }), + return output.baitLeftConeLeftEvens!({ + num: num, }); if (data.role === 'tank') - return output.baitCloneFar!({ - num: output.num!({ num: num }), + return output.baitCloneOppositeTowers!({ + num: num, }); - // DPS Unkmown party composition + // DPS Unknown party composition return output.bait!({ - num: output.num!({ num: num }), + num: num, + }); + } + + // AAAABBBB, Baits + if (config === 'bowtie' && !isForsakenGroupA) { + // Group B Avoids Towers + return output.mechs!({ + num: num, + mech1: output.beNear!(), + mech2: output.avoid!(), }); } - // Group B // If someone has stack from beginning - if (marker === 'stack' || marker === 'unknown') + if ( + (config !== 'none') && + (marker === 'stack' || marker === 'unknown') + ) return; + // AAAABBBB, Soaks + if (config === 'bowtie' && isForsakenGroupA) { + // Tower soakers don't bait ends + // Group A Soaks Towers + const group = data.forsakenGroupA; + // Partner is whoever has the same marker + const partner = playerHeadmarkers[group[0] ?? 0] === marker + ? group[0] + : playerHeadmarkers[group[1] ?? 0] === marker + ? group[1] + : group[2]; // Or unknown matched + const name = data.party.member(partner); + if (marker === 'spread') + return output.mechs3!({ + num: num, + mech1: output.rightTower!(), + mech2: output.spreadWithPlayer!({ player: name }), + mech3: output.outOfHitbox!(), + }); + if (marker === 'cone') + return output.mechs3!({ + num: num, + mech1: output.leftTower!(), + mech2: output.baitConeFromPlayer!({ player: name }), + mech3: output.outOfHitbox!(), + }); + } + // Spread Players have to be far in the tower, cones need to bait end const nearFar = marker === 'spread' ? output.beFar!() : output.beNear!(); - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.mechs!({ - mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) - ? output.rightTower!() - : output.leftTower!(), - mech2: nearFar, + // Tower Soaks + if (config === 'kroxy-rinon' || config === 'abba') { + if (data.role === 'healer') { + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.leftTower!(), + mech3: nearFar, + }); + } + + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const group = data.forsakenGroupB; + const member1 = group[0] ?? ''; + const member2 = group[1] ?? ''; + const member3 = group[2] ?? ''; + if (data.role === 'tank') { + // Need to look at what healer has in relation to us + // Partner is whoever has the same marker + const partner = data.party.isHealer(member1) + ? member1 + : data.party.isHealer(member2) + ? member2 + : data.party.isHealer(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not get priority + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + mech3: nearFar, + }); + } + + if (Util.isMeleeDpsJob(data.job)) { + const isRangedDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); + }; + // Partner should be a ranged dps, for standard comp + const partner = isRangedDPS(member1) + ? member1 + : isRangedDPS(member2) + ? member2 + : isRangedDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find caster or phys ranged partner + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + mech3: nearFar, + }); + } + + // If we find a melee in our group we are the ranged priority + // Partner should be a melee dps, for optimal comp + const isMeleeDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isMeleeDpsJob(jobName); + }; + const partner = isMeleeDPS(member1) + ? member1 + : isMeleeDPS(member2) + ? member2 + : isMeleeDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find melee dps + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + // Highest priority right + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.rightTower!(), + mech3: nearFar, }); } - return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.tower!(), - mech2: nearFar, + // No strategy selected + // Many options: Tower, Bait Cone, Share Stack? + return output.mechsNoStrategy!({ + num: num, + marker: output[marker]!(), + mechs: output.towerOrBeNear!({ + tower: output.tower!(), + near: output.beNear!(), + }), }); }, outputStrings: forsakenOutputStrings, @@ -606,34 +942,66 @@ const triggerSet: TriggerSet = { id: 'DMU P2 Path of Light Towers 5', // BADC All Things Ending (Future) // BADD All Things Ending (Past) - // There should be two stacks, a cone and an aoe + // Expecting 2 Stacks, 1 Cone, and 1 Spread soak towers + // However, AAAABBBB has 2 Cones and 2 Spreads soak towers type: 'StartsUsing', netRegex: { id: ['BADC', 'BADD'], source: 'Kefka', capture: false }, condition: (data) => data.pathOfLightCounter === 5, suppressSeconds: 1, alertText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; + const isForsakenGroupA = data.isForsakenGroupA; - // Group A - if (data.isForsakenGroupA) { + // Baits and Stacks + if ( + (isForsakenGroupA && config === 'kroxy-rinon') || + (!isForsakenGroupA && config === 'abba') + ) { // So long as it is standard party composition... if (data.role === 'tank') return output.leftStack!({ - num: output.num!({ num: num }), + num: num, avoid: output.avoid!(), }); if (data.role === 'healer') return output.baitLeftConeOutOdds!({ - num: output.num!({ num: num }), + num: num, }); return output.rightStack!({ - num: output.num!({ num: num }), + num: num, avoid: output.avoid!(), }); } - // Group B + if (config === 'bowtie') { + // Bowtie has people bait cones, but cones could bait eachother if they wanted + if (!isForsakenGroupA) { + return output.markerOnYouTower!({ + num: num, + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + } + if (data.role === 'tank') + return output.baitLeftConeLeftBowtie!({ + num: num, + }); + if (data.role === 'healer') + return output.baitLeftConeOutBowtie!({ + num: num, + }); + return output.getHitBySpreadRightBowtie!({ + num: num, + }); + } + + // Tower Soaks + // In AAAABBBB, there is no stack if (marker === 'stack') { // Need to know for priority const players = data.pathOfLightStackPlayers.map( @@ -644,35 +1012,41 @@ const triggerSet: TriggerSet = { }, ); const msg = players?.join(', '); - return output.markerOnYouTower!({ - num: output.num!({ num: num }), - marker: output.stacksOnPlayers!({ players: msg }), + + // Assuming none config soaks + return output.stacksOnPlayersTower!({ + num: num, + stack: output.stacksOnPlayers!({ players: msg }), tower: output.tower!(), }); } - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + // This ends up being Group B || Group A for respective config + if (config === 'kroxy-rinon' || config === 'abba') { return output.markerOnYouTower!({ - num: output.num!({ num: num }), + num: num, marker: output[marker]!(), tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), }); } - return output.markerOnYouTower!({ - num: output.num!({ num: num }), + + // No strategy + if (marker === 'unknown') + return output.baitConeOrStackNoStrategy!({ + num: num, + }); + return output.markerOnYouNoStrategy!({ + num: num, marker: output[marker]!(), - tower: output.tower!(), }); }, outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Path of Light Towers 6', - // This set should not contain stack markers - // If stacks exist, they came from first set - // 2 Cones and 2 Spreads will soak towers + // Expecting 2 Cones and 2 Spreads soak towers // // Headmarkers come out ~2s before Future's/Past's End type: 'HeadMarker', @@ -689,34 +1063,75 @@ const triggerSet: TriggerSet = { durationSeconds: 9, suppressSeconds: 1, infoText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; + const isForsakenGroupA = data.isForsakenGroupA; - // Group A - if (data.isForsakenGroupA) { + // Baits + if ( + isForsakenGroupA && + (config === 'kroxy-rinon' || config === 'abba') + ) { if (data.role === 'healer') - return output.baitLeftConeEvens!({ - num: output.num!({ num: num }), + return output.baitLeftConeLeftEvens!({ + num: num, }); if (data.role === 'tank') - return output.baitCloneFar!({ - num: output.num!({ num: num }), + return output.baitCloneOppositeTowers!({ + num: num, }); // DPS Unknown party composition return output.bait!({ - num: output.num!({ num: num }), + num: num, }); } - // Group B + if (config === 'bowtie') { + // Group A Baits Ends + if (isForsakenGroupA) + return output.numBeNearSpreadBowtie!({ + num: num, + near: output.beNear!(), + spread: output.spreadBowtie!(), + }); + + // Tower soakers don't bait ends + // Group B Soaks Towers + const group = data.forsakenGroupB; + // Partner is whoever has the same marker + const partner = playerHeadmarkers[group[0] ?? 0] === marker + ? group[0] + : playerHeadmarkers[group[1] ?? 0] === marker + ? group[1] + : group[2]; // Or unknown matched + const name = data.party.member(partner); + if (marker === 'spread') + return output.mechs3!({ + num: num, + mech1: output.rightTower!(), + mech2: output.spreadWithPlayer!({ player: name }), + mech3: output.outOfHitbox!(), + }); + if (marker === 'cone') + return output.mechs3!({ + num: num, + mech1: output.leftTower!(), + mech2: output.baitConeFromPlayer!({ player: name }), + mech3: output.outOfHitbox!(), + }); + } + // Spread Players have to be far in the tower, cones need to bait end const nearFar = marker === 'spread' ? output.beFar!() : output.beNear!(); - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { + // Group B + if (config === 'kroxy-rinon' || config === 'abba') { return output.mechs!({ - num: output.num!({ num: num }), + num: num, mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) ? output.rightTower!() : output.leftTower!(), @@ -724,10 +1139,19 @@ const triggerSet: TriggerSet = { }); } - return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.tower!(), - mech2: nearFar, + // No strategy selected + // Many options: Tower, Bait Cone, Share Stack? + if (marker === 'unknown') + return output.baitNoStrategy!({ + num: num, + }); + return output.mechsNoStrategy!({ + num: num, + marker: output[marker]!(), + mechs: output.towerOrBeNear!({ + tower: output.tower!(), + near: output.beNear!(), + }), }); }, outputStrings: forsakenOutputStrings, @@ -736,34 +1160,36 @@ const triggerSet: TriggerSet = { id: 'DMU P2 Path of Light Towers 7', // BADC All Things Ending (Future) // BADD All Things Ending (Past) - // There should be two stacks, a cone and an aoe + // Expecting 2 Stacks, 1 Cone, and 1 Spread soak towers type: 'StartsUsing', netRegex: { id: ['BADC', 'BADD'], source: 'Kefka', capture: false }, condition: (data) => data.pathOfLightCounter === 7, suppressSeconds: 1, alertText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; - // Group A - if (data.isForsakenGroupA) { - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.markerOnYouTower!({ - num: output.num!({ num: num }), - marker: output[marker]!(), - tower: marker === 'cone' - ? output.leftTower!() - : output.rightTower!(), + // Baits and Stacks + if (data.isForsakenGroupA && config !== 'none') { + // So long as it is standard party composition... + if (data.role === 'tank') + return output.leftStack!({ + num: num, + avoid: output.avoid!(), }); - } - return output.markerOnYouTower!({ - num: output.num!({ num: num }), - marker: output[marker]!(), - tower: output.tower!(), + if (data.role === 'healer') + return output.baitLeftConeOutOdds!({ + num: num, + }); + return output.rightStack!({ + num: num, + avoid: output.avoid!(), }); } - // Group B + // Tower soaks if (marker === 'stack') { // Need to know for priority const players = data.pathOfLightStackPlayers.map( @@ -774,21 +1200,44 @@ const triggerSet: TriggerSet = { }, ); const msg = players?.join(', '); - return output.markerOnYouTower!({ - num: output.num!({ num: num }), - marker: output.stacksOnPlayers!({ players: msg }), + + // Assuming none config soaks + return output.stacksOnPlayersTower!({ + num: num, + stack: output.stacksOnPlayers!({ players: msg }), tower: output.tower!(), }); } + + // Cone/Stack Tower Soaks + // Group B + if (config !== 'none') + return output.markerOnYouTower!({ + num: num, + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + + // No strategy + if (marker === 'unknown') + return output.baitConeOrStackNoStrategy!({ + num: num, + }); + return output.markerOnYouNoStrategy!({ + num: num, + marker: output[marker]!(), + }); }, outputStrings: forsakenOutputStrings, }, { id: 'DMU P2 Path of Light Towers 8', - // There won't be headmarkers for this set in AAABBBBA or ABBAABBA + // Shouldn't be new headmarkers from previous towers // This set should not contain stack markers - // If stacks exist, they came from first set - // 2 Cones and 2 Spreads will soak towers + // Expecting 2 Cones and 2 Spreads soak towers + // However AAAABBBB will have 4 Stacks soak towers // // Track based on tower soak or fail // BABF The River of Light @@ -806,51 +1255,238 @@ const triggerSet: TriggerSet = { durationSeconds: 9, suppressSeconds: 9999, infoText: (data, _matches, output) => { - const num = data.pathOfLightCounter; - const marker = data.myPathOfLights.at(-1) ?? 'unknown'; // Current headmarker + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const num = output.num!({ num: data.pathOfLightCounter }); + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker + const config = data.triggerSetConfig.forsaken; - // Group A if (data.isForsakenGroupA) { - if (marker === 'stack' || marker === 'unknown') - return; + // Tower Soaks for ABBABBA and AAABBBBA + if (config === 'kroxy-rinon' || config === 'abba') { + // This means player from A accidentally took tower previously + if (marker === 'stack' || marker === 'unknown') + return; - // Spread Players have to be far in the tower, cones need to bait end - const nearFar = marker === 'spread' - ? output.beFar!() - : output.beNear!(); + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = marker === 'spread' + ? output.beFar!() + : output.beNear!(); - if (data.triggerSetConfig.forsaken === 'kroxy-rinon') { - return output.mechs!({ - num: output.num!({ num: num }), - mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) - ? output.rightTower!() - : output.leftTower!(), - mech2: nearFar, + if (data.role === 'healer') { + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.leftTower!(), + mech3: nearFar, + }); + } + + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const group = data.forsakenGroupA; + const member1 = group[0] ?? ''; + const member2 = group[1] ?? ''; + const member3 = group[2] ?? ''; + if (data.role === 'tank') { + // Need to look at what healer has in relation to us + // Partner is whoever has the same marker + const partner = data.party.isHealer(member1) + ? member1 + : data.party.isHealer(member2) + ? member2 + : data.party.isHealer(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not get priority + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + mech3: nearFar, + }); + } + + if (Util.isMeleeDpsJob(data.job)) { + const isRangedDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); + }; + // Partner should be a ranged dps, for standard comp + const partner = isRangedDPS(member1) + ? member1 + : isRangedDPS(member2) + ? member2 + : isRangedDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find caster or phys ranged partner + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + mech3: nearFar, + }); + } + + // If we find a melee in our group we are the ranged priority + // Partner should be a melee dps, for optimal comp + const isMeleeDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isMeleeDpsJob(jobName); + }; + const partner = isMeleeDPS(member1) + ? member1 + : isMeleeDPS(member2) + ? member2 + : isMeleeDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find melee dps + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + // Highest priority right + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.rightTower!(), + mech3: nearFar, }); } - return output.mechs!({ - num: output.num!({ num: num }), - mech1: output.tower!(), - mech2: nearFar, - }); + // End Baits for AAAABBBB + if (config === 'bowtie') + return output.numBeNearSpreadBowtie!({ + num: num, + near: output.beNear!(), + spread: output.spreadBowtie!(), + }); } - // Group B - if (data.role === 'healer') - return output.baitLeftConeEvens!({ - num: output.num!({ num: num }), + // Baits for ABBAABBA and AAABBBBA + if (config === 'kroxy-rinon' || config === 'abba') { + if (data.role === 'healer') + return output.baitLeftConeLeftEvens!({ + num: num, + }); + if (data.role === 'tank') + return output.baitCloneOppositeTowers!({ + num: num, + }); + return output.bait!({ + num: num, }); - if (data.role === 'tank') - return output.baitCloneFar!({ - num: output.num!({ num: num }), + } + if (config === 'bowtie') { + // Each person in Group B will have a stack marker + if (data.role === 'healer' || data.role === 'tank') + return output.spreadTowersBowtie!({ + num: num, + tower: output.leftTower!(), + spread: output.spreadBowtie!(), + }); + return output.spreadTowersBowtie!({ + num: num, + tower: output.rightTower!(), + spread: output.spreadBowtie!(), }); - return output.bait!({ - num: output.num!({ num: num }), + } + + // No strategy selected + // Many options: Tower, Bait Cone, Share Stack? + if (marker === 'unknown') + return output.baitNoStrategy!({ + num: num, + }); + return output.mechsNoStrategy!({ + num: num, + marker: output[marker]!(), + mechs: output.towerOrBeNear!({ + tower: output.tower!(), + near: output.beNear!(), + }), }); }, outputStrings: forsakenOutputStrings, }, + { + id: 'DMU P2 Path of Light Tower 8 AAAABBBB Special', + // BAD2 Future's End or BAD3 Past's End will go off same time as 4 players + // take a 3-person stack solo + // For some reason the phase is coded such that the 7th tower will give 4 stacks + // under this scenario + type: 'StartsUsing', + netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, + condition: (data) => { + return data.role === 'tank' && data.pathOfLightCounter === 8 && data.triggerSetConfig.forsaken === 'bowtie'; + }, + delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 3, // 6.4s castTime, this is 4s before damage + alarmText: (_data, _matches, output) => output.text!(), + outputStrings: { + text: { + en: 'TANK LB!!', + de: 'TANK LB!!', + fr: 'LB TANK !!', + ja: 'タンクLB!!', + cn: '坦克LB!!', + ko: '탱리밋!!', + tc: '坦克LB!!', + }, + }, + }, { id: 'DMU P2 Light of Judgment', type: 'StartsUsing', @@ -919,6 +1555,7 @@ const triggerSet: TriggerSet = { 'locale': 'en', 'replaceText': { 'Future\'s End/Past\'s End': 'Future/Past\'s End', + 'Spelldriver/Spellscatter/Spellwave': 'Spelldriver/scatter/wave', }, }, ], From b808349051fc2256af2d0836a36e970a371b7df9 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 10 Jun 2026 07:42:23 -0400 Subject: [PATCH 18/49] lint --- .../data/07-dt/ultimate/dancing_mad.ts | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 1cc50930886..04269e99de7 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -189,8 +189,7 @@ const triggerSet: TriggerSet = { { id: 'forsaken', comment: { - en: - `There should be two groups of four players, choose tower soak order. + en: `There should be two groups of four players, choose tower soak order.
Kroxy-Rinon 3/4/1: Kefka Bin
Modified ABBA: Raidplan
Bowtie: Raidplan (Will require Tank LB3)
@@ -292,7 +291,8 @@ const triggerSet: TriggerSet = { // Clear previous Headmarker if set data.pathOfLightStackPlayers = data.pathOfLightStackPlayers.filter((t) => t !== target); - data.forsakenPlayerHeadmarkers[matches.target] = forsakenHeadmarkerIdToName[id] ?? 'unknown'; + data.forsakenPlayerHeadmarkers[matches.target] = forsakenHeadmarkerIdToName[id] ?? + 'unknown'; // On first headmarker, start everyone in same group // Excluding self as this reduces number of lookups to find partner @@ -441,18 +441,18 @@ const triggerSet: TriggerSet = { (!isForsakenGroupA && config === 'kroxy-rinon') || (isForsakenGroupA && config === 'abba') ) { - if (data.role === 'healer') - return output.baitLeftConeLeftEvens!({ - num: num, - }); - if (data.role === 'tank') - return output.baitCloneOppositeTowers!({ - num: num, - }); - // DPS Unknown party composition - return output.bait!({ + if (data.role === 'healer') + return output.baitLeftConeLeftEvens!({ + num: num, + }); + if (data.role === 'tank') + return output.baitCloneOppositeTowers!({ num: num, }); + // DPS Unknown party composition + return output.bait!({ + num: num, + }); } // ABBA (unmodified) and AAAABBBB, Baits @@ -828,7 +828,7 @@ const triggerSet: TriggerSet = { mech1: output[marker]!(), mech2: output.tower!(), mech3: nearFar, - }); + }); return output.mechs3!({ num: num, @@ -871,7 +871,7 @@ const triggerSet: TriggerSet = { mech1: output[marker]!(), mech2: output.tower!(), mech3: nearFar, - }); + }); return output.mechs3!({ num: num, @@ -914,7 +914,7 @@ const triggerSet: TriggerSet = { mech1: output[marker]!(), mech2: output.tower!(), mech3: nearFar, - }); + }); // Highest priority right return output.mechs3!({ @@ -1310,7 +1310,7 @@ const triggerSet: TriggerSet = { mech1: output[marker]!(), mech2: output.tower!(), mech3: nearFar, - }); + }); return output.mechs3!({ num: num, @@ -1353,7 +1353,7 @@ const triggerSet: TriggerSet = { mech1: output[marker]!(), mech2: output.tower!(), mech3: nearFar, - }); + }); return output.mechs3!({ num: num, @@ -1396,7 +1396,7 @@ const triggerSet: TriggerSet = { mech1: output[marker]!(), mech2: output.tower!(), mech3: nearFar, - }); + }); // Highest priority right return output.mechs3!({ @@ -1471,7 +1471,8 @@ const triggerSet: TriggerSet = { type: 'StartsUsing', netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, condition: (data) => { - return data.role === 'tank' && data.pathOfLightCounter === 8 && data.triggerSetConfig.forsaken === 'bowtie'; + return data.role === 'tank' && data.pathOfLightCounter === 8 && + data.triggerSetConfig.forsaken === 'bowtie'; }, delaySeconds: (_data, matches) => parseFloat(matches.castTime) - 3, // 6.4s castTime, this is 4s before damage alarmText: (_data, _matches, output) => output.text!(), From de3475b632c6dd25485ae5f483720b1027d4117f Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 10 Jun 2026 08:09:30 -0400 Subject: [PATCH 19/49] add missing tower 6 kroxy-rinon group b priority. --- .../data/07-dt/ultimate/dancing_mad.ts | 146 ++++++++++++++++-- 1 file changed, 136 insertions(+), 10 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 04269e99de7..9097e7350ca 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1123,19 +1123,145 @@ const triggerSet: TriggerSet = { }); } - // Spread Players have to be far in the tower, cones need to bait end - const nearFar = marker === 'spread' - ? output.beFar!() - : output.beNear!(); - // Group B if (config === 'kroxy-rinon' || config === 'abba') { - return output.mechs!({ + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = marker === 'spread' + ? output.beFar!() + : output.beNear!(); + + if (data.role === 'healer') { + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.leftTower!(), + mech3: nearFar, + }); + } + + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const group = data.forsakenGroupA; + const member1 = group[0] ?? ''; + const member2 = group[1] ?? ''; + const member3 = group[2] ?? ''; + if (data.role === 'tank') { + // Need to look at what healer has in relation to us + // Partner is whoever has the same marker + const partner = data.party.isHealer(member1) + ? member1 + : data.party.isHealer(member2) + ? member2 + : data.party.isHealer(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not get priority + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + mech3: nearFar, + }); + } + + if (Util.isMeleeDpsJob(data.job)) { + const isRangedDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); + }; + // Partner should be a ranged dps, for standard comp + const partner = isRangedDPS(member1) + ? member1 + : isRangedDPS(member2) + ? member2 + : isRangedDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find caster or phys ranged partner + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + mech3: nearFar, + }); + } + + // If we find a melee in our group we are the ranged priority + // Partner should be a melee dps, for optimal comp + const isMeleeDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isMeleeDpsJob(jobName); + }; + const partner = isMeleeDPS(member1) + ? member1 + : isMeleeDPS(member2) + ? member2 + : isMeleeDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find melee dps + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, + }); + + // Highest priority right + return output.mechs3!({ num: num, - mech1: data.role === 'tank' || Util.isMeleeDpsJob(data.job) - ? output.rightTower!() - : output.leftTower!(), - mech2: nearFar, + mech1: output[marker]!(), + mech2: output.rightTower!(), + mech3: nearFar, }); } From df08c6a9c1a8c1634a462c18e11105b7ad241f55 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 10 Jun 2026 08:12:06 -0400 Subject: [PATCH 20/49] typo in group list --- 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 9097e7350ca..10f566ff9a2 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1140,7 +1140,7 @@ const triggerSet: TriggerSet = { } const playerHeadmarkers = data.forsakenPlayerHeadmarkers; - const group = data.forsakenGroupA; + const group = data.forsakenGroupB; const member1 = group[0] ?? ''; const member2 = group[1] ?? ''; const member3 = group[2] ?? ''; From bc87dbdd58251463f380e170534704225e402b80 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Wed, 10 Jun 2026 08:58:00 -0400 Subject: [PATCH 21/49] Remove pathOfLights array and swapTowers output This will have to be redesigned to perhaps tracking the previous tower and comparing it to the expected next tower instead of trying to base on what our marker was so that we avoid recalculating priorities. --- .../data/07-dt/ultimate/dancing_mad.ts | 169 ++++++++++++++---- 1 file changed, 131 insertions(+), 38 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 10f566ff9a2..43cf42d2c3b 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -32,7 +32,6 @@ export interface Data extends RaidbossData { pathOfLightCounter: number; pathOfLightStackPlayers: string[]; // Quick lookup/listing of players with stacks forsakenPlayerHeadmarkers: { [id: string]: forsakenHeadmarker }; // Quickly check player's headmarker - myPathOfLights: string[]; // History of your markers isForsakenGroupA: boolean; // Quick lookup for group check forsakenGroupA: string[]; // List of players in Group A forsakenGroupB: string[]; // List of players in Group B @@ -114,9 +113,6 @@ const forsakenOutputStrings: OutputStrings = { stackOnYouTower: { // Used in first tower only en: '${num}${tower} + ${marker}', }, - swapTowers: { // Used in second tower only - en: '${num}Swap Towers', - }, markerOnYouStacksOnPlayers: { // Used only for first tower en: '${num}${marker} + ${stacks}', }, @@ -216,7 +212,6 @@ const triggerSet: TriggerSet = { phase: 'p1', // Phase 2 pathOfLightCounter: 1, - myPathOfLights: [], pathOfLightStackPlayers: [], forsakenPlayerHeadmarkers: {}, isForsakenGroupA: false, @@ -283,12 +278,6 @@ const triggerSet: TriggerSet = { const id = matches.id; const target = matches.target; - // Storing self for simple lookups later - // This can also be used to track how many towers have been soaked - // and what was soaked before to handle who baits where on evens - if (data.me === target) - data.myPathOfLights.push(forsakenHeadmarkerIdToName[id] ?? 'unknown'); - // Clear previous Headmarker if set data.pathOfLightStackPlayers = data.pathOfLightStackPlayers.filter((t) => t !== target); data.forsakenPlayerHeadmarkers[matches.target] = forsakenHeadmarkerIdToName[id] ?? @@ -482,36 +471,140 @@ const triggerSet: TriggerSet = { const nearFar = marker === 'spread' ? output.beFar!() : output.beNear!(); - // Check our previous headmarker - // Supports Left, DPS Right - if (data.role === 'healer' || data.role === 'tank') { - // Support had cone in left tower 1, moves up in tower - if (data.myPathOfLights[0] === 'cone') - return output.mechs!({ + + if (data.role === 'healer') { + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.leftTower!(), + mech3: nearFar, + }); + } + + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const member1 = group[0] ?? ''; + const member2 = group[1] ?? ''; + const member3 = group[2] ?? ''; + if (data.role === 'tank') { + // Need to look at what healer has in relation to us + // Partner is whoever has the same marker + const partner = data.party.isHealer(member1) + ? member1 + : data.party.isHealer(member2) + ? member2 + : data.party.isHealer(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not get priority + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ num: num, - mech1: output.tower!(), - mech2: nearFar, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, }); - // Support with spread on right tower 1 changes to left - if (data.myPathOfLights[0] === 'spread') - return output.mechs!({ + + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + mech3: nearFar, + }); + } + + if (Util.isMeleeDpsJob(data.job)) { + const isRangedDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); + }; + // Partner should be a ranged dps, for standard comp + const partner = isRangedDPS(member1) + ? member1 + : isRangedDPS(member2) + ? member2 + : isRangedDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find caster or phys ranged partner + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ num: num, - mech1: output.swapTowers!(), - mech2: nearFar, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, }); - } - if (data.myPathOfLights[0] === 'cone') - return output.mechs!({ + + return output.mechs3!({ num: num, - mech1: output.swapTowers!(), - mech2: nearFar, + mech1: output[marker]!(), + mech2: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + mech3: nearFar, }); - if (data.myPathOfLights[0] === 'spread') - return output.mechs!({ + } + + // If we find a melee in our group we are the ranged priority + // Partner should be a melee dps, for optimal comp + const isMeleeDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isMeleeDpsJob(jobName); + }; + const partner = isMeleeDPS(member1) + ? member1 + : isMeleeDPS(member2) + ? member2 + : isMeleeDPS(member3) + ? member3 + : 'unknown'; + // Get partner's marker + const pMarker = playerHeadmarkers[partner ?? 0]; + + // Could not find melee dps + if ( + partner === 'unknown' || + pMarker === undefined || + pMarker === 'unknown' + ) + return output.mechs3!({ num: num, - mech1: output.tower!(), - mech2: nearFar, + mech1: output[marker]!(), + mech2: output.tower!(), + mech3: nearFar, }); + + // Highest priority right + return output.mechs3!({ + num: num, + mech1: output[marker]!(), + mech2: output.rightTower!(), + mech3: nearFar, + }); } // ABBA (unmodified) and AAAABBBB, Soaks @@ -783,13 +876,13 @@ const triggerSet: TriggerSet = { }); } - // Spread Players have to be far in the tower, cones need to bait end - const nearFar = marker === 'spread' - ? output.beFar!() - : output.beNear!(); - // Tower Soaks if (config === 'kroxy-rinon' || config === 'abba') { + // Spread Players have to be far in the tower, cones need to bait end + const nearFar = marker === 'spread' + ? output.beFar!() + : output.beNear!(); + if (data.role === 'healer') { return output.mechs3!({ num: num, From 71ee9bcc9cd0de470d3febbf42bce8ca4c5149b4 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 11 Jun 2026 04:45:45 -0400 Subject: [PATCH 22/49] add trines triggers + adjust wings triggers --- .../data/07-dt/ultimate/dancing_mad.ts | 190 +++++++++++++++--- 1 file changed, 162 insertions(+), 28 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 8c330bcde90..75769cffd8b 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1,7 +1,7 @@ import Conditions from '../../../../../resources/conditions'; import Outputs from '../../../../../resources/outputs'; import { Responses } from '../../../../../resources/responses'; -import Util from '../../../../../resources/util'; +import Util, { Directions } from '../../../../../resources/util'; import ZoneId from '../../../../../resources/zone_id'; import { RaidbossData } from '../../../../../types/data'; import { OutputStrings, TriggerSet } from '../../../../../types/trigger'; @@ -14,8 +14,8 @@ const phases: { [id: string]: Phase } = { 'C3F7': 'p3', // Aero III Assault (from Kefka), Chaos and Exdeath }; -// const centerX = 100; -// const centerY = 100; +const centerX = 100; +const centerY = 100; type forsakenHeadmarker = 'cone' | 'spread' | 'stack' | 'unknown'; type forsakenHeadmarkerMap = { [key: string]: forsakenHeadmarker }; @@ -61,6 +61,8 @@ export interface Data extends RaidbossData { isForsakenGroupA: boolean; // Quick lookup for group check forsakenGroupA: string[]; // List of players in Group A forsakenGroupB: string[]; // List of players in Group B + trineDirNums: number[]; + middleTrineFacing?: 'east' | 'west'; } const headMarkerData = { @@ -370,6 +372,7 @@ const triggerSet: TriggerSet = { isForsakenGroupA: false, forsakenGroupA: [], forsakenGroupB: [], + trineDirNums: [], }; }, triggers: [ @@ -2801,6 +2804,107 @@ const triggerSet: TriggerSet = { netRegex: { id: 'BABD', source: 'Kefka', capture: false }, response: Responses.bigAoe('alert'), }, + { + id: 'DMU P2 Trine Collector', + // TODO: Get other two pattern coords + // Kefkabin solution: https://raidplan.io/plan/apkh6ytq72w8pt3v + // Trines are added ~0.5s after BADF Trine ability + // They have BNpcID 1EBFB3 and 1EBFB2. + // On release of Patch 7.51, the Northwest-ish trine is bugged (rotated 60 degrees) + // There are 3 patterns + // Pattern 1: + // Set 1: Northwest-ish(88.45, 90), South-ish (97.11, 115), North-ish(102.89, 85) + // Set 2: Southeast-ish (115.55, 110) + // Set 3: West-ish (85.57, 105), Middle (100,100)3*, East-ish(114.43, 95) + // + // Pattern 2: + // Set 1: + // Set 2: + // Set 3: + // + // Pattern 3: + // Set 1: + // Set 2: + // Set 3: + // + // * Guaranteed in set 3, and its heading points West or East + // + // 273 ActorControlExtra lines that follow: + // 019D|10|20 => Falling down animation? + // 019D|40|80 => Landed animation? (~1.4s after add) + // 019D|4|8 => Explosion animation? + // + // Trines starts with 3 Trines spawning, then 1, then 3 More + // BACD/BACE Wings of Destruction halfroom cleave happens while 3rd set is landing + // As Trines 1 detonate, the near/far tankbuster C487 Wings of Destruction begins casting + // At the 3rd detonation, the tankbuster will snapshot + type: 'CombatantMemory', + netRegex: { + change: 'Add', + pair: [{ key: 'BNpcID', value: ['1EBFB2', '1EBFB3'] }], + capture: true, + }, + run: (data, matches) => { + // Need heading of middle trine for near tank bait and/or greedy melee + // With exception of bugged 7.51 NW Trine rotated 60 degrees off, Heading is defined by the BNpcID + // 1EBFB3 => West + // 1EBFB2 => East + const x = parseFloat(matches.pairPosX ?? '0'); + const y = parseFloat(matches.pairPosY ?? '0'); + + // Exception for center trine + if (data.trineDirNums.length === 3) { + if (x > 99 && x < 101) { + data.middleTrineFacing = matches.pairBNpcID === '1EBFB2' ? 'west' : 'east'; + return; + } + } + + // Don't need the last set's x,y + if (data.trineDirNums.length !== 3) { + const dirNum = Directions.xyTo16DirNum(centerX, centerY, x, y); + data.trineDirNums.push(dirNum); + } + }, + }, + { + id: 'DMU P2 Trines 1 (Early)', + type: 'CombatantMemory', + netRegex: { + change: 'Add', + pair: [{ key: 'BNpcID', value: ['1EBFB2', '1EBFB3'] }], + capture: true, + }, + condition: (data) => data.trineDirNums.length === 3, + durationSeconds: 12, // Detonation occurs ~12.9s + suppressSeconds: 99999, + infoText: (data, _matches, output) => { + const dirNums = data.trineDirNums; + const sorted = dirNums.sort((a, b) => a - b); // Sorts clockwise + const trine1 = sorted[0] !== undefined + ? Directions.output16Dir[sorted[0]] ?? 'unknown' + : 'unknown'; + const trine2 = sorted[1] !== undefined + ? Directions.output16Dir[sorted[1]] ?? 'unknown' + : 'unknown'; + const trine3 = sorted[2] !== undefined + ? Directions.output16Dir[sorted[2]] ?? 'unknown' + : 'unknown'; + + return output.safeSpots!({ + dir1: output[trine1]!(), + dir2: output[trine2]!(), + dir3: output[trine3]!(), + }); + }, + outputStrings: { + ...Directions.outputStrings16Dir, + unknown: Outputs.unknown, + safeSpots: { + en: '${dir1}/${dir2}/${dir3} Later', + }, + }, + }, { id: 'DMU P2 Single Wing of Destruction', // BACD Wings of Destruction, Left wing highlight @@ -2820,33 +2924,63 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P2 Wings of Destruction', + // In DMU, players need to move for trines at same time as the tankbuster call + // Melee most likely won't be able to hit the boss due to trine aoes type: 'StartsUsing', netRegex: { id: 'C487', source: 'Kefka', capture: false }, - response: (data, _matches, output) => { - // cactbot-builtin-response - output.responseOutputStrings = { - maxMeleeAvoidTanks: { - en: 'Max Melee: Avoid Tanks', - de: 'Max Nahkampf: Weg von den Tanks', - fr: 'Max mêlée : éloignez-vous des tanks', - ja: '近接最大レンジ タンクから離れる', - cn: '最大近战距离,避开坦克', - ko: '칼끝딜: 탱커 피하기', - tc: '最大近戰距離,避開坦克', - }, - wingsBeNearFar: { - en: 'Wings: Be Near/Far', - de: 'Schwingen: Nah/Fern', - fr: 'Ailes : Placez-vous près/loin', - ja: '翼: めり込む/離れる', - cn: '双翅膀:近或远', - ko: '양날개: 가까이/멀리', - tc: '雙翅膀:近或遠', - }, - }; - if (data.role === 'tank') - return { alertText: output.wingsBeNearFar!() }; - return { infoText: output.maxMeleeAvoidTanks!() }; + alertText: (data, _matches, output) => { + const dirNums = data.trineDirNums; + const sorted = dirNums.sort((a, b) => a - b); // Sorts clockwise + const trine1 = sorted[0] !== undefined + ? Directions.output16Dir[sorted[0]] ?? 'unknown' + : 'unknown'; + const trine2 = sorted[1] !== undefined + ? Directions.output16Dir[sorted[1]] ?? 'unknown' + : 'unknown'; + const trine3 = sorted[2] !== undefined + ? Directions.output16Dir[sorted[2]] ?? 'unknown' + : 'unknown'; + + return output.dirWings!({ + dirs: output.safeSpots!({ + dir1: output[trine1]!(), + dir2: output[trine2]!(), + dir3: output[trine3]!(), + }), + wings: data.role !== 'tank' + ? output.wingsParty!() + : data.middleTrineFacing + ? output.wingsTrine!({ + wings: output.wingsTank!(), + trine: output[data.middleTrineFacing]!() + }) + : output.wingsTank!(), + }); + }, + outputStrings: { + ...Directions.outputStrings16Dir, + unknown: Outputs.unknown, + safeSpots: { + en: '${dir1}/${dir2}/${dir3}', + }, + wingsTrine: { + en: '${wings} + ${trine}', + }, + dirWings: { + en: '${dirs} + ${wings}' + }, + wingsParty: { + en: 'Outer 2 Rings', + }, + wingsTank: { + en: 'Be Near/Far', + }, + east: { + en: 'Eastward Trine', + }, + west: { + en: 'Westward Trine', + }, }, }, { From 4e48003d7ddd702101bec29c6215f3081547ac97 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 11 Jun 2026 04:48:39 -0400 Subject: [PATCH 23/49] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 75769cffd8b..0b66a363186 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2952,7 +2952,7 @@ const triggerSet: TriggerSet = { : data.middleTrineFacing ? output.wingsTrine!({ wings: output.wingsTank!(), - trine: output[data.middleTrineFacing]!() + trine: output[data.middleTrineFacing]!(), }) : output.wingsTank!(), }); @@ -2967,7 +2967,7 @@ const triggerSet: TriggerSet = { en: '${wings} + ${trine}', }, dirWings: { - en: '${dirs} + ${wings}' + en: '${dirs} + ${wings}', }, wingsParty: { en: 'Outer 2 Rings', From bd4ca1521a297380407a4469685873fe711493a3 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 11 Jun 2026 04:53:31 -0400 Subject: [PATCH 24/49] 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 0b66a363186..af120c60ad3 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2873,7 +2873,7 @@ const triggerSet: TriggerSet = { netRegex: { change: 'Add', pair: [{ key: 'BNpcID', value: ['1EBFB2', '1EBFB3'] }], - capture: true, + capture: false, }, condition: (data) => data.trineDirNums.length === 3, durationSeconds: 12, // Detonation occurs ~12.9s From 7cd320b961301e199bd01026ee9a56d043c32b07 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 11 Jun 2026 04:56:14 -0400 Subject: [PATCH 25/49] comment update --- 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 af120c60ad3..258bc7915b0 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2860,7 +2860,7 @@ const triggerSet: TriggerSet = { } } - // Don't need the last set's x,y + // Not storing the last two sets' x,y coords if (data.trineDirNums.length !== 3) { const dirNum = Directions.xyTo16DirNum(centerX, centerY, x, y); data.trineDirNums.push(dirNum); From 4d4dd1c7fe5db320a0def13350f0a482d89bab2d Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 12 Jun 2026 18:49:21 -0400 Subject: [PATCH 26/49] rename outputs for clarity --- .../data/07-dt/ultimate/dancing_mad.ts | 274 +++++++++--------- 1 file changed, 139 insertions(+), 135 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 8f975d74602..6f0db337f69 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -280,14 +280,14 @@ const forsakenOutputStrings: OutputStrings = { stackOnYouTower: { // Used in first tower only en: '${num}${tower} + ${marker}', }, - swapTowers: { // Used in second tower only - en: '${num}Swap Towers', - }, markerOnYouStacksOnPlayers: { // Used only for first tower en: '${num}${marker} + ${stacks}', }, - markerOnYouTower: { // Used for Cone or Spread - en: '${num}${marker} + ${tower}', + markerOnYouTowerOdds: { // Used for Cone or Spread (Stack gets separate output) + en: '${num}${marker} + ${tower} + ${far}', + }, + markerOnYouTowerEvens: { // Used for Cones + Spreads (no stacks taking the towers) + en: '${num}${marker} + ${tower} + ${nearfar}', }, baitLeftConeOutOdds: { en: '${num}Bait Left Cone Out', @@ -301,12 +301,6 @@ const forsakenOutputStrings: OutputStrings = { rightStack: { en: '${num}Right Stack + ${avoid}', }, - mechs: { - en: '${num}${mech1} + ${mech2}', - }, - mechs3: { - en: '${num}${mech1} + ${mech2} + ${mech3}', - }, bait: { en: '${num}Bait Cone Right or Clone Near', }, @@ -319,6 +313,12 @@ const forsakenOutputStrings: OutputStrings = { baitCloneOppositeTowers: { en: '${num}Bait Clone Opposite Towers Near', }, + mechsBowtie: { + en: '${num}${mech1} + ${mech2}', + }, + mechs3Bowtie: { + en: '${num}${mech1} + ${mech2} + ${mech3}', + }, numBeNearSpreadBowtie: { en: '${num}${near} + ${spread}', }, @@ -1588,7 +1588,7 @@ const triggerSet: TriggerSet = { if (config === 'bowtie' && !data.isForsakenGroupA) { // Group A Avoids Towers (ABBA) // Group B Avoids Towers (AAAABBBB) - return output.mechs!({ + return output.mechsBowtie!({ num: num, mech1: output.beNear!(), mech2: output.avoid!(), @@ -1613,11 +1613,11 @@ const triggerSet: TriggerSet = { : output.beNear!(); if (data.role === 'healer') { - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.leftTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: nearFar, }); } @@ -1645,20 +1645,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.rightTower!() : output.leftTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -1688,20 +1688,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.leftTower!() : output.rightTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -1731,19 +1731,19 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); // Highest priority right - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.rightTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: nearFar, }); } @@ -1761,14 +1761,14 @@ const triggerSet: TriggerSet = { : group[2]; // Or unknown matched const name = data.party.member(partner); if (marker === 'spread') - return output.mechs3!({ + return output.mechs3Bowtie!({ num: num, mech1: output.rightTower!(), mech2: output.spreadWithPlayer!({ player: name }), mech3: output.outOfHitbox!(), }); if (marker === 'cone') - return output.mechs3!({ + return output.mechs3Bowtie!({ num: num, mech1: output.leftTower!(), mech2: output.baitConeFromPlayer!({ player: name }), @@ -1884,12 +1884,13 @@ const triggerSet: TriggerSet = { ) || (!isForsakenGroupA && config === 'abba') ) { - return output.markerOnYouTower!({ + return output.markerOnYouTowerOdds!({ num: num, marker: output[marker]!(), tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), + far: output.beFar!(), }); } @@ -1974,7 +1975,7 @@ const triggerSet: TriggerSet = { // AAAABBBB, Baits if (config === 'bowtie' && !isForsakenGroupA) { // Group B Avoids Towers - return output.mechs!({ + return output.mechsBowtie!({ num: num, mech1: output.beNear!(), mech2: output.avoid!(), @@ -2001,14 +2002,14 @@ const triggerSet: TriggerSet = { : group[2]; // Or unknown matched const name = data.party.member(partner); if (marker === 'spread') - return output.mechs3!({ + return output.mechs3Bowtie!({ num: num, mech1: output.rightTower!(), mech2: output.spreadWithPlayer!({ player: name }), mech3: output.outOfHitbox!(), }); if (marker === 'cone') - return output.mechs3!({ + return output.mechs3Bowtie!({ num: num, mech1: output.leftTower!(), mech2: output.baitConeFromPlayer!({ player: name }), @@ -2024,11 +2025,11 @@ const triggerSet: TriggerSet = { : output.beNear!(); if (data.role === 'healer') { - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.leftTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: nearFar, }); } @@ -2056,20 +2057,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.rightTower!() : output.leftTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -2099,20 +2100,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.leftTower!() : output.rightTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -2142,19 +2143,19 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); // Highest priority right - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.rightTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: nearFar, }); } @@ -2212,12 +2213,13 @@ const triggerSet: TriggerSet = { if (config === 'bowtie') { // Bowtie has people bait cones, but cones could bait eachother if they wanted if (!isForsakenGroupA) { - return output.markerOnYouTower!({ + return output.markerOnYouTowerOdds!({ num: num, marker: output[marker]!(), tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), + far: output.beFar!(), }); } if (data.role === 'tank') @@ -2256,12 +2258,13 @@ const triggerSet: TriggerSet = { // This ends up being Group B || Group A for respective config if (config === 'kroxy-rinon' || config === 'abba') { - return output.markerOnYouTower!({ + return output.markerOnYouTowerOdds!({ num: num, marker: output[marker]!(), tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), + far: output.beFar!(), }); } @@ -2341,14 +2344,14 @@ const triggerSet: TriggerSet = { : group[2]; // Or unknown matched const name = data.party.member(partner); if (marker === 'spread') - return output.mechs3!({ + return output.mechs3Bowtie!({ num: num, mech1: output.rightTower!(), mech2: output.spreadWithPlayer!({ player: name }), mech3: output.outOfHitbox!(), }); if (marker === 'cone') - return output.mechs3!({ + return output.mechs3Bowtie!({ num: num, mech1: output.leftTower!(), mech2: output.baitConeFromPlayer!({ player: name }), @@ -2364,11 +2367,11 @@ const triggerSet: TriggerSet = { : output.beNear!(); if (data.role === 'healer') { - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.leftTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: nearFar, }); } @@ -2396,20 +2399,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.rightTower!() : output.leftTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -2439,20 +2442,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.leftTower!() : output.rightTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -2482,19 +2485,19 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); // Highest priority right - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.rightTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: nearFar, }); } @@ -2571,12 +2574,13 @@ const triggerSet: TriggerSet = { // Cone/Stack Tower Soaks // Group B if (config !== 'none') - return output.markerOnYouTower!({ + return output.markerOnYouTowerOdds!({ num: num, marker: output[marker]!(), tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), + far: output.beFar!(), }); // No strategy @@ -2632,11 +2636,11 @@ const triggerSet: TriggerSet = { : output.beNear!(); if (data.role === 'healer') { - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.leftTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: nearFar, }); } @@ -2664,20 +2668,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.rightTower!() : output.leftTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -2707,20 +2711,20 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: pMarker === marker + marker: output[marker]!(), + tower: pMarker === marker ? output.leftTower!() : output.rightTower!(), - mech3: nearFar, + nearfar: nearFar, }); } @@ -2750,19 +2754,19 @@ const triggerSet: TriggerSet = { pMarker === undefined || pMarker === 'unknown' ) - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.tower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.tower!(), + nearfar: nearFar, }); // Highest priority right - return output.mechs3!({ + return output.markerOnYouTowerEvens!({ num: num, - mech1: output[marker]!(), - mech2: output.rightTower!(), - mech3: nearFar, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: nearFar, }); } From bac43fc90cda0b4ccf90ecaa5db9cb80d67816ae Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 12 Jun 2026 18:53:48 -0400 Subject: [PATCH 27/49] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 6f0db337f69..5cb07dddfb0 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2264,7 +2264,7 @@ const triggerSet: TriggerSet = { tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), - far: output.beFar!(), + far: output.beFar!(), }); } @@ -2580,7 +2580,7 @@ const triggerSet: TriggerSet = { tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), - far: output.beFar!(), + far: output.beFar!(), }); // No strategy From d88fc1c221c26a3e3f7b43411fb89fb41e2ba2e1 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 12 Jun 2026 19:23:10 -0400 Subject: [PATCH 28/49] Add tower or bait/stack to all things ending --- .../data/07-dt/ultimate/dancing_mad.ts | 61 +++++++++++++++++-- 1 file changed, 56 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 5cb07dddfb0..8db3056a73c 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1802,14 +1802,59 @@ const triggerSet: TriggerSet = { delaySeconds: 1.2, // Time until headmarker and future/past damage alertText: (data, matches, output) => { const isFuture = matches.id === 'BAD2'; - if (data.pathOfLightCounter !== 9) - return isFuture ? output.future!() : output.past!(); + const count = data.pathOfLightCounter; + const config = data.triggerSetConfig.forsaken; + const isForsakenGroupA = data.isForsakenGroupA; + + const time = isFuture ? output.future!() : output.past!(); + if (count === 3) { + if (config === 'kroxy-rinon' || config === 'bowtie') + return output.baitThenMech!({ + bait: time, + mech: isForsakenGroupA + ? output.tower!() + : output.baitOrStack!(), + }); + if (config === 'abba') + return output.baitThenMech!({ + bait: time, + mech: isForsakenGroupA + ? output.baitOrStack!() + : output.tower!(), + }); + } else if (count === 5) { + if (config === 'abba') + return output.baitThenMech!({ + bait: time, + mech: isForsakenGroupA + ? output.tower!() + : output.baitOrStack!(), + }); + if (config === 'kroxy-rinon' || config === 'bowtie') + return output.baitThenMech!({ + bait: time, + mech: isForsakenGroupA + ? output.baitOrStack!() + : output.tower!(), + }); + } else if (count === 7) { + if (config !== 'none') + return output.baitThenMech!({ + bait: time, + mech: isForsakenGroupA + ? output.baitOrStack!() + : output.tower!(), + }); + } else + return isFuture + ? output.lastFuture!({ action: output.behind!() }) + : output.lastPast!({ action: output.stay!() }); - return isFuture - ? output.lastFuture!({ action: output.behind!() }) - : output.lastPast!({ action: output.stay!() }); + // No Strategy + return time; }, outputStrings: { + tower: Outputs.getTowers, behind: Outputs.getBehind, stay: { en: 'Stay', @@ -1819,12 +1864,18 @@ const triggerSet: TriggerSet = { ko: '대기', tc: '停', }, + baitOrStack: { + en: 'Bait/Stack', + }, future: { en: 'Bait Ending opposite Towers', }, past: { en: 'Bait Ending between Towers', }, + baitThenMech: { + en: '${bait} => ${mech}', + }, lastFuture: { en: 'Bait Ending => ${action}', }, From 2225769003cbd6758906adb84f11c8dc27f0fc62 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 12 Jun 2026 19:25:29 -0400 Subject: [PATCH 29/49] remove avoidTowers from stack calls --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 10 ++-------- 1 file changed, 2 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 8db3056a73c..f6f9154e2d7 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -296,10 +296,10 @@ const forsakenOutputStrings: OutputStrings = { en: '${num}Bait Left Cone Left', }, leftStack: { - en: '${num}Left Stack + ${avoid}', + en: '${num}Left Stack', }, rightStack: { - en: '${num}Right Stack + ${avoid}', + en: '${num}Right Stack', }, bait: { en: '${num}Bait Cone Right or Clone Near', @@ -1956,7 +1956,6 @@ const triggerSet: TriggerSet = { if (data.role === 'tank') return output.leftStack!({ num: num, - avoid: output.avoid!(), }); if (data.role === 'healer') return output.baitLeftConeOutOdds!({ @@ -1965,7 +1964,6 @@ const triggerSet: TriggerSet = { // 2 DPS in stack return output.rightStack!({ num: num, - avoid: output.avoid!(), }); } @@ -2249,7 +2247,6 @@ const triggerSet: TriggerSet = { if (data.role === 'tank') return output.leftStack!({ num: num, - avoid: output.avoid!(), }); if (data.role === 'healer') return output.baitLeftConeOutOdds!({ @@ -2257,7 +2254,6 @@ const triggerSet: TriggerSet = { }); return output.rightStack!({ num: num, - avoid: output.avoid!(), }); } @@ -2590,7 +2586,6 @@ const triggerSet: TriggerSet = { if (data.role === 'tank') return output.leftStack!({ num: num, - avoid: output.avoid!(), }); if (data.role === 'healer') return output.baitLeftConeOutOdds!({ @@ -2598,7 +2593,6 @@ const triggerSet: TriggerSet = { }); return output.rightStack!({ num: num, - avoid: output.avoid!(), }); } From c5c08c1f927ef8b091a71cfa7a15e07d1358d386 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 12 Jun 2026 19:29:28 -0400 Subject: [PATCH 30/49] 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 f6f9154e2d7..2a938a83414 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1823,7 +1823,7 @@ const triggerSet: TriggerSet = { : output.tower!(), }); } else if (count === 5) { - if (config === 'abba') + if (config === 'abba') return output.baitThenMech!({ bait: time, mech: isForsakenGroupA From 88d0b39e516074b883d2b27b02aa717cb0858c9a Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 12 Jun 2026 21:23:08 -0400 Subject: [PATCH 31/49] fix last stack left in array --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 +++- 1 file changed, 3 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 2a938a83414..8ea005dbde5 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1387,7 +1387,9 @@ const triggerSet: TriggerSet = { type: 'LosesEffect', netRegex: { effectId: '13DB', capture: true }, run: (data, matches) => { - delete data.forsakenPlayerHeadmarkers[matches.target]; + const target = matches.target; + data.pathOfLightStackPlayers = data.pathOfLightStackPlayers.filter((t) => t !== target); + delete data.forsakenPlayerHeadmarkers[target]; }, }, { From 94d06c135e6f36b2b3b7e815f1bdded79de47aa5 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sat, 13 Jun 2026 20:49:26 -0400 Subject: [PATCH 32/49] add location to first stack call --- .../data/07-dt/ultimate/dancing_mad.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 8ea005dbde5..9b6261c7271 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -234,6 +234,12 @@ const forsakenOutputStrings: OutputStrings = { tc: '遠離塔', }, outOfHitbox: Outputs.outOfHitbox, + innerHitbox: { + en: 'Inner Hitbox', + }, + outerHitbox: { + en: 'Outer Hitbox', + }, cone: { en: 'Cone on YOU', }, @@ -268,6 +274,9 @@ const forsakenOutputStrings: OutputStrings = { ko: '멀리 있기', }, stackOnYou: Outputs.stackOnYou, + stackOnYouLocation: { // Used only in first tower + en: '${stack} ${location}', + }, stackOnPlayer: { // Used only in first tower (role-based) en: 'Stack is on ${player}', }, @@ -1482,12 +1491,18 @@ const triggerSet: TriggerSet = { return output.stackOnYouTower!({ num: num, tower: output.leftTower!(), - marker: output.stackOnYou!(), + marker: output.stackOnYouLocation!({ + stack: output.stackOnYou!(), + location: output.outerHitbox!(), + }), }); return output.stackOnYouTower!({ num: num, tower: output.rightTower!(), - marker: output.stackOnYou!(), + marker: output.stackOnYouLocation!({ + stack: output.stackOnYou!(), + location: output.innerHitbox!(), + }), }); } From e7bd43eb567933ef8761ee19acbfc9cbbbd8e3eb Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 14 Jun 2026 06:52:18 -0400 Subject: [PATCH 33/49] fix trines: parameters were mixed --- .../data/07-dt/ultimate/dancing_mad.ts | 27 ++++++++++--------- 1 file changed, 15 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 cbb71eb9a6a..5a2abb0cd82 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2924,27 +2924,30 @@ const triggerSet: TriggerSet = { }, { id: 'DMU P2 Trine Collector', - // TODO: Get other two pattern coords // Kefkabin solution: https://raidplan.io/plan/apkh6ytq72w8pt3v // Trines are added ~0.5s after BADF Trine ability // They have BNpcID 1EBFB3 and 1EBFB2. - // On release of Patch 7.51, the Northwest-ish trine is bugged (rotated 60 degrees) - // There are 3 patterns // Pattern 1: // Set 1: Northwest-ish(88.45, 90), South-ish (97.11, 115), North-ish(102.89, 85) // Set 2: Southeast-ish (115.55, 110) - // Set 3: West-ish (85.57, 105), Middle (100,100)3*, East-ish(114.43, 95) + // Set 3: West-ish (85.57, 105), Middle (100,100)*, East-ish(114.43, 95) // // Pattern 2: - // Set 1: - // Set 2: - // Set 3: + // Set 1: Southeast-ish(111.55, 110), South-ish (97.11, 115) East-ish (114.43,95) + // Set 2: North-ish (102.89, 85) + // Set 3: West-ish (85.57, 105), Northwest-ish(88.45, 90), Middle (100, 100)* // // Pattern 3: - // Set 1: - // Set 2: - // Set 3: + // Set 1: South-ish (97.11, 115), Southeast-ish (111.55, 110), East-ish (114.43, 95) + // Set 2: Northwest-ish (88.45, 90) + // Set 3: West-ish (85.57, 105), Middle (100, 100)*, North-ish (102.89, 85) // + // Pattern 4: + // Set 1: Northwest-ish(88.45, 90), South-ish (97.11, 115), North-ish(102.89, 85) + // Set 2: East-ish (114.43, 95) + // Set 3: West-ish (85.57, 105), Middle (100,100)*, Southeast-ish (111.55, 110) + // + // There's probably more patterns // * Guaranteed in set 3, and its heading points West or East // // 273 ActorControlExtra lines that follow: @@ -2964,7 +2967,7 @@ const triggerSet: TriggerSet = { }, run: (data, matches) => { // Need heading of middle trine for near tank bait and/or greedy melee - // With exception of bugged 7.51 NW Trine rotated 60 degrees off, Heading is defined by the BNpcID + // Heading is defined by the BNpcID // 1EBFB3 => West // 1EBFB2 => East const x = parseFloat(matches.pairPosX ?? '0'); @@ -2980,7 +2983,7 @@ const triggerSet: TriggerSet = { // Not storing the last two sets' x,y coords if (data.trineDirNums.length !== 3) { - const dirNum = Directions.xyTo16DirNum(centerX, centerY, x, y); + const dirNum = Directions.xyTo16DirNum(x, y, centerX, centerY); data.trineDirNums.push(dirNum); } }, From 81e1bc4c3843c9bbe28233083f5f3e09bf283712 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 14 Jun 2026 07:02:54 -0400 Subject: [PATCH 34/49] missing bracket --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 59268b0eaa9..6b9ba60738a 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1901,6 +1901,7 @@ const triggerSet: TriggerSet = { }, lastPast: { en: 'Bait Ending => ${action}', + }, }, }, { From 8049c9fb5aafc4336dd29ab797def2e06900550f Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 14 Jun 2026 07:05:30 -0400 Subject: [PATCH 35/49] lint --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 6b9ba60738a..d2237ef71cd 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2933,7 +2933,7 @@ const triggerSet: TriggerSet = { // Set 3: West-ish (85.57, 105), Middle (100,100)*, East-ish(114.43, 95) // // Pattern 2: - // Set 1: Southeast-ish(111.55, 110), South-ish (97.11, 115) East-ish (114.43,95) + // Set 1: Southeast-ish(111.55, 110), South-ish (97.11, 115) East-ish (114.43,95) // Set 2: North-ish (102.89, 85) // Set 3: West-ish (85.57, 105), Northwest-ish(88.45, 90), Middle (100, 100)* // @@ -2946,7 +2946,7 @@ const triggerSet: TriggerSet = { // Set 1: Northwest-ish(88.45, 90), South-ish (97.11, 115), North-ish(102.89, 85) // Set 2: East-ish (114.43, 95) // Set 3: West-ish (85.57, 105), Middle (100,100)*, Southeast-ish (111.55, 110) - // + // // There's probably more patterns // * Guaranteed in set 3, and its heading points West or East // From 744af8aea41b6456290c02219d1996d59bdac4fe Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 15 Jun 2026 07:39:13 -0400 Subject: [PATCH 36/49] add tower/marker info to ending baits --- .../data/07-dt/ultimate/dancing_mad.ts | 296 +++++++++++++++--- 1 file changed, 260 insertions(+), 36 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index d2237ef71cd..5967a82749d 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: P1 Tele-Portent configuration options +// TODO: P2 Old AAAABBBB plan was found at https://raidplan.io/plan/kj2d734d36es2ugs, would like to find replacement // TODO: Earlier phase tracking for P5 (counting the jumps to middle?) type Phase = 'p1' | 'p2' | 'p3' | 'p4' | 'p5'; @@ -370,7 +371,7 @@ const triggerSet: TriggerSet = { en: `There should be two groups of four players, choose tower soak order.
Kroxy-Rinon 3/4/1: Kefka Bin
Modified ABBA: Raidplan
- Bowtie: Raidplan (Will require Tank LB3)
+ Bowtie AAAABBBB 4/4: Using same priority as the kroxy-rinon. (Will require Tank LB3)
Default will be Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.`, }, name: { @@ -380,8 +381,8 @@ const triggerSet: TriggerSet = { options: { en: { 'AAABBBBA (3/4/1), Kroxy-Rinon': 'kroxy-rinon', - 'ABBAABBA (1/2/2/2/1) Modified': 'abba', - 'AAAABBBB (4/4) Bowtie': 'bowtie', + 'ABBAABBA (1/2/2/2/1), Modified': 'abba', + 'AAAABBBB (4/4), Bowtie': 'bowtie', 'Generic calls.': 'none', }, }, @@ -1819,63 +1820,250 @@ const triggerSet: TriggerSet = { // TODO: Get Tower Locations type: 'Ability', netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, - delaySeconds: 1.2, // Time until headmarker and future/past damage + delaySeconds: 1.3, // Time until headmarker and future/past damage alertText: (data, matches, output) => { const isFuture = matches.id === 'BAD2'; const count = data.pathOfLightCounter; + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + const marker = playerHeadmarkers[data.me] ?? 'unknown'; // Current headmarker const config = data.triggerSetConfig.forsaken; const isForsakenGroupA = data.isForsakenGroupA; const time = isFuture ? output.future!() : output.past!(); if (count === 3) { - if (config === 'kroxy-rinon' || config === 'bowtie') - return output.baitThenMech!({ + // Stacks should soak towers + if (marker === 'stack') { + if ( + ( + isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') + ) || + (!isForsakenGroupA && config === 'abba') || + (config === 'none') + ) { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Assuming none config soaks + return output.baitThenStacks!({ + bait: time, + stacks: output.stacksOnPlayers!({ players: msg }), + }); + } + } + + // Tower soakers, non stack markers + if ( + ( + isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') + ) || + (!isForsakenGroupA && config === 'abba') + ) { + return output.baitThenMarkerTower!({ bait: time, - mech: isForsakenGroupA - ? output.tower!() - : output.baitOrStack!(), + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), }); - if (config === 'abba') + } + + // Baits and Stacks + if ( + ( + !isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') + ) || + (isForsakenGroupA && config === 'abba') + ) { + // So long as it is standard party composition... + if (data.role === 'tank') + return output.baitThenMech!({ + bait: time, + mech: output.leftStack!(), + }); + if (data.role === 'healer') + return output.baitThenMech!({ + bait: time, + mech: output.leftBaitOut!(), + }); + // 2 DPS in stack return output.baitThenMech!({ bait: time, - mech: isForsakenGroupA - ? output.baitOrStack!() - : output.tower!(), + mech: output.rightStack!(), }); + } + + // No config + return output.baitThenMarker!({ + bait: time, + marker: output[marker]!(), + }); } else if (count === 5) { - if (config === 'abba') + // Baits and Stacks + if ( + (isForsakenGroupA && config === 'kroxy-rinon') || + (!isForsakenGroupA && config === 'abba') + ) { + // So long as it is standard party composition... + if (data.role === 'tank') + return output.baitThenMech!({ + bait: time, + mech: output.leftStack!(), + }); + if (data.role === 'healer') + return output.baitThenMech!({ + bait: time, + mech: output.leftBaitOut!(), + }); + // 2 DPS in stack return output.baitThenMech!({ bait: time, - mech: isForsakenGroupA - ? output.tower!() - : output.baitOrStack!(), + mech: output.rightStack!(), }); - if (config === 'kroxy-rinon' || config === 'bowtie') + } + + if (config === 'bowtie') { + // Bowtie has people bait cones, but cones could bait eachother if they wanted + if (!isForsakenGroupA) { + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), + }); + } + if (data.role === 'tank') + return output.baitThenMech!({ + bait: time, + mech: output.leftBaitLeftBowtie!(), + }); + if (data.role === 'healer') + return output.baitThenMech!({ + bait: time, + mech: output.leftBaitOutBowtie!(), + }); + // 2 DPS in spread return output.baitThenMech!({ bait: time, - mech: isForsakenGroupA - ? output.baitOrStack!() - : output.tower!(), + mech: output.getHitRightSpreadBowtie!(), + }); + } + + // Tower Soaks + // In AAAABBBB, there is no stack + if (marker === 'stack') { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Assuming none config soaks + return output.baitThenStacks!({ + bait: time, + stacks: output.stacksOnPlayers!({ players: msg }), + }); + } + + // This ends up being Group B || Group A for respective config + if (config === 'kroxy-rinon' || config === 'abba') { + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), }); + } + + // No config + return output.baitThenMarker!({ + bait: time, + marker: output[marker]!(), + }); } else if (count === 7) { - if (config !== 'none') - return output.baitThenMech!({ + if (config !== 'none') { + if (isForsakenGroupA) { + // So long as it is standard party composition... + if (data.role === 'tank') + return output.baitThenMech!({ + bait: time, + mech: output.leftStack!(), + }); + if (data.role === 'healer') + return output.baitThenMech!({ + bait: time, + mech: output.leftBaitOut!(), + }); + // 2 DPS in stack + return output.baitThenMech!({ + bait: time, + mech: output.rightStack!(), + }); + } + if (marker === 'stack') { + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Assuming none config soaks + return output.baitThenStacks!({ + bait: time, + stacks: output.stacksOnPlayers!({ players: msg }), + }); + } + + return output.baitThenMarkerTower!({ bait: time, - mech: isForsakenGroupA - ? output.baitOrStack!() - : output.tower!(), + marker: output[marker]!(), + tower: marker === 'cone' + ? output.leftTower!() + : output.rightTower!(), }); - } else - return isFuture - ? output.lastFuture!({ action: output.behind!() }) - : output.lastPast!({ action: output.stay!() }); + } - // No Strategy - return time; + // No config + return output.baitThenMarker!({ + bait: time, + marker: output[marker]!(), + }); + } + return isFuture + ? output.lastFuture!({ action: output.behind!() }) + : output.lastPast!({ action: output.stay!() }); }, outputStrings: { tower: Outputs.getTowers, behind: Outputs.getBehind, + cone: { + en: 'Cone on YOU', + }, + spread: { + en: 'Spread on YOU', + }, + you: { + en: 'YOU', + }, + stacksOnPlayers: { + en: 'Stacks on ${players}', + }, stay: { en: 'Stay', de: 'Bleib stehen', @@ -1884,23 +2072,59 @@ const triggerSet: TriggerSet = { ko: '대기', tc: '停', }, + leftTower: { + en: 'Left Tower', + }, + rightTower: { + en: 'Right Tower', + }, + leftStack: { + en: 'Left Stack', + }, + rightStack: { + en: 'Right Stack', + }, + leftBaitOut: { + en: 'Left Bait Out', + }, baitOrStack: { en: 'Bait/Stack', }, future: { - en: 'Bait Ending opposite Towers', + en: 'Bait opposite Towers', }, past: { - en: 'Bait Ending between Towers', + en: 'Bait between Towers', + }, + baitThenMarker: { + en: '${bait} => ${marker}', }, baitThenMech: { en: '${bait} => ${mech}', }, + baitThenMarkerTower: { + en: '${bait} => ${marker} ${tower}', + }, + baitThenTower: { + en: '${bait} => ${tower}', + }, + baitThenStacks: { + en: '${bait} => ${stacks}', + }, lastFuture: { - en: 'Bait Ending => ${action}', + en: 'Bait => ${action}', }, lastPast: { - en: 'Bait Ending => ${action}', + en: 'Bait => ${action}', + }, + getHitRightSpreadBowtie: { + en: 'Hit by Right Spread' + }, + leftBaitLeftBowtie: { + en: 'Left Bait Left', + }, + leftBaitOutBowtie: { + en: 'Left Bait Out', }, }, }, From 1e2e9f91130b158fe167e32366fb6891efa19842 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 15 Jun 2026 08:01:36 -0400 Subject: [PATCH 37/49] 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 5967a82749d..0b97264f4d2 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2118,7 +2118,7 @@ const triggerSet: TriggerSet = { en: 'Bait => ${action}', }, getHitRightSpreadBowtie: { - en: 'Hit by Right Spread' + en: 'Hit by Right Spread', }, leftBaitLeftBowtie: { en: 'Left Bait Left', From 95e22e3106220b7831ad3aeeaef9151c3e9f4150 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Mon, 15 Jun 2026 22:09:20 -0400 Subject: [PATCH 38/49] add more delay for headmarker --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index e819030aa36..d0b970dd70d 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -404,7 +404,7 @@ const triggerSet: TriggerSet = { 'AAABBBBA (3/4/1), Kroxy-Rinon': 'kroxy-rinon', 'ABBAABBA (1/2/2/2/1), Modified': 'abba', 'AAAABBBB (4/4), Bowtie': 'bowtie', - 'Generic calls.': 'none', + 'Generic calls': 'none', }, }, default: 'none', @@ -1960,7 +1960,7 @@ const triggerSet: TriggerSet = { // TODO: Get Tower Locations type: 'Ability', netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, - delaySeconds: 1.3, // Time until headmarker and future/past damage + delaySeconds: 1.4, // Time until headmarker and future/past damage alertText: (data, matches, output) => { const isFuture = matches.id === 'BAD2'; const count = data.pathOfLightCounter; From 1fbbdf1df9a54f773e70796050ca64dbca343e86 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Tue, 16 Jun 2026 22:43:26 -0400 Subject: [PATCH 39/49] add past/future early call Since there is more time now, this trigger can fulfill a different purpose by being an early info. --- .../data/07-dt/ultimate/dancing_mad.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index d0b970dd70d..4998b20a1bb 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -1950,6 +1950,26 @@ const triggerSet: TriggerSet = { }, outputStrings: forsakenOutputStrings, }, + { + id: 'DMU P2 Future\'s End/Past\'s End (Early)', + // There are four end casts + // This output will need to be short as in 1.4s another trigger will fire + type: 'StartsUsing', + netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, + infoText: (_data, matches, output) => { + return matches.id === 'BAD2' ? output.future!() : output.past!(); + }, + outputStrings: { + future: { + en: 'Future', + ko: '미래', + }, + past: { + en: 'Past', + ko: '과거', + }, + }, + }, { id: 'DMU P2 All Things Ending Baits', // Using the following spells for timing: From 3ab5a536a4a22220790682db3009324adcfb0db0 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 18 Jun 2026 02:21:09 -0400 Subject: [PATCH 40/49] followup trigger when future is last --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 11 ++++++++++- 1 file changed, 10 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 432bc02503e..9bba5d15cd7 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2088,7 +2088,7 @@ const triggerSet: TriggerSet = { { id: 'DMU P2 All Things Ending Baits', // Using the following spells for timing: - // BAD2 Future's End => Need to bait BACD All Things Ending + // BAD2 Future's End => Need to bait BADC All Things Ending // BAD3 Past's End => Need to bait BADD All Things Ending // There are four end casts, each 10s apart // BAD2 and BAD3 are the castbar, damage doesn't go out until later @@ -3415,6 +3415,15 @@ const triggerSet: TriggerSet = { }, }, }, + { + id: 'DMU P2 Last All Things Ending Followup', + // BADC All Things Ending (Future) + type: 'StartsUsing', + netRegex: { id: 'BADC', source: 'Kefka', capture: false }, + condition: (data) => data.pathOfLightCounter === 9, + suppressSeconds: 1, + response: Responses.getBehind('alert'), + }, { id: 'DMU P2 Light of Judgment', type: 'StartsUsing', From 26c2ff243dfab79f31115a479cada21daf9bf8ee Mon Sep 17 00:00:00 2001 From: Legends0 Date: Thu, 18 Jun 2026 02:26:07 -0400 Subject: [PATCH 41/49] remove redundat severity --- 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 9bba5d15cd7..da852145156 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -3422,7 +3422,7 @@ const triggerSet: TriggerSet = { netRegex: { id: 'BADC', source: 'Kefka', capture: false }, condition: (data) => data.pathOfLightCounter === 9, suppressSeconds: 1, - response: Responses.getBehind('alert'), + response: Responses.getBehind(), }, { id: 'DMU P2 Light of Judgment', From 2597acb5e80080a7e2833bcb9aa807818c0ff439 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 19 Jun 2026 03:33:46 -0400 Subject: [PATCH 42/49] fix trine facing --- 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 539048df05c..3aca007def5 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -3505,7 +3505,7 @@ const triggerSet: TriggerSet = { // Exception for center trine if (data.trineDirNums.length === 3) { if (x > 99 && x < 101) { - data.middleTrineFacing = matches.pairBNpcID === '1EBFB2' ? 'west' : 'east'; + data.middleTrineFacing = matches.pairBNpcID === '1EBFB2' ? 'east' : 'west'; return; } } From cea7960f806c811925f378bc90b3a3176d223208 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 19 Jun 2026 07:50:22 -0400 Subject: [PATCH 43/49] remove redundant unkown outputStrings --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 3aca007def5..9db8354a794 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -433,11 +433,11 @@ const triggerSet: TriggerSet = { { id: 'forsaken', comment: { - en: `There should be two groups of four players, choose tower soak order.
+ en: `There should be two groups of four players, choose tower soak order
Kroxy-Rinon 3/4/1: Kefka Bin
Modified ABBA: Raidplan
Bowtie AAAABBBB 4/4: Using same priority as the kroxy-rinon. (Will require Tank LB3)
- Default will be Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in.`, + Default will be Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in`, }, name: { en: 'P2 Forsaken Strategy', @@ -448,7 +448,7 @@ const triggerSet: TriggerSet = { 'AAABBBBA (3/4/1), Kroxy-Rinon': 'kroxy-rinon', 'ABBAABBA (1/2/2/2/1), Modified': 'abba', 'AAAABBBB (4/4), Bowtie': 'bowtie', - 'Generic calls': 'none', + 'Generic Calls': 'none', }, }, default: 'none', @@ -1286,7 +1286,6 @@ const triggerSet: TriggerSet = { southwest: Outputs.southwest, west: Outputs.west, northwest: Outputs.northwest, - unknown: Outputs.unknown, upup: { en: 'Up Portents', ko: '위쪽 화살표', @@ -3549,7 +3548,6 @@ const triggerSet: TriggerSet = { }, outputStrings: { ...Directions.outputStrings16Dir, - unknown: Outputs.unknown, safeSpots: { en: '${dir1}/${dir2}/${dir3} Later', }, @@ -3609,7 +3607,6 @@ const triggerSet: TriggerSet = { }, outputStrings: { ...Directions.outputStrings16Dir, - unknown: Outputs.unknown, safeSpots: { en: '${dir1}/${dir2}/${dir3}', }, From 6fc931ae5ede648579995c3c3161b0178368f5a2 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Fri, 19 Jun 2026 22:17:37 -0400 Subject: [PATCH 44/49] more delay needed Finding another log with longer time between updated headmarker and the ability. --- 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 2ee52337d14..9a0c8e6a7e0 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -2126,7 +2126,7 @@ const triggerSet: TriggerSet = { // TODO: Get Tower Locations type: 'Ability', netRegex: { id: ['BAD2', 'BAD3'], source: 'Kefka', capture: true }, - delaySeconds: 1.4, // Time until headmarker and future/past damage + delaySeconds: 1.5, // Time until headmarker and future/past damage alertText: (data, matches, output) => { const isFuture = matches.id === 'BAD2'; const count = data.pathOfLightCounter; From 1681c38db00f4f1ef58cea268c726e09ec74f366 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sat, 20 Jun 2026 04:27:02 -0400 Subject: [PATCH 45/49] fix break html Co-authored-by: Dowon --- 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 9a0c8e6a7e0..e3c85abcd9a 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -433,10 +433,10 @@ const triggerSet: TriggerSet = { { id: 'forsaken', comment: { - en: `There should be two groups of four players, choose tower soak order
- Kroxy-Rinon 3/4/1: Kefka Bin
- Modified ABBA: Raidplan
- Bowtie AAAABBBB 4/4: Using same priority as the kroxy-rinon. (Will require Tank LB3)
+ en: `There should be two groups of four players, choose tower soak order
+ Kroxy-Rinon 3/4/1: Kefka Bin
+ Modified ABBA: Raidplan
+ Bowtie AAAABBBB 4/4: Using same priority as the kroxy-rinon. (Will require Tank LB3)
Default will be Cones + Support Stack Left and Spread + DPS Stack Right, relative towers to facing in`, }, name: { From 21641d310faa4f58cb4a30da9662876b5a529705 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 21 Jun 2026 21:01:18 -0400 Subject: [PATCH 46/49] reduce code with global func, HTMR prio for stacks --- .../data/07-dt/ultimate/dancing_mad.ts | 1078 +++++++++-------- 1 file changed, 541 insertions(+), 537 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 9a0c8e6a7e0..56664b6a351 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -251,6 +251,70 @@ const trapOutputStrings: OutputStrings = { }, }; +// Get Partner's HeadMarker following HTMR Priority +// Requires data and Forsaken Group +// Will return the forsaken headmarker of partner +const getHTMRPartnerMarker = ( + data: Data, + group: string[], +): forsakenHeadmarker => { + // Avoiding use of unbound method with `this` in data.party.isHealer + const isHealer = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isHealerJob(jobName); + }; + // Functions for determining party member DPS subroles + const isRangedDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); + }; + const isMeleeDPS = ( + x: string, + ): boolean => { + const jobName = data.party.jobName(x); + if (jobName === undefined) + return false; + return Util.isMeleeDpsJob(jobName); + }; + // Function to dynamically determine which role to check + const getRoleFunction = ( + role: string + ): (name: string) => boolean => { + // Only a healer will supercede the tank + if (role === 'tank') + return isHealer; + if (Util.isMeleeDpsJob(data.job)) + return isRangedDPS; + // If we find a melee in our group we are the ranged priority + // Partner should be a melee dps, for optimal comp + return isMeleeDPS; + }; + const playerHeadmarkers = data.forsakenPlayerHeadmarkers; + // Need to look at what healer has in relation to us + // Partner is whoever has the same marker + const isMyRoleSameAs = getRoleFunction(data.role); + const member1 = group[0] ?? ''; + const member2 = group[1] ?? ''; + const member3 = group[2] ?? ''; + const partner = isMyRoleSameAs(member1) + ? member1 + : isMyRoleSameAs(member2) + ? member2 + : isMyRoleSameAs(member3) + ? member3 + : 'unknown'; + // Return partner's marker + return playerHeadmarkers[partner ?? 0] ?? 'unknown'; +}; + const forsakenOutputStrings: OutputStrings = { spreadBowtie: Outputs.spread, tower: Outputs.getTowers, @@ -285,9 +349,7 @@ const forsakenOutputStrings: OutputStrings = { spread: { en: 'Spread on YOU', }, - stack: { // This generally won't get called unless there is a wrong config or missed tower - en: 'Stack stored on YOU', - }, + stack: Outputs.stackOnYou, num: { en: '${num}: ', de: '${num}: ', @@ -322,7 +384,7 @@ const forsakenOutputStrings: OutputStrings = { stacksOnPlayers: { en: 'Stacks on ${players}', }, - stacksOnPlayersTower: { // Used after first tower + stacksOnPlayersTower: { // Used after first tower for when partner couldn't be found or none config en: '${num}${stack} + ${tower}', }, stackOnYouTower: { // Used in first tower only @@ -331,8 +393,8 @@ const forsakenOutputStrings: OutputStrings = { markerOnYouStacksOnPlayers: { // Used only for first tower en: '${num}${marker} + ${stacks}', }, - markerOnYouTowerOdds: { // Used for Cone or Spread (Stack gets separate output) - en: '${num}${marker} + ${tower} + ${far}', + markerOnYouTowerOdds: { // Used for Odd Towers (excluding first set) + en: '${num}${marker} + ${tower} + ${nearfar}', }, markerOnYouTowerEvens: { // Used for Cones + Spreads (no stacks taking the towers) en: '${num}${marker} + ${tower} + ${nearfar}', @@ -1919,137 +1981,55 @@ const triggerSet: TriggerSet = { ? output.beFar!() : output.beNear!(); - if (data.role === 'healer') { - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.leftTower!(), - nearfar: nearFar, - }); - } - - const playerHeadmarkers = data.forsakenPlayerHeadmarkers; - const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; - const member1 = group[0] ?? ''; - const member2 = group[1] ?? ''; - const member3 = group[2] ?? ''; - if (data.role === 'tank') { - // Need to look at what healer has in relation to us - // Partner is whoever has the same marker - const partner = data.party.isHealer(member1) - ? member1 - : data.party.isHealer(member2) - ? member2 - : data.party.isHealer(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not get priority - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + switch (data.role) { + case 'healer': return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.leftTower!(), nearfar: nearFar, }); + default: { + const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const pMarker = getHTMRPartnerMarker(data, group); - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.rightTower!() - : output.leftTower!(), - nearfar: nearFar, - }); - } + // Could not get priority + if (pMarker === 'unknown') + break; + if (data.role === 'tank') + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: nearFar, + }); - if (Util.isMeleeDpsJob(data.job)) { - const isRangedDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); - }; - // Partner should be a ranged dps, for standard comp - const partner = isRangedDPS(member1) - ? member1 - : isRangedDPS(member2) - ? member2 - : isRangedDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find caster or phys ranged partner - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: nearFar, + }); + + // Ranged DPS highest priority right return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.rightTower!(), nearfar: nearFar, }); - - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.leftTower!() - : output.rightTower!(), - nearfar: nearFar, - }); + } } - - // If we find a melee in our group we are the ranged priority - // Partner should be a melee dps, for optimal comp - const isMeleeDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isMeleeDpsJob(jobName); - }; - const partner = isMeleeDPS(member1) - ? member1 - : isMeleeDPS(member2) - ? member2 - : isMeleeDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find melee dps - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.tower!(), - nearfar: nearFar, - }); - - // Highest priority right + // Unable to determine priority return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.rightTower!(), + tower: output.tower!(), nearfar: nearFar, }); } @@ -2143,25 +2123,65 @@ const triggerSet: TriggerSet = { ( isForsakenGroupA && (config === 'kroxy-rinon' || config === 'bowtie') ) || - (!isForsakenGroupA && config === 'abba') || - (config === 'none') + (!isForsakenGroupA && config === 'abba') ) { - // Need to know for priority - const players = data.pathOfLightStackPlayers.map( - (player) => { - if (player === data.me) - return output.you!(); - return data.party.member(player); - }, - ); - const msg = players?.join(', '); - - // Assuming none config soaks - return output.baitThenStacks!({ - bait: time, - stacks: output.stacksOnPlayers!({ players: msg }), - }); + switch (data.role) { + case 'healer': + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: output.leftTower!() + }); + default: { + const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const pMarker = getHTMRPartnerMarker(data, group); + + // Could not get priority + if (pMarker === 'unknown') + break; // Fallback to none config + if (data.role === 'tank') + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + }); + + // Ranged is highest priority right + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: output.rightTower!(), + }); + } + } } + // None config + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Assuming none config soaks + return output.baitThenStacks!({ + bait: time, + stacks: output.stacksOnPlayers!({ players: msg }), + }); } // Tower soakers, non stack markers @@ -2265,6 +2285,49 @@ const triggerSet: TriggerSet = { // Tower Soaks // In AAAABBBB, there is no stack if (marker === 'stack') { + if (config !== 'none') { + switch (data.role) { + case 'healer': + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: output.leftTower!() + }); + default: { + const group = config === 'abba' ? data.forsakenGroupA : data.forsakenGroupB; + const pMarker = getHTMRPartnerMarker(data, group); + + // Could not get priority + if (pMarker === 'unknown') + break; // Fallback to none config + if (data.role === 'tank') + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + }); + + // Ranged is highest priority right + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: output.rightTower!(), + }); + } + } + } + // None config // Need to know for priority const players = data.pathOfLightStackPlayers.map( (player) => { @@ -2299,43 +2362,84 @@ const triggerSet: TriggerSet = { marker: output[marker]!(), }); } else if (count === 7) { - if (config !== 'none') { - if (isForsakenGroupA) { - // So long as it is standard party composition... - if (data.role === 'tank') - return output.baitThenMech!({ - bait: time, - mech: output.leftStack!(), - }); - if (data.role === 'healer') - return output.baitThenMech!({ - bait: time, - mech: output.leftBaitOut!(), - }); - // 2 DPS in stack + if (isForsakenGroupA && config !== 'none') { + // So long as it is standard party composition... + if (data.role === 'tank') return output.baitThenMech!({ bait: time, - mech: output.rightStack!(), + mech: output.leftStack!(), }); - } - if (marker === 'stack') { - // Need to know for priority - const players = data.pathOfLightStackPlayers.map( - (player) => { - if (player === data.me) - return output.you!(); - return data.party.member(player); - }, - ); - const msg = players?.join(', '); - - // Assuming none config soaks - return output.baitThenStacks!({ + if (data.role === 'healer') + return output.baitThenMech!({ bait: time, - stacks: output.stacksOnPlayers!({ players: msg }), + mech: output.leftBaitOut!(), }); + // 2 DPS in stack + return output.baitThenMech!({ + bait: time, + mech: output.rightStack!(), + }); + } + if (marker === 'stack') { + if (config !== 'none') { + switch (data.role) { + case 'healer': + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: output.leftTower!() + }); + default: { + const pMarker = getHTMRPartnerMarker(data, data.forsakenGroupB); + + // Could not get priority + if (pMarker === 'unknown') + break; // Fallback to none config + if (data.role === 'tank') + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + }); + + // Ranged is highest priority right + return output.baitThenMarkerTower!({ + bait: time, + marker: output[marker]!(), + tower: output.rightTower!(), + }); + } + } } + // None config + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + // Assuming none config soaks + return output.baitThenStacks!({ + bait: time, + stacks: output.stacksOnPlayers!({ players: msg }), + }); + } + if (config !== 'none') return output.baitThenMarkerTower!({ bait: time, marker: output[marker]!(), @@ -2343,8 +2447,6 @@ const triggerSet: TriggerSet = { ? output.leftTower!() : output.rightTower!(), }); - } - // No config return output.baitThenMarker!({ bait: time, @@ -2364,6 +2466,7 @@ const triggerSet: TriggerSet = { spread: { en: 'Spread on YOU', }, + stack: Outputs.stackOnYou, you: { en: 'YOU', }, @@ -2459,23 +2562,72 @@ const triggerSet: TriggerSet = { (!isForsakenGroupA && config === 'abba') || (config === 'none') ) { - // Need to know for priority - const players = data.pathOfLightStackPlayers.map( - (player) => { - if (player === data.me) - return output.you!(); - return data.party.member(player); - }, - ); - const msg = players?.join(', '); - - // Assuming none config soaks - return output.stacksOnPlayersTower!({ - num: num, - stack: output.stacksOnPlayers!({ players: msg }), - tower: output.tower!(), - }); + switch (data.role) { + case 'healer': + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: output.outerHitbox!(), + }); + default: { + const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const pMarker = getHTMRPartnerMarker(data, group); + + // Could not get priority + if (pMarker === 'unknown') + break; // Fallback to none config + if (data.role === 'tank') + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: pMarker === marker + ? output.innerHitbox!() + : output.outerHitbox!(), + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: pMarker === marker + ? output.outerHitbox!() + : output.innerHitbox!(), + }); + + // Ranged is highest priority right + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: output.innerHitbox!(), + }); + } + } } + // None config + // Need to know for priority + const players = data.pathOfLightStackPlayers.map( + (player) => { + if (player === data.me) + return output.you!(); + return data.party.member(player); + }, + ); + const msg = players?.join(', '); + + // Assuming none config soaks + return output.stacksOnPlayersTower!({ + num: num, + stack: output.stacksOnPlayers!({ players: msg }), + tower: output.tower!(), + }); } // Tower soakers, non stack markers @@ -2491,7 +2643,7 @@ const triggerSet: TriggerSet = { tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), - far: output.beFar!(), + nearfar: output.beFar!(), }); } @@ -2623,137 +2775,54 @@ const triggerSet: TriggerSet = { ? output.beFar!() : output.beNear!(); - if (data.role === 'healer') { - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.leftTower!(), - nearfar: nearFar, - }); - } - - const playerHeadmarkers = data.forsakenPlayerHeadmarkers; - const group = data.forsakenGroupB; - const member1 = group[0] ?? ''; - const member2 = group[1] ?? ''; - const member3 = group[2] ?? ''; - if (data.role === 'tank') { - // Need to look at what healer has in relation to us - // Partner is whoever has the same marker - const partner = data.party.isHealer(member1) - ? member1 - : data.party.isHealer(member2) - ? member2 - : data.party.isHealer(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not get priority - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + switch (data.role) { + case 'healer': return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.leftTower!(), nearfar: nearFar, }); + default: { + const pMarker = getHTMRPartnerMarker(data, data.forsakenGroupB); - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.rightTower!() - : output.leftTower!(), - nearfar: nearFar, - }); - } + // Could not get priority + if (pMarker === 'unknown') + break; + if (data.role === 'tank') + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: nearFar, + }); - if (Util.isMeleeDpsJob(data.job)) { - const isRangedDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); - }; - // Partner should be a ranged dps, for standard comp - const partner = isRangedDPS(member1) - ? member1 - : isRangedDPS(member2) - ? member2 - : isRangedDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find caster or phys ranged partner - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: nearFar, + }); + + // Ranged DPS highest priority right return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.rightTower!(), nearfar: nearFar, }); - - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.leftTower!() - : output.rightTower!(), - nearfar: nearFar, - }); + } } - - // If we find a melee in our group we are the ranged priority - // Partner should be a melee dps, for optimal comp - const isMeleeDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isMeleeDpsJob(jobName); - }; - const partner = isMeleeDPS(member1) - ? member1 - : isMeleeDPS(member2) - ? member2 - : isMeleeDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find melee dps - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.tower!(), - nearfar: nearFar, - }); - - // Highest priority right + // Unable to determine priority return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.rightTower!(), + tower: output.tower!(), nearfar: nearFar, }); } @@ -2816,7 +2885,7 @@ const triggerSet: TriggerSet = { tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), - far: output.beFar!(), + nearfar: output.beFar!(), }); } if (data.role === 'tank') @@ -2835,6 +2904,57 @@ const triggerSet: TriggerSet = { // Tower Soaks // In AAAABBBB, there is no stack if (marker === 'stack') { + if (config !== 'none') { + switch (data.role) { + case 'healer': + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: output.outerHitbox!(), + }); + default: { + const group = config === 'abba' ? data.forsakenGroupA : data.forsakenGroupB; + const pMarker = getHTMRPartnerMarker(data, group); + + // Could not get priority + if (pMarker === 'unknown') + break; // Fallback to none config + if (data.role === 'tank') + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: pMarker === marker + ? output.innerHitbox!() + : output.outerHitbox!(), + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: pMarker === marker + ? output.outerHitbox!() + : output.innerHitbox!(), + }); + + // Ranged is highest priority right + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: output.innerHitbox!(), + }); + } + } + } + // None config // Need to know for priority const players = data.pathOfLightStackPlayers.map( (player) => { @@ -2861,7 +2981,7 @@ const triggerSet: TriggerSet = { tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), - far: output.beFar!(), + nearfar: output.beFar!(), }); } @@ -2963,137 +3083,54 @@ const triggerSet: TriggerSet = { ? output.beFar!() : output.beNear!(); - if (data.role === 'healer') { - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.leftTower!(), - nearfar: nearFar, - }); - } - - const playerHeadmarkers = data.forsakenPlayerHeadmarkers; - const group = data.forsakenGroupB; - const member1 = group[0] ?? ''; - const member2 = group[1] ?? ''; - const member3 = group[2] ?? ''; - if (data.role === 'tank') { - // Need to look at what healer has in relation to us - // Partner is whoever has the same marker - const partner = data.party.isHealer(member1) - ? member1 - : data.party.isHealer(member2) - ? member2 - : data.party.isHealer(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not get priority - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + switch (data.role) { + case 'healer': return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.leftTower!(), nearfar: nearFar, }); + default: { + const pMarker = getHTMRPartnerMarker(data, data.forsakenGroupB); - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.rightTower!() - : output.leftTower!(), - nearfar: nearFar, - }); - } + // Could not get priority + if (pMarker === 'unknown') + break; + if (data.role === 'tank') + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: nearFar, + }); - if (Util.isMeleeDpsJob(data.job)) { - const isRangedDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); - }; - // Partner should be a ranged dps, for standard comp - const partner = isRangedDPS(member1) - ? member1 - : isRangedDPS(member2) - ? member2 - : isRangedDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find caster or phys ranged partner - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: nearFar, + }); + + // Ranged DPS highest priority right return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.rightTower!(), nearfar: nearFar, }); - - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.leftTower!() - : output.rightTower!(), - nearfar: nearFar, - }); + } } - - // If we find a melee in our group we are the ranged priority - // Partner should be a melee dps, for optimal comp - const isMeleeDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isMeleeDpsJob(jobName); - }; - const partner = isMeleeDPS(member1) - ? member1 - : isMeleeDPS(member2) - ? member2 - : isMeleeDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find melee dps - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.tower!(), - nearfar: nearFar, - }); - - // Highest priority right + // Unable to determine priority return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.rightTower!(), + tower: output.tower!(), nearfar: nearFar, }); } @@ -3148,6 +3185,56 @@ const triggerSet: TriggerSet = { // Tower soaks if (marker === 'stack') { + if (config !== 'none') { + switch (data.role) { + case 'healer': + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: output.leftTower!(), + nearfar: output.outerHitbox!(), + }); + default: { + const pMarker = getHTMRPartnerMarker(data, data.forsakenGroupB); + + // Could not get priority + if (pMarker === 'unknown') + break; // Fallback to none config + if (data.role === 'tank') + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: pMarker === marker + ? output.innerHitbox!() + : output.outerHitbox!(), + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: pMarker === marker + ? output.outerHitbox!() + : output.innerHitbox!(), + }); + + // Ranged is highest priority right + return output.markerOnYouTowerOdds!({ + num: num, + marker: output[marker]!(), + tower: output.rightTower!(), + nearfar: output.innerHitbox!(), + }); + } + } + } + // None config // Need to know for priority const players = data.pathOfLightStackPlayers.map( (player) => { @@ -3175,7 +3262,7 @@ const triggerSet: TriggerSet = { tower: marker === 'cone' ? output.leftTower!() : output.rightTower!(), - far: output.beFar!(), + nearfar: output.beFar!(), }); // No strategy @@ -3230,137 +3317,54 @@ const triggerSet: TriggerSet = { ? output.beFar!() : output.beNear!(); - if (data.role === 'healer') { - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.leftTower!(), - nearfar: nearFar, - }); - } - - const playerHeadmarkers = data.forsakenPlayerHeadmarkers; - const group = data.forsakenGroupA; - const member1 = group[0] ?? ''; - const member2 = group[1] ?? ''; - const member3 = group[2] ?? ''; - if (data.role === 'tank') { - // Need to look at what healer has in relation to us - // Partner is whoever has the same marker - const partner = data.party.isHealer(member1) - ? member1 - : data.party.isHealer(member2) - ? member2 - : data.party.isHealer(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not get priority - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + switch (data.role) { + case 'healer': return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.leftTower!(), nearfar: nearFar, }); - - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.rightTower!() - : output.leftTower!(), - nearfar: nearFar, - }); - } - - if (Util.isMeleeDpsJob(data.job)) { - const isRangedDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isRangedDpsJob(jobName) || Util.isCasterDpsJob(jobName); - }; - // Partner should be a ranged dps, for standard comp - const partner = isRangedDPS(member1) - ? member1 - : isRangedDPS(member2) - ? member2 - : isRangedDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find caster or phys ranged partner - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) + default: { + const pMarker = getHTMRPartnerMarker(data, data.forsakenGroupA); + + // Could not get priority + if (pMarker === 'unknown') + break; + if (data.role === 'tank') + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.rightTower!() + : output.leftTower!(), + nearfar: nearFar, + }); + + if (Util.isMeleeDpsJob(data.job)) + return output.markerOnYouTowerEvens!({ + num: num, + marker: output[marker]!(), + tower: pMarker === marker + ? output.leftTower!() + : output.rightTower!(), + nearfar: nearFar, + }); + + // Ranged DPS highest priority right return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.tower!(), + tower: output.rightTower!(), nearfar: nearFar, }); - - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: pMarker === marker - ? output.leftTower!() - : output.rightTower!(), - nearfar: nearFar, - }); + } } - - // If we find a melee in our group we are the ranged priority - // Partner should be a melee dps, for optimal comp - const isMeleeDPS = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isMeleeDpsJob(jobName); - }; - const partner = isMeleeDPS(member1) - ? member1 - : isMeleeDPS(member2) - ? member2 - : isMeleeDPS(member3) - ? member3 - : 'unknown'; - // Get partner's marker - const pMarker = playerHeadmarkers[partner ?? 0]; - - // Could not find melee dps - if ( - partner === 'unknown' || - pMarker === undefined || - pMarker === 'unknown' - ) - return output.markerOnYouTowerEvens!({ - num: num, - marker: output[marker]!(), - tower: output.tower!(), - nearfar: nearFar, - }); - - // Highest priority right + // Unable to determine priority return output.markerOnYouTowerEvens!({ num: num, marker: output[marker]!(), - tower: output.rightTower!(), + tower: output.tower!(), nearfar: nearFar, }); } From 16fe7dfbd5aa952ad3d2fbaaadd14cf0bc1b203c Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 21 Jun 2026 21:07:06 -0400 Subject: [PATCH 47/49] lint --- .../data/07-dt/ultimate/dancing_mad.ts | 24 ++++++++++++------- 1 file changed, 16 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 d238db64ff8..c49e783410d 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -286,7 +286,7 @@ const getHTMRPartnerMarker = ( }; // Function to dynamically determine which role to check const getRoleFunction = ( - role: string + role: string, ): (name: string) => boolean => { // Only a healer will supercede the tank if (role === 'tank') @@ -1990,7 +1990,9 @@ const triggerSet: TriggerSet = { nearfar: nearFar, }); default: { - const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const group = config === 'kroxy-rinon' + ? data.forsakenGroupA + : data.forsakenGroupB; const pMarker = getHTMRPartnerMarker(data, group); // Could not get priority @@ -2130,10 +2132,12 @@ const triggerSet: TriggerSet = { return output.baitThenMarkerTower!({ bait: time, marker: output[marker]!(), - tower: output.leftTower!() + tower: output.leftTower!(), }); default: { - const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const group = config === 'kroxy-rinon' + ? data.forsakenGroupA + : data.forsakenGroupB; const pMarker = getHTMRPartnerMarker(data, group); // Could not get priority @@ -2291,7 +2295,7 @@ const triggerSet: TriggerSet = { return output.baitThenMarkerTower!({ bait: time, marker: output[marker]!(), - tower: output.leftTower!() + tower: output.leftTower!(), }); default: { const group = config === 'abba' ? data.forsakenGroupA : data.forsakenGroupB; @@ -2387,7 +2391,7 @@ const triggerSet: TriggerSet = { return output.baitThenMarkerTower!({ bait: time, marker: output[marker]!(), - tower: output.leftTower!() + tower: output.leftTower!(), }); default: { const pMarker = getHTMRPartnerMarker(data, data.forsakenGroupB); @@ -2571,7 +2575,9 @@ const triggerSet: TriggerSet = { nearfar: output.outerHitbox!(), }); default: { - const group = config === 'kroxy-rinon' ? data.forsakenGroupA : data.forsakenGroupB; + const group = config === 'kroxy-rinon' + ? data.forsakenGroupA + : data.forsakenGroupB; const pMarker = getHTMRPartnerMarker(data, group); // Could not get priority @@ -2914,7 +2920,9 @@ const triggerSet: TriggerSet = { nearfar: output.outerHitbox!(), }); default: { - const group = config === 'abba' ? data.forsakenGroupA : data.forsakenGroupB; + const group = config === 'abba' + ? data.forsakenGroupA + : data.forsakenGroupB; const pMarker = getHTMRPartnerMarker(data, group); // Could not get priority From 26b1d4304a5b2a229d3b365c1c644f5ba7a00ff4 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 21 Jun 2026 21:38:22 -0400 Subject: [PATCH 48/49] comments --- .../data/07-dt/ultimate/dancing_mad.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index c49e783410d..56a28e3e6a7 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -253,11 +253,21 @@ const trapOutputStrings: OutputStrings = { // Get Partner's HeadMarker following HTMR Priority // Requires data and Forsaken Group -// Will return the forsaken headmarker of partner +// Will return the forsaken headmarker of partner: +// Tanks + Healers are partners +// Melee DPS + Range/Caster are Partners +// Tanks look for healer as they are left unless healer has it +// Melee DPS look for the Range/Caster as they are left if ranged has it +// Range/Caster looks for existence of a Melee DPS in case there is fake melee const getHTMRPartnerMarker = ( data: Data, group: string[], ): forsakenHeadmarker => { + // Healer role should not be parsed with this function + // as they have highest priority left + if (data.role === 'healer') + return 'unknown'; + // Avoiding use of unbound method with `this` in data.party.isHealer const isHealer = ( x: string, @@ -298,19 +308,20 @@ const getHTMRPartnerMarker = ( return isMeleeDPS; }; const playerHeadmarkers = data.forsakenPlayerHeadmarkers; - // Need to look at what healer has in relation to us - // Partner is whoever has the same marker - const isMyRoleSameAs = getRoleFunction(data.role); + + // Check each player in the group if they are our partner + const isMyPartner = getRoleFunction(data.role); const member1 = group[0] ?? ''; const member2 = group[1] ?? ''; const member3 = group[2] ?? ''; - const partner = isMyRoleSameAs(member1) + const partner = isMyPartner(member1) ? member1 - : isMyRoleSameAs(member2) + : isMyPartner(member2) ? member2 - : isMyRoleSameAs(member3) + : isMyPartner(member3) ? member3 : 'unknown'; + // Return partner's marker return playerHeadmarkers[partner ?? 0] ?? 'unknown'; }; From c4027c7c2f4d76d928cbc21b48eb09887ff82ed3 Mon Sep 17 00:00:00 2001 From: Legends0 Date: Sun, 21 Jun 2026 23:42:28 -0400 Subject: [PATCH 49/49] use .bind(data.party) instead of Util --- ui/raidboss/data/07-dt/ultimate/dancing_mad.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts index 56a28e3e6a7..d90bc2683bf 100644 --- a/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts +++ b/ui/raidboss/data/07-dt/ultimate/dancing_mad.ts @@ -268,15 +268,6 @@ const getHTMRPartnerMarker = ( if (data.role === 'healer') return 'unknown'; - // Avoiding use of unbound method with `this` in data.party.isHealer - const isHealer = ( - x: string, - ): boolean => { - const jobName = data.party.jobName(x); - if (jobName === undefined) - return false; - return Util.isHealerJob(jobName); - }; // Functions for determining party member DPS subroles const isRangedDPS = ( x: string, @@ -300,7 +291,7 @@ const getHTMRPartnerMarker = ( ): (name: string) => boolean => { // Only a healer will supercede the tank if (role === 'tank') - return isHealer; + return data.party.isHealer.bind(data.party); if (Util.isMeleeDpsJob(data.job)) return isRangedDPS; // If we find a melee in our group we are the ranged priority