Skip to content

Commit b2685cb

Browse files
author
github-actions
committed
triggers build: raidboss: r8n triggers and timeline (OverlayPlugin#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> 50cfaf7
1 parent caddd34 commit b2685cb

2 files changed

Lines changed: 511 additions & 1 deletion

File tree

07-dt/raid/r8n.js

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

0 commit comments

Comments
 (0)