Skip to content

Commit 50cfaf7

Browse files
Legends0JLGarber
andauthored
raidboss: r8n triggers and timeline (#777)
Skipping Terrestrial Titans because it didn't look simple enough to port from savage, the mechanic uses three towers instead of two and, while entities spawn at locations and face where they will fall in normal as well, I would need to look more into the safe spot locations from patterns as it isn't clear from a quick look on if there are static spots or what the call even would say. Could use some more digging through fflogs to find the add enrage. Right now the timeline adds an extra 60 seconds. There also wasn't a mapeffects to sync to like savage has, but perhaps there could be another method than just startsUsing. **EDIT:** Enrage was added, although timings manually adjusted. --------- Co-authored-by: Jonathan Garber <linkthevaliant@gmail.com>
1 parent efd9de5 commit 50cfaf7

2 files changed

Lines changed: 539 additions & 2 deletions

File tree

ui/raidboss/data/07-dt/raid/r8n.ts

Lines changed: 365 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,377 @@
1+
import Conditions from '../../../../../resources/conditions';
2+
import Outputs from '../../../../../resources/outputs';
3+
import { callOverlayHandler } from '../../../../../resources/overlay_plugin_api';
4+
import { Responses } from '../../../../../resources/responses';
5+
import { Directions } from '../../../../../resources/util';
16
import ZoneId from '../../../../../resources/zone_id';
27
import { RaidbossData } from '../../../../../types/data';
38
import { TriggerSet } from '../../../../../types/trigger';
49

5-
export type Data = RaidbossData;
10+
export interface Data extends RaidbossData {
11+
reignDir?: number;
12+
moonbeamBites: number[];
13+
moonbeamBitesTracker: number;
14+
moonlightQuadrant2?: string;
15+
}
16+
17+
const centerX = 100;
18+
const centerY = 100;
19+
20+
const headMarkerData = {
21+
// Shared tankbuster marker used for Great Divide
22+
'tankbuster': '0256',
23+
// Stack marker used for first two Heavensearths
24+
'stack': '003E',
25+
// Stack marker used for Heavensearth in Tactical Pack
26+
'stackAdds': '005D',
27+
// Spread marker used for Gust in Tactical Pack
28+
'spread': '0178',
29+
// Big, pulsing, 4-arrow stack marker for Tracking Tremors
30+
'eightHitStack': '013C',
31+
} as const;
32+
33+
const moonlightOutputStrings = {
34+
...Directions.outputStrings8Dir,
35+
safeQuad: {
36+
en: '${quad}',
37+
de: '${quad}',
38+
ja: '${quad}',
39+
cn: '${quad}',
40+
ko: '${quad}',
41+
},
42+
safeQuadrants: {
43+
en: '${quad1} => ${quad2}',
44+
de: '${quad1} => ${quad2}',
45+
ja: '${quad1} => ${quad2}',
46+
cn: '${quad1} => ${quad2}',
47+
ko: '${quad1} => ${quad2}',
48+
},
49+
};
650

751
const triggerSet: TriggerSet<Data> = {
852
id: 'AacCruiserweightM4',
953
zoneId: ZoneId.AacCruiserweightM4,
1054
timelineFile: 'r8n.txt',
11-
triggers: [],
55+
initData: () => ({
56+
moonbeamBites: [],
57+
moonbeamBitesTracker: 0,
58+
}),
59+
triggers: [
60+
{
61+
id: 'R8N Extraplanar Pursuit',
62+
type: 'StartsUsing',
63+
netRegex: { id: 'A38E', source: 'Howling Blade', capture: false },
64+
response: Responses.aoe(),
65+
},
66+
{
67+
id: 'R8N Great Divide',
68+
type: 'HeadMarker',
69+
netRegex: { id: [headMarkerData.tankbuster], capture: true },
70+
response: Responses.sharedTankBuster(),
71+
},
72+
{
73+
id: 'R8N Wolves\' Reign',
74+
type: 'StartsUsing',
75+
// A96D and A96E are used in first part of fight
76+
// A90F or A910 is used after adds
77+
// Normal mode is always in
78+
netRegex: { id: ['A96D', 'A96E', 'A90F', 'A910'], source: 'Howling Blade', capture: false },
79+
infoText: (_data, _matches, output) => {
80+
return output.inLater!();
81+
},
82+
outputStrings: {
83+
inLater: {
84+
en: '(In Later)',
85+
de: '(später Rein)',
86+
ja: '(あとで内側)',
87+
cn: '(稍后内侧)',
88+
ko: '(나중에 안)',
89+
},
90+
},
91+
},
92+
{
93+
id: 'R8N Wolves\' Reign Direction',
94+
type: 'StartsUsing',
95+
netRegex: { id: ['A96D', 'A96E', 'A90F', 'A910'], source: 'Howling Blade', capture: true },
96+
delaySeconds: (_data, matches) => parseFloat(matches.castTime) + 1.2,
97+
promise: async (data, matches) => {
98+
const actors = (await callOverlayHandler({
99+
call: 'getCombatants',
100+
ids: [parseInt(matches.sourceId, 16)],
101+
})).combatants;
102+
const actor = actors[0];
103+
if (actors.length !== 1 || actor === undefined) {
104+
console.error(
105+
`R8N Wolves' Reign Direction: Wrong actor count ${actors.length}`,
106+
);
107+
return;
108+
}
109+
110+
data.reignDir = (Directions.hdgTo16DirNum(actor.Heading) + 8) % 16;
111+
},
112+
infoText: (data, _matches, output) => {
113+
const dir = output[Directions.output16Dir[data.reignDir ?? -1] ?? 'unknown']!();
114+
return output.inDir!({ dir: dir });
115+
},
116+
run: (data) => {
117+
data.reignDir = undefined;
118+
},
119+
outputStrings: {
120+
...Directions.outputStrings16Dir,
121+
inDir: {
122+
en: 'In ${dir}',
123+
de: 'Rein ${dir}',
124+
ja: '内側 ${dir}',
125+
cn: '内侧 ${dir}',
126+
ko: '${dir} 안',
127+
},
128+
},
129+
},
130+
{
131+
id: 'R8N Heavensearth',
132+
type: 'HeadMarker',
133+
netRegex: { id: [headMarkerData.stack, headMarkerData.stackAdds], capture: true },
134+
response: Responses.stackMarkerOn(),
135+
},
136+
{
137+
id: 'R8N Beckon Moonlight Quadrants',
138+
type: 'Ability',
139+
// First set is constant, later sets use different spell ids
140+
// A97E/A3DE => Right cleave self-cast
141+
// A97F/A3DF => Left cleave self-cast
142+
netRegex: { id: ['A97E', 'A3DE', 'A97F', 'A3DF'], source: 'Moonlit Shadow', capture: true },
143+
delaySeconds: 0.1,
144+
durationSeconds: 10,
145+
promise: async (data, matches) => {
146+
const actors = (await callOverlayHandler({
147+
call: 'getCombatants',
148+
ids: [parseInt(matches.sourceId, 16)],
149+
})).combatants;
150+
const actor = actors[0];
151+
if (actors.length !== 1 || actor === undefined) {
152+
console.error(
153+
`R8N Beckon Moonlight Quadrants: Wrong actor count ${actors.length}`,
154+
);
155+
return;
156+
}
157+
158+
const dirNum = Directions.xyTo8DirNum(actor.PosX, actor.PosY, centerX, centerY);
159+
// Moonbeam's Bite (A97C/A376 Left / A97D/A377 Right) half-room cleaves
160+
// Defining the cleaved side
161+
if (matches.id === 'A97E' || matches.id === 'A3DE') {
162+
const counterclock = dirNum === 0 ? 6 : dirNum - 2;
163+
data.moonbeamBites.push(counterclock);
164+
}
165+
if (matches.id === 'A97F' || matches.id === 'A3DF') {
166+
const clockwise = (dirNum + 2) % 8;
167+
data.moonbeamBites.push(clockwise);
168+
}
169+
},
170+
infoText: (data, _matches, output) => {
171+
if (data.moonbeamBites.length === 1 || data.moonbeamBites.length === 3)
172+
return;
173+
174+
const quadrants = [1, 3, 5, 7];
175+
const moonbeam1 = data.moonbeamBites[0] ?? -1;
176+
const moonbeam2 = data.moonbeamBites[1] ?? -1;
177+
let safeQuads1 = quadrants.filter((quadrant) => {
178+
return quadrant !== moonbeam1 + 1;
179+
});
180+
safeQuads1 = safeQuads1.filter((quadrant) => {
181+
return quadrant !== (moonbeam1 === 0 ? 7 : moonbeam1 - 1);
182+
});
183+
safeQuads1 = safeQuads1.filter((quadrant) => {
184+
return quadrant !== moonbeam2 + 1;
185+
});
186+
safeQuads1 = safeQuads1.filter((quadrant) => {
187+
return quadrant !== (moonbeam2 === 0 ? 7 : moonbeam2 - 1);
188+
});
189+
190+
// Early output for first two
191+
if (data.moonbeamBites.length === 2) {
192+
if (safeQuads1.length !== 1 || safeQuads1[0] === undefined) {
193+
console.error(
194+
`R8N Beckon Moonlight Quadrants: Invalid safeQuads1, length of ${safeQuads1.length}.`,
195+
);
196+
return;
197+
}
198+
const quad = output[Directions.outputFrom8DirNum(safeQuads1[0] ?? -1)]!();
199+
return output.safeQuad!({ quad: quad });
200+
}
201+
202+
const moonbeam3 = data.moonbeamBites[2] ?? -1;
203+
const moonbeam4 = data.moonbeamBites[3] ?? -1;
204+
let safeQuads2 = quadrants.filter((quadrant) => {
205+
return quadrant !== moonbeam3 + 1;
206+
});
207+
safeQuads2 = safeQuads2.filter((quadrant) => {
208+
return quadrant !== (moonbeam3 === 0 ? 7 : moonbeam3 - 1);
209+
});
210+
safeQuads2 = safeQuads2.filter((quadrant) => {
211+
return quadrant !== moonbeam4 + 1;
212+
});
213+
safeQuads2 = safeQuads2.filter((quadrant) => {
214+
return quadrant !== (moonbeam4 === 0 ? 7 : moonbeam4 - 1);
215+
});
216+
217+
if (safeQuads1[0] === undefined || safeQuads2[0] === undefined) {
218+
console.error(
219+
`R8N Beckon Moonlight Quadrants: First safeQuads missing`,
220+
);
221+
return;
222+
}
223+
if (safeQuads1.length !== 1) {
224+
console.error(
225+
`R8N Beckon Moonlight Quadrants: Invalid safeQuads1, length of ${safeQuads1.length}`,
226+
);
227+
return;
228+
}
229+
if (safeQuads2.length !== 1) {
230+
console.error(
231+
`R8N Beckon Moonlight Quadrants: Invalid safeQuads2, length of ${safeQuads2.length}`,
232+
);
233+
return;
234+
}
235+
236+
// Store quadrant for move call
237+
data.moonlightQuadrant2 = output[Directions.outputFrom8DirNum(safeQuads2[0] ?? -1)]!();
238+
239+
const quad1 = output[Directions.outputFrom8DirNum(safeQuads1[0] ?? -1)]!();
240+
return output.safeQuadrants!({ quad1: quad1, quad2: data.moonlightQuadrant2 });
241+
},
242+
outputStrings: moonlightOutputStrings,
243+
},
244+
{
245+
id: 'R8N Beckon Moonlight Quadrant Two',
246+
type: 'StartsUsing',
247+
// A97C/A376 => Moonbeam's Bite dash with Left cleave
248+
// A97D/A377 => Moonbeam's Bite dash with Right cleave
249+
netRegex: { id: ['A97C', 'A376', 'A97D', 'A377'], source: 'Moonlit Shadow', capture: true },
250+
condition: (data) => {
251+
data.moonbeamBitesTracker = data.moonbeamBitesTracker + 1;
252+
if (data.moonbeamBitesTracker === 2)
253+
return true;
254+
return false;
255+
},
256+
delaySeconds: (_data, matches) => parseFloat(matches.castTime),
257+
infoText: (data, _matches, output) => {
258+
return output.safeQuad!({ quad: data.moonlightQuadrant2 });
259+
},
260+
run: (data) => {
261+
// Reset for additional Beckon Moonlights
262+
if (data.moonbeamBitesTracker === 4) {
263+
data.moonbeamBites = [];
264+
delete data.moonlightQuadrant2;
265+
data.moonbeamBitesTracker = 0;
266+
}
267+
},
268+
outputStrings: moonlightOutputStrings,
269+
},
270+
{
271+
id: 'R8N Shadowchase',
272+
// A981 cast before adds
273+
// A374 cast after adds
274+
type: 'StartsUsing',
275+
netRegex: { id: ['A981', 'A374'], source: 'Howling Blade', capture: false },
276+
suppressSeconds: 1,
277+
alertText: (_data, _matches, output) => {
278+
return output.behindClones!();
279+
},
280+
outputStrings: {
281+
behindClones: {
282+
en: 'Behind Clones',
283+
},
284+
},
285+
},
286+
{
287+
id: 'R8N Gust',
288+
type: 'HeadMarker',
289+
netRegex: { id: [headMarkerData.spread], capture: true },
290+
condition: Conditions.targetIsYou(),
291+
response: Responses.spread(),
292+
},
293+
{
294+
id: 'R8N Ravenous Saber',
295+
type: 'StartsUsing',
296+
netRegex: { id: 'A743', source: 'Howling Blade', capture: false },
297+
durationSeconds: 7,
298+
response: Responses.bigAoe(),
299+
},
300+
{
301+
id: 'R8N Weal of Stone',
302+
// TODO: Add direction such as Avoid lines from ${dir}
303+
// There are two casts: A989 and A988
304+
type: 'StartsUsing',
305+
netRegex: { id: 'A989', source: 'Wolf of Stone', capture: false },
306+
suppressSeconds: 1,
307+
alertText: (_data, _matches, output) => {
308+
return output.lines!();
309+
},
310+
outputStrings: {
311+
lines: {
312+
en: 'Avoid Lines',
313+
de: 'Vermeide Linien',
314+
ja: '直線攻撃を避ける',
315+
cn: '躲避直线 AoE',
316+
ko: '직선장판 피하기',
317+
},
318+
},
319+
},
320+
{
321+
id: 'R8N Shadowchase Rotate',
322+
// Call to move behind Dragon Head after clones dash
323+
// These only spawn after adds
324+
// Dragon Heads cast Roaring Wind (A984 and A985)
325+
type: 'StartsUsing',
326+
netRegex: { id: 'A374', source: 'Howling Blade', capture: true },
327+
delaySeconds: (_data, matches) => parseFloat(matches.castTime),
328+
suppressSeconds: 1,
329+
infoText: (_data, _matches, output) => {
330+
return output.rotate!();
331+
},
332+
outputStrings: {
333+
rotate: {
334+
en: 'Rotate',
335+
de: 'Rotieren',
336+
ja: '回転',
337+
cn: '旋转',
338+
ko: '회전',
339+
},
340+
},
341+
},
342+
{
343+
id: 'R8N Tracking Tremors',
344+
type: 'StartsUsing',
345+
netRegex: { id: 'A4E2', source: 'Howling Blade', capture: false },
346+
durationSeconds: 10,
347+
infoText: (_data, _matches, output) => output.text!(),
348+
outputStrings: {
349+
text: {
350+
en: 'Stack x5',
351+
de: 'Sammeln x5',
352+
fr: 'Package x5',
353+
ja: '頭割り x5',
354+
cn: '5次分摊',
355+
ko: '쉐어 5번',
356+
},
357+
},
358+
},
359+
{
360+
id: 'R8N Weal of Stone Cardinals',
361+
// There are two casts and cardinals is always safe:
362+
// A98C Weal of Stone
363+
// A98D Weal of Stone
364+
type: 'StartsUsing',
365+
netRegex: { id: 'A98D', source: 'Wolf of Stone', capture: false },
366+
suppressSeconds: 1,
367+
alertText: (_data, _matches, output) => {
368+
return output.cardinals!();
369+
},
370+
outputStrings: {
371+
cardinals: Outputs.cardinals,
372+
},
373+
},
374+
],
12375
};
13376

14377
export default triggerSet;

0 commit comments

Comments
 (0)