From fb8db89eeaff08450ead88622df2696931aecff4 Mon Sep 17 00:00:00 2001 From: Adrienne Walker Date: Sun, 8 Oct 2023 19:10:35 -0700 Subject: [PATCH] raidboss: more ex7 triggers (#5846) --- ui/raidboss/data/06-ew/trial/zeromus-ex.ts | 561 ++++++++++++++++----- 1 file changed, 444 insertions(+), 117 deletions(-) diff --git a/ui/raidboss/data/06-ew/trial/zeromus-ex.ts b/ui/raidboss/data/06-ew/trial/zeromus-ex.ts index 62c3de27f9..4e35e65ce7 100644 --- a/ui/raidboss/data/06-ew/trial/zeromus-ex.ts +++ b/ui/raidboss/data/06-ew/trial/zeromus-ex.ts @@ -6,30 +6,38 @@ import { Directions } from '../../../../../resources/util'; import ZoneId from '../../../../../resources/zone_id'; import { RaidbossData } from '../../../../../types/data'; import { PluginCombatantState } from '../../../../../types/event'; -import { NetMatches } from '../../../../../types/net_matches'; import { TriggerSet } from '../../../../../types/trigger'; +// TODO: Abyssal Echoes safe spots +// TODO: Flare safe spots +// TODO: Meteor tether calls (could we say like 3 left, 1 right?) + export interface Data extends RaidbossData { - decOffset?: number; + phase: 'one' | 'two'; + seenSableThread?: boolean; miasmicBlasts: PluginCombatantState[]; + busterPlayers: string[]; forkedPlayers: string[]; + blackHolePlayer?: string; + flareMechanic?: 'spread' | 'stack'; + noxPlayers: string[]; + flowLocation?: 'north' | 'middle' | 'south'; } const headmarkerMap = { - 'tankbuster': '016C', - 'blackHole': '014A', - 'spread': '0017', - 'enums': '00D3', - 'stack': '003E', + tankBuster: '016C', + blackHole: '014A', + tether: '0146', + // Most spread markers (Big Bang, Big Crunch, Dark Divides) + spread: '0178', + accelerationBomb: '010B', + nox: '00C5', + akhRhaiSpread: '0017', + enums: '00D3', + // The Dark Beckons, but also Umbral Rays + stack: '003E', } as const; -const firstHeadmarker = parseInt(headmarkerMap.tankbuster, 16); -const getHeadmarkerId = (data: Data, matches: NetMatches['HeadMarker']) => { - if (typeof data.decOffset === 'undefined') - data.decOffset = parseInt(matches.id, 16) - firstHeadmarker; - return (parseInt(matches.id, 16) - data.decOffset).toString(16).toUpperCase().padStart(4, '0'); -}; - const centerX = 100; const centerY = 100; @@ -39,146 +47,139 @@ const triggerSet: TriggerSet = { timelineFile: 'zeromus-ex.txt', initData: () => { return { + phase: 'one', miasmicBlasts: [], + busterPlayers: [], forkedPlayers: [], + noxPlayers: [], }; }, - triggers: [ + timelineTriggers: [ { - id: 'ZeromusEx Headmarker Tracker', - type: 'HeadMarker', - netRegex: {}, - condition: (data) => data.decOffset === undefined, - // Unconditionally set the first headmarker here so that future triggers are conditional. - run: (data, matches) => getHeadmarkerId(data, matches), + id: 'ZeromusEx Flare', + // Extra time for spreading out. + // This could also be StartsUsing 85BD. + regex: /^Flare$/, + beforeSeconds: 13, + suppressSeconds: 20, + response: Responses.getTowers(), }, { - id: 'ZeromusEx Visceral Whirl NE Safe', - type: 'StartsUsing', - netRegex: { id: '8B43', capture: false }, - infoText: (_data, _matches, output) => { - return output.text!({ dir1: output.ne!(), dir2: output.sw!() }); - }, + id: 'ZeromusEx Big Bang Spread', + // Extra time for spreading out. + // This could alternatively be StartsUsing 8B4C or HeadMarker 0178. + regex: /^Big Bang$/, + beforeSeconds: 13, + suppressSeconds: 20, + response: Responses.spread('alert'), + }, + { + id: 'ZeromusEx Big Crunch Spread', + // Extra time for spreading out. + // This could alternatively be StartsUsing 8B4D or HeadMarker 0178. + regex: /^Big Crunch$/, + beforeSeconds: 13, + suppressSeconds: 20, + response: Responses.spread('alert'), + }, + ], + triggers: [ + { + id: 'ZeromusEx Abyssal Nox', + type: 'GainsEffect', + netRegex: { effectId: '6E9', capture: false }, + suppressSeconds: 5, + alertText: (_data, _matches, output) => output.text!(), outputStrings: { text: { - en: '${dir1}/${dir2}', - de: '${dir1}/${dir2}', + en: 'Heal to full', }, - ne: Outputs.northeast, - sw: Outputs.southwest, }, }, { - id: 'ZeromusEx Visceral Whirl NW Safe', - type: 'StartsUsing', - netRegex: { id: '8B46', capture: false }, - infoText: (_data, _matches, output) => { - return output.text!({ dir1: output.nw!(), dir2: output.se!() }); + id: 'ZeromusEx Sable Thread', + type: 'Ability', + netRegex: { id: '8AEF', source: 'Zeromus' }, + alertText: (data, matches, output) => { + const num = data.seenSableThread ? 7 : 6; + data.seenSableThread = true; + if (matches.target === data.me) + return output.lineStackOnYou!({ num: num }); + return output.lineStackOn!({ num: num, player: data.ShortName(matches.target) }); }, outputStrings: { - text: { - en: '${dir1}/${dir2}', - de: '${dir1}/${dir2}', + lineStackOn: { + en: '${num}x line stack on ${player}', + }, + lineStackOnYou: { + en: '${num}x line stack on YOU', }, - nw: Outputs.northwest, - se: Outputs.southeast, }, }, { - id: 'ZeromusEx Fractured Eventide NE Safe', - type: 'StartsUsing', - netRegex: { id: '8B3C', capture: false }, - alertText: (_data, _matches, output) => output.ne!(), - outputStrings: { - ne: Outputs.northeast, + id: 'ZeromusEx Dark Matter You', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.tankBuster }, + alertText: (data, matches, output) => { + data.busterPlayers.push(matches.target); + if (data.me === matches.target) + return output.tankBusterOnYou!(); }, - }, - { - id: 'ZeromusEx Fractured Eventide NW Safe', - type: 'StartsUsing', - netRegex: { id: '8B3D', capture: false }, - alertText: (_data, _matches, output) => output.nw!(), outputStrings: { - nw: Outputs.northwest, + tankBusterOnYou: Outputs.tankBusterOnYou, }, }, { - id: 'ZeromusEx Black Hole Headmarker', + id: 'ZeromusEx Dark Matter Others', type: 'HeadMarker', - netRegex: {}, - condition: (data) => data.role === 'tank', - suppressSeconds: 20, - alertText: (data, matches, output) => { - const id = getHeadmarkerId(data, matches); - if (id === headmarkerMap.blackHole) - return output.blackHole!(); + netRegex: { id: headmarkerMap.tankBuster, capture: false }, + delaySeconds: 0.5, + suppressSeconds: 2, + infoText: (data, _matches, output) => { + if (!data.busterPlayers.includes(data.me)) + return output.tankBusters!(); }, outputStrings: { - blackHole: { - en: 'Black Hole on YOU', - de: 'Schwarzes Loch auf DIR', - }, + tankBusters: Outputs.tankBusters, }, }, { - id: 'ZeromusEx Spread Headmarker', - type: 'HeadMarker', - netRegex: { capture: true }, - condition: (data, matches) => - data.decOffset !== undefined && getHeadmarkerId(data, matches) === headmarkerMap.spread, - suppressSeconds: 2, - response: Responses.spread(), + id: 'ZeromusEx Dark Matter Cleanup', + type: 'Ability', + netRegex: { id: '8B84', source: 'Zeromus', capture: false }, + suppressSeconds: 5, + run: (data) => data.busterPlayers = [], }, { - id: 'ZeromusEx Enum Headmarker', - type: 'HeadMarker', - netRegex: { capture: true }, - condition: (data, matches) => - data.decOffset !== undefined && getHeadmarkerId(data, matches) === headmarkerMap.enums, - suppressSeconds: 2, - infoText: (_data, _matches, output) => output.enumeration!(), + id: 'ZeromusEx Visceral Whirl NE Safe', + type: 'StartsUsing', + netRegex: { id: '8B43', source: 'Zeromus', capture: false }, + infoText: (_data, _matches, output) => { + return output.text!({ dir1: output.ne!(), dir2: output.sw!() }); + }, outputStrings: { - enumeration: { - en: 'Enumeration', - de: 'Enumeration', - fr: 'Énumération', - ja: 'エアーバンプ', - cn: '蓝圈分摊', - ko: '2인 장판', + text: { + en: '${dir1} / ${dir2}', + de: '${dir1} / ${dir2}', }, + ne: Outputs.northeast, + sw: Outputs.southwest, }, }, { - id: 'ZeromusEx Forked Lightning Collect', - type: 'GainsEffect', - netRegex: { effectId: 'ED7' }, - run: (data, matches) => data.forkedPlayers.push(matches.target), - }, - { - id: 'ZeromusEx Stack Headmarker', - type: 'HeadMarker', - netRegex: { capture: true }, - condition: (data, matches) => - data.decOffset !== undefined && getHeadmarkerId(data, matches) === headmarkerMap.stack, - alertText: (data, matches, output) => { - if (data.forkedPlayers !== undefined && data.forkedPlayers.includes(data.me)) - return output.forkedLightning!(); - if (data.me === matches.target) - return output.stackOnYou!(); - return output.stackOnTarget!({ player: data.ShortName(matches.target) }); + id: 'ZeromusEx Visceral Whirl NW Safe', + type: 'StartsUsing', + netRegex: { id: '8B46', source: 'Zeromus', capture: false }, + infoText: (_data, _matches, output) => { + return output.text!({ dir1: output.nw!(), dir2: output.se!() }); }, - run: (data) => data.forkedPlayers = [], outputStrings: { - stackOnYou: Outputs.stackOnYou, - stackOnTarget: Outputs.stackOnPlayer, - forkedLightning: { - en: 'Lightning on YOU', - de: 'Blitz auf DIR', - fr: 'Éclair sur VOUS', - ja: '自分にフォークライトニング', - cn: '雷点名', - ko: '갈래 번개 대상자', + text: { + en: '${dir1} / ${dir2}', + de: '${dir1} / ${dir2}', }, + nw: Outputs.northwest, + se: Outputs.southeast, }, }, { @@ -191,7 +192,7 @@ const triggerSet: TriggerSet = { { id: 'ZeromusEx Miasmic Blast Safe Spots', type: 'StartsUsing', - netRegex: { id: '8B49', capture: true }, + netRegex: { id: '8B49', source: 'Zeromus', capture: true }, delaySeconds: 0.5, promise: async (data, matches) => { const combatants = (await callOverlayHandler({ @@ -265,13 +266,339 @@ const triggerSet: TriggerSet = { ...Directions.outputStrings16Dir, }, }, + { + id: 'ZeromusEx Big Bang', + type: 'StartsUsing', + netRegex: { id: '8B4C', source: 'Zeromus', capture: false }, + response: Responses.bleedAoe(), + }, + { + id: 'ZeromusEx Forked Lightning', + type: 'GainsEffect', + netRegex: { effectId: 'ED7' }, + condition: (data, matches) => { + data.forkedPlayers.push(matches.target); + return matches.target === data.me; + }, + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 6, + durationSeconds: 5, + alarmText: (_data, _matches, output) => output.forkedLightning!(), + outputStrings: { + forkedLightning: { + en: 'Spread (forked lightning)', + }, + }, + }, + { + id: 'ZeromusEx The Dark Beckons Stack', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.stack }, + condition: (data) => data.phase === 'one', + // Wait to collect tank markers. + delaySeconds: 0.5, + alertText: (data, matches, output) => { + if (data.busterPlayers.includes(data.me)) + return; + if (data.forkedPlayers.includes(data.me)) + return; + if (data.me === matches.target) + return output.stackOnYou!(); + return output.stackOnTarget!({ player: data.ShortName(matches.target) }); + }, + outputStrings: { + stackOnYou: Outputs.stackOnYou, + stackOnTarget: Outputs.stackOnPlayer, + }, + }, { id: 'ZeromusEx Acceleration Bomb', type: 'GainsEffect', netRegex: { effectId: 'A61' }, condition: Conditions.targetIsYou(), - delaySeconds: (_data, matches) => parseFloat(matches.duration) - 4, - response: Responses.stopMoving(), + delaySeconds: (_data, matches) => parseFloat(matches.duration) - 3, + response: Responses.stopEverything(), + }, + { + id: 'ZeromusEx Tether Bait', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.tether, capture: false }, + suppressSeconds: 5, + alertText: (_data, _matches, output) => output.text!(), + outputStrings: { + text: { + en: 'Group middle for tethers', + }, + }, + }, + { + id: 'ZeromusEx Tether', + type: 'Tether', + netRegex: { id: ['00A3', '010B'] }, + condition: (data, matches) => data.me === matches.target || data.me === matches.source, + suppressSeconds: 10, + alertText: (data, matches, output) => { + const partner = matches.source === data.me ? matches.target : matches.source; + return output.breakTether!({ partner: data.ShortName(partner) }); + }, + outputStrings: { + breakTether: { + en: 'Break tether (w/ ${partner})', + de: 'Verbindung brechen (mit ${partner})', + ja: '線切る (${partner})', + cn: '拉断连线 (和 ${partner})', + ko: '선 끊기 (+ ${partner})', + }, + }, + }, + { + id: 'ZeromusEx Black Hole Tracker', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.blackHole }, + run: (data, matches) => data.blackHolePlayer = matches.target, + }, + { + id: 'ZeromusEx Fractured Eventide NE Safe', + type: 'StartsUsing', + netRegex: { id: '8B3C', source: 'Zeromus', capture: false }, + alarmText: (data, _matches, output) => { + if (data.me === data.blackHolePlayer) + return output.blackHole!(); + }, + alertText: (_data, _matches, output) => output.northeast!(), + run: (data) => delete data.blackHolePlayer, + outputStrings: { + northeast: Outputs.northeast, + blackHole: { + en: 'East Black Hole on Wall', + }, + }, + }, + { + id: 'ZeromusEx Fractured Eventide NW Safe', + type: 'StartsUsing', + netRegex: { id: '8B3D', source: 'Zeromus', capture: false }, + alarmText: (data, _matches, output) => { + if (data.me === data.blackHolePlayer) + return output.blackHole!(); + }, + alertText: (_data, _matches, output) => output.northwest!(), + run: (data) => delete data.blackHolePlayer, + outputStrings: { + northwest: Outputs.northwest, + blackHole: { + en: 'West Black Hole on Wall', + }, + }, + }, + { + id: 'ZeromusEx Big Crunch', + type: 'StartsUsing', + netRegex: { id: '8B4D', source: 'Zeromus', capture: false }, + response: Responses.bleedAoe(), + }, + { + id: 'ZeromusEx Sparking Flare Tower', + type: 'StartsUsing', + netRegex: { id: '8B5E', source: 'Zeromus', capture: false }, + durationSeconds: 6, + alertText: (_data, _matches, output) => output.text!(), + run: (data) => data.flareMechanic = 'spread', + outputStrings: { + text: { + en: 'Get Towers => Spread', + }, + }, + }, + { + id: 'ZeromusEx Branding Flare Tower', + type: 'StartsUsing', + netRegex: { id: '8B5F', source: 'Zeromus', capture: false }, + durationSeconds: 6, + alertText: (_data, _matches, output) => output.text!(), + run: (data) => data.flareMechanic = 'stack', + outputStrings: { + text: { + en: 'Get Towers => Partner Stacks', + }, + }, + }, + { + id: 'ZeromusEx Flare Mechanic With Nox', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.nox }, + condition: (data, matches) => { + data.noxPlayers.push(matches.target); + return data.me === matches.target; + }, + alarmText: (data, _matches, output) => { + if (data.flareMechanic === 'stack') + return output.stackWithNox!(); + if (data.flareMechanic === 'spread') + return output.spreadWithNox!(); + }, + outputStrings: { + stackWithNox: { + en: 'Partner Stack + Chasing Nox', + }, + spreadWithNox: { + en: 'Spread + Chasing Nox', + }, + }, + }, + { + id: 'ZeromusEx Flare Mechanic No Nox', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.nox, capture: false }, + delaySeconds: 0.5, + suppressSeconds: 5, + alertText: (data, _matches, output) => { + if (data.noxPlayers.includes(data.me)) + return; + if (data.flareMechanic === 'stack') + return output.stack!(); + if (data.flareMechanic === 'spread') + return output.spread!(); + }, + outputStrings: { + stack: { + en: 'Partner Stack', + }, + spread: { + en: 'Spread', + }, + }, + }, + { + id: 'ZeromusEx Rend the Rift', + type: 'StartsUsing', + netRegex: { id: '8C0D', source: 'Zeromus', capture: false }, + response: Responses.aoe(), + run: (data) => data.phase = 'two', + }, + { + id: 'ZeromusEx Nostalgia', + type: 'Ability', + // Call this on the ability not the cast so 10 second mits last. + netRegex: { id: '8B6B', source: 'Zeromus', capture: false }, + suppressSeconds: 5, + response: Responses.bigAoe(), + }, + { + id: 'ZeromusEx Flow of the Abyss', + type: 'MapEffect', + netRegex: { flags: '00020001', location: ['02', '03', '04'] }, + infoText: (data, matches, output) => { + const flowMap: { [location: string]: Data['flowLocation'] } = { + '02': 'north', + '03': 'middle', + '04': 'south', + } as const; + + data.flowLocation = flowMap[matches.location]; + if (data.flowLocation === 'north') + return output.north!(); + if (data.flowLocation === 'middle') + return output.middle!(); + if (data.flowLocation === 'south') + return output.south!(); + }, + outputStrings: { + north: { + en: 'Out of North', + }, + middle: { + en: 'Out of Middle', + }, + south: { + en: 'Out of South', + }, + }, + }, + { + id: 'ZeromusEx Akh Rhai', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.akhRhaiSpread }, + condition: Conditions.targetIsYou(), + alertText: (data, _matches, output) => { + if (data.flowLocation === undefined) + return output.spread!(); + return output[`${data.flowLocation}Spread`]!(); + }, + run: (data) => delete data.flowLocation, + outputStrings: { + spread: Outputs.spread, + northSpread: { + en: 'Spread Middle/South', + }, + middleSpread: { + en: 'Spread North/South', + }, + southSpread: { + en: 'Spread North/Middle', + }, + }, + }, + { + id: 'ZeromusEx Akh Rhai Followup', + type: 'Ability', + netRegex: { id: '8B74', source: 'Zeromus', capture: false }, + suppressSeconds: 5, + response: Responses.moveAway(), + }, + { + id: 'ZeromusEx Umbral Prism Enumeration', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.enums, capture: false }, + suppressSeconds: 2, + alertText: (data, _matches, output) => { + if (data.flowLocation === undefined) + return output.enumeration!(); + return output[`${data.flowLocation}Enumeration`]!(); + }, + run: (data) => delete data.flowLocation, + outputStrings: { + enumeration: { + en: 'Enumeration', + de: 'Enumeration', + fr: 'Énumération', + ja: 'エアーバンプ', + cn: '蓝圈分摊', + ko: '2인 장판', + }, + northEnumeration: { + en: 'Enumeration Middle/South', + }, + middleEnumeration: { + en: 'Enumeration North/South', + }, + southEnumeration: { + en: 'Enumeration North/Middle', + }, + }, + }, + { + id: 'ZeromusEx Umbral Rays Stack', + type: 'HeadMarker', + netRegex: { id: headmarkerMap.stack, capture: false }, + condition: (data) => data.phase === 'two', + alertText: (data, _matches, output) => { + if (data.flowLocation === undefined) + return output.stack!(); + return output[`${data.flowLocation}Stack`]!(); + }, + run: (data) => delete data.flowLocation, + outputStrings: { + stack: Outputs.stackMarker, + northStack: { + en: 'Stack Middle', + }, + middleStack: { + en: 'Stack North', + }, + southStack: { + en: 'Stack North/Middle', + }, + }, }, ], timelineReplace: [