-
Notifications
You must be signed in to change notification settings - Fork 383
/
Copy pathe8s.ts
1572 lines (1550 loc) · 57.2 KB
/
e8s.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import Conditions from '../../../../../resources/conditions';
import Outputs from '../../../../../resources/outputs';
import { callOverlayHandler } from '../../../../../resources/overlay_plugin_api';
import { Responses } from '../../../../../resources/responses';
import { ConfigValue } from '../../../../../resources/user_config';
import Util from '../../../../../resources/util';
import ZoneId from '../../../../../resources/zone_id';
import { RaidbossData } from '../../../../../types/data';
import { PluginCombatantState } from '../../../../../types/event';
import { TriggerSet } from '../../../../../types/trigger';
// TODO: Mirror Mirror 5
// TODO: Icelit Dragonsong callouts
// TODO: Wyrm's Lament 2, maybe print who your partner is for BLU
// Note: for BLU, there's not much you can do for Diamond Frost.
// The people who get flares are not necessarily the people with the Freezing debuff.
// The people who drop puddles can be anybody. For non-BLU, we could call out
// who has puddles or not or who is going to get flares, but not for BLU.
export type ConfigIds = 'uptimeKnockbackStrat';
export type MirrorColor = 'blue' | 'red' | 'green';
export type MirrorLoc = 'dirN' | 'dirNE' | 'dirE' | 'dirSE' | 'dirS' | 'dirSW' | 'dirW' | 'dirNW';
export type MirrorInfo = { color: MirrorColor; location: MirrorLoc };
const mirrorFlags: { [flags: string]: MirrorColor } = {
'00020001': 'blue',
'00200010': 'green',
'02000100': 'red',
} as const;
const mirrorLocs: { [loc: string]: MirrorLoc } = {
// Mirror Mirror 1 + 2
'03': 'dirN',
'05': 'dirE',
'07': 'dirS',
'09': 'dirW',
// unsure on these
'04': 'dirNE',
'06': 'dirSE',
'08': 'dirSW',
'0A': 'dirNW',
// Mirror Mirror 3 + 4
'0B': 'dirN',
'0D': 'dirE',
'0F': 'dirS',
'11': 'dirW',
// unsure on these
'0C': 'dirNE',
'0E': 'dirSE',
'10': 'dirSW',
'12': 'dirNW',
} as const;
type MirrorThreeDir = 'dirNW' | 'dirNE' | 'dirSW' | 'dirSE';
export interface Data extends RaidbossData {
triggerSetConfig: { [key in ConfigIds]: ConfigValue };
combatantData: PluginCombatantState[];
lightsteepedCount: { [name: string]: number };
mirrorMirrorCount: number;
mirrors: MirrorInfo[];
mirrorMap: { [loc in MirrorLoc]?: MirrorColor };
firstFrost?: 'biting' | 'driving';
firstKick?: 'scythe' | 'axe';
diamondFrostFreezeTargets: string[];
diamondFrostStars: string[];
calledIcicleImpact?: boolean;
pathOfLightCounter: number;
asunderSide?: 'east' | 'west';
asunderCount: number;
rushCount: number;
akhMornTargets: string[];
wyrmsLament: number;
wyrmclawNumber?: number;
wyrmfangNumber?: number;
wyrmsLamentMirrorCount: number;
mirrorThreeDirs: MirrorThreeDir[];
}
const triggerSet: TriggerSet<Data> = {
id: 'EdensVerseRefulgenceSavage',
zoneId: ZoneId.EdensVerseRefulgenceSavage,
config: [
{
id: 'uptimeKnockbackStrat',
name: {
en: 'Enable uptime knockback strat',
de: 'Aktiviere Uptime Rückstoß Strategie',
fr: 'Activer la strat Poussée-Uptime',
ja: 'エデン零式共鳴編4層:cactbot「ヘヴンリーストライク (ノックバック)」ギミック', // FIXME
cn: '启用 cactbot 精确计时防击退策略',
ko: '정확한 타이밍 넉백방지 공략 사용',
},
comment: {
en: `If you want cactbot to callout Mirror Mirror 4's double knockback, enable this option.
Callout happens during/after boss turns and requires <1.4s reaction time
to avoid both Green and Red Mirror knockbacks.
Example: https://clips.twitch.tv/CreativeDreamyAsparagusKlappa
Group splits into two groups behind boss after the jump.
Tanks adjust to where the Red and Green Mirror are located.
One tank must be inbetween the party, the other closest to Greem Mirror.
Once Green Mirror goes off, the tanks adjust for Red Mirror.`,
de:
`Wenn du möchten, dass Cactbot den doppelten Knockback von Spiegelland 4 auslöst, aktivieren Sie diese Option.
Die Anzeige erfolgt während/nach den Drehungen des Bosses und erfordert <1,4s Reaktionszeit
um sowohl den grünen als auch den roten Spiegel-Rückstoß zu vermeiden.
Beispiel: https://clips.twitch.tv/CreativeDreamyAsparagusKlappa
Die Gruppe teilt sich nach dem Sprung hinter dem Boss in zwei Gruppen auf.
Die Tanks passen sich danach an, wo sich der rote und der grüne Spiegel befinden.
Ein Tank muss sich in der Mitte der Gruppe befinden, der andere in der Nähe des grünen Spiegels.
Sobald der grüne Spiegel ausgelöst wird, passen sich die Tanks auf den roten Spiegel an.`,
fr:
`Si vous voulez que cactbot signale le double knockback de Mirror Mirror 4, activez cette option.
L'annonce se fait pendant/après les tours du boss et nécessite un temps de réaction < à 1.4s
pour éviter les deux poussées du miroir vert et du miroir rouge.
Exemple : https://clips.twitch.tv/CreativeDreamyAsparagusKlappa
Le groupe se divise en deux groupes derrière le boss après le saut.
Les tanks s'adaptent à l'emplacement des miroirs rouge et vert.
Un tank doit être entre les deux groupes, l'autre doit être le plus proche du miroir vert.
Une fois que le miroir vert s'éteint, les tanks s'ajustent au miroir rouge.`,
cn: `此选项可让 cactbot 提示第四次镜中奇遇的双击退。
会在 BOSS 转身期间或之后播报提示, 需要小于 1.4 秒
的反应时间来同时躲避绿镜和红镜击退。
示例: https://clips.twitch.tv/CreativeDreamyAsparagusKlappa
人群在 BOSS 瞬移后分成两组, 分别站在 BOSS 身后。
坦克根据红镜和绿镜的位置进行调整。
一个坦克必须在分组中间,另一个坦克最靠近绿镜。
绿镜熄灭后,坦克调整到红镜位置。`,
ko: `캑트봇이 거울 나라 4의 이중 넉백을 호출하게 하려면 이 옵션을 활성화하세요.
알람은 보스의 시전 중간이나 이후에 발생하며
녹색 및 빨강 거울의 넉백을 모두 피하려면 반응 시간이 1.4초 미만이어야 합니다.
예시: https://clips.twitch.tv/CreativeDreamyAsparagusKlappa
점프 후 보스 뒤에서 파티가 두 그룹으로 나뉩니다.
탱커 빨강 및 초록 거울이 있는 위치에 맞춰 조정합니다.
탱커 한 명은 파티 사이에, 다른 한 명은 초록 거울에 가장 가까운 곳에 위치해야 합니다.
초록 거울이 발동하면 탱커는 빨강 거울에 맞춰 위치를 조정합니다.`,
},
type: 'checkbox',
default: (options) => {
const oldSetting = options['cactbote8sUptimeKnockbackStrat'];
return typeof oldSetting === 'boolean' ? oldSetting : false;
},
},
],
timelineFile: 'e8s.txt',
initData: () => {
return {
combatantData: [],
lightsteepedCount: {},
mirrorMirrorCount: 0,
mirrors: [],
mirrorMap: {},
diamondFrostFreezeTargets: [],
diamondFrostStars: [],
pathOfLightCounter: 0,
bonusLightSteeped: {},
asunderCount: 0,
rushCount: 0,
akhMornTargets: [],
wyrmsLament: 0,
wyrmsLamentMirrorCount: 0,
mirrorThreeDirs: [],
};
},
timelineTriggers: [
{
id: 'E8S Shining Armor',
regex: /(?<!Reflected )Shining Armor/,
beforeSeconds: 2,
suppressSeconds: 15,
response: Responses.lookAway('alert'),
},
{
id: 'E8S Reflected Armor',
regex: /Reflected Armor/,
beforeSeconds: 2,
suppressSeconds: 15,
response: Responses.lookAway('alert'),
},
{
id: 'E8S Frost Armor',
// Not the reflected one, as we want the "move" call there
// which will happen naturally from `Reflected Drachen Armor`.
regex: /^Frost Armor$/,
beforeSeconds: 2,
suppressSeconds: 15,
response: Responses.stopMoving('alert'),
},
{
id: 'E8S Rush',
regex: /Rush \d/,
beforeSeconds: 5,
suppressSeconds: 15,
infoText: (data, _matches, output) => {
data.rushCount = data.rushCount + 1;
return output.text!({ num: data.rushCount });
},
outputStrings: {
text: {
en: 'Tether ${num}',
de: 'Verbindung ${num}',
fr: 'Lien ${num}',
ja: '線 ${num}',
cn: '和${num}连线',
ko: '선: ${num}',
},
},
},
],
triggers: [
{
id: 'E8S Lightsteeped Gain Tracker',
type: 'GainsEffect',
netRegex: { effectId: '8D1' },
run: (data, matches) => data.lightsteepedCount[matches.target] = parseInt(matches.count),
},
{
id: 'E8S Lightsteeped Lose Tracker',
type: 'LosesEffect',
netRegex: { effectId: '8D1' },
run: (data, matches) => data.lightsteepedCount[matches.target] = 0,
},
{
id: 'E8S Absolute Zero',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4DCC', capture: false },
response: Responses.aoe(),
},
{
id: 'E8S Mirror Mirror',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D5A', capture: false },
infoText: (data, _matches, output) => {
data.mirrorMirrorCount++;
data.mirrors = [];
data.mirrorMap = {};
if (data.mirrorMirrorCount === 2) {
if (data.firstKick === 'axe')
return output.scytheNext!();
if (data.firstKick === 'scythe')
return output.axeNext!();
}
},
outputStrings: {
scytheNext: {
en: '(under boss => under mirrors soon)',
de: '(unter den Boss => gleich unter den Spiegel)',
fr: '(sous le boss => sous les miroirs bientôt',
cn: '(BOSS 下方 => 即将去镜子下方)',
ko: '(보스 밑 => 이후 거울 밑)',
},
axeNext: {
en: '(out => middle soon)',
de: '(raus => gleich Mitte)',
cn: '(外 => 即将去中间)',
ko: '(밖 => 이후 중앙)',
},
},
},
{
id: 'E8S Mirror Collect',
type: 'MapEffect',
netRegex: { flags: Object.keys(mirrorFlags), location: Object.keys(mirrorLocs) },
run: (data, matches) => {
const color = mirrorFlags[matches.flags];
const location = mirrorLocs[matches.location];
if (color === undefined || location === undefined)
return;
data.mirrors.push({ color, location });
data.mirrorMap[location] = color;
},
},
{
id: 'E8S Biting Frost First Mirror',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D66', capture: false },
condition: (data) => data.firstFrost === undefined,
alertText: (data, _matches, output) => {
if (data.mirrorMap['dirW'] === 'red')
return output.redMirrorWest!();
if (data.mirrorMap['dirE'] === 'red')
return output.redMirrorEast!();
return output.getBehind!();
},
outputStrings: {
redMirrorWest: {
en: 'Behind => SW',
de: 'Hinten => SW',
fr: 'Derrière => SO',
cn: '后 => 左下 (西南)',
ko: '뒤 => 남서',
},
redMirrorEast: {
en: 'Behind => SE',
de: 'Hinten => SO',
fr: 'Derrière => SE',
cn: '后 => 右下 (东南)',
ko: '뒤 => 남동',
},
getBehind: Outputs.getBehind,
},
},
{
id: 'E8S Driving Frost First Mirror',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D67', capture: false },
condition: (data) => data.firstFrost === undefined,
alertText: (data, _matches, output) => {
if (data.mirrorMap['dirE'] === 'red')
return output.redMirrorEast!();
if (data.mirrorMap['dirW'] === 'red')
return output.redMirrorWest!();
return output.goFront!();
},
outputStrings: {
redMirrorEast: {
en: 'Front => NW',
de: 'Vorne => NW',
fr: 'Devant => NO',
cn: '前 => 左上 (西北)',
ko: '앞 => 북서',
},
redMirrorWest: {
en: 'Front => NE',
de: 'Vorne => NO',
fr: 'Devant => NE',
cn: '前 => 右上 (东北)',
ko: '앞 => 북동',
},
goFront: Outputs.goFront,
},
},
{
id: 'E8S Reflected Frost 1',
type: 'Ability',
netRegex: { source: 'Frozen Mirror', id: '4DB[78]', capture: false },
suppressSeconds: 5,
infoText: (_data, _matches, output) => output.text!(),
outputStrings: {
text: {
en: 'Swap Sides',
de: 'Seiten wechseln',
fr: 'Changez de côté',
ja: '反対側へ',
cn: '换边',
ko: '반대로 이동',
},
},
},
{
id: 'E8S Biting Frost',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D66', capture: false },
alertText: (data, _matches, output) => {
// The first one is part of Mirror Mirror 1.
if (data.firstFrost !== undefined)
return output.getBehind!();
},
run: (data) => data.firstFrost ??= 'biting',
outputStrings: {
getBehind: Outputs.getBehind,
},
},
{
id: 'E8S Driving Frost',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D67', capture: false },
alertText: (data, _matches, output) => {
// The first one is part of Mirror Mirror 1.
if (data.firstFrost !== undefined)
return output.goFront!();
},
run: (data) => data.firstFrost ??= 'driving',
outputStrings: {
goFront: Outputs.goFront,
},
},
{
id: 'E8S Forgetful Tank Second Frost',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D6[67]', capture: false },
condition: (data) => data.role === 'tank' || data.job === 'BLU',
delaySeconds: 43,
suppressSeconds: 80,
infoText: (data, _matches, output) => {
if (data.firstFrost === 'driving')
return output.bitingFrostNext!();
return output.drivingFrostNext!();
},
outputStrings: {
bitingFrostNext: {
en: 'Biting Next (face outward)',
de: 'Frosthieb als nächstes (nach außen drehen)',
fr: 'Taillade de givre (pointez vers l\'extérieur)',
ja: '次はフロストスラッシュ', // FIXME
cn: '冰霜斩 (去背后)',
ko: '서리 참격 (뒤로)',
},
drivingFrostNext: {
en: 'Driving Next (face inward)',
de: 'Froststoß als nächstes (nach innen drehen)',
fr: 'Percée de givre (pointez vers l\'intérieur)',
ja: '次はフロストスラスト', // FIXME
cn: '冰霜刺 (去前面)',
ko: '서리 일격 (앞으로)',
},
},
},
{
id: 'E8S Diamond Frost',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D6C', capture: false },
response: Responses.aoe(),
},
{
id: 'E8S Diamond Frost Freezing',
type: 'GainsEffect',
netRegex: { effectId: '8CB' },
condition: (data, matches) => {
// Ignore Icelit Dragonsong.
if (data.mirrorMirrorCount !== 1)
return false;
data.diamondFrostFreezeTargets.push(matches.target);
return data.diamondFrostFreezeTargets.length === 2;
},
infoText: (data, _matches, output) => {
if (!Util.canCleanse(data.job))
return;
const players = data.diamondFrostFreezeTargets.sort().map((x) => data.party.member(x));
return output.cleanse!({ players: players });
},
outputStrings: {
cleanse: {
en: 'Cleanse: ${players}',
de: 'Reinige: ${players}',
fr: 'Guérison : ${players}',
cn: '驱散: ${players}',
ko: '에스나: ${players}',
},
},
},
{
id: 'E8S Diamond Frost Frigid Needle Star',
type: 'HeadMarker',
netRegex: { id: '0060' },
condition: (data, matches) => data.mirrorMirrorCount === 1 && matches.target === data.me,
alertText: (_data, _matches, output) => output.text!(),
outputStrings: {
text: {
en: 'Star on YOU',
de: 'Stern auf DIR',
fr: 'Étoile sur VOUS',
cn: '冰针点名',
ko: '별 징 대상자',
},
},
},
{
id: 'E8S Icicle Impact',
type: 'StartsUsingExtra',
netRegex: { id: '4DA0' },
condition: (data) => !data.calledIcicleImpact,
durationSeconds: 6,
suppressSeconds: 20,
infoText: (_data, matches, output) => {
const x = parseFloat(matches.x);
if (x >= 99 && x <= 101)
return output.northSouth!();
return output.eastWest!();
},
run: (data) => data.calledIcicleImpact = true,
outputStrings: {
northSouth: {
en: 'North / South',
de: 'Norden / Süden',
fr: 'Nord / Sud',
ja: '南 / 北',
cn: '南北站位',
ko: '남 / 북',
},
eastWest: {
en: 'East / West',
de: 'Osten / Westen',
fr: 'Est / Ouest',
ja: '東 / 西',
cn: '东西站位',
ko: '동 / 서',
},
},
},
{
id: 'E8S Icicle Impact Backup',
type: 'Ability',
// In case the OP 263/0x107 lines are missing, here's a late backup based on
// when the first circles go off.
netRegex: { source: 'Shiva', id: '4DA0' },
condition: (data) => !data.calledIcicleImpact,
suppressSeconds: 20,
infoText: (_data, matches, output) => {
const x = parseFloat(matches.x);
if (x >= 99 && x <= 101)
return output.northSouth!();
return output.eastWest!();
},
run: (data) => data.calledIcicleImpact = true,
outputStrings: {
northSouth: {
en: 'North / South',
de: 'Norden / Süden',
fr: 'Nord / Sud',
ja: '南 / 北',
cn: '南北站位',
ko: '남 / 북',
},
eastWest: {
en: 'East / West',
de: 'Osten / Westen',
fr: 'Est / Ouest',
ja: '東 / 西',
cn: '东西站位',
ko: '동 / 서',
},
},
},
{
id: 'E8S Double Slap',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D65' },
response: Responses.tankBusterSwap(),
},
{
id: 'E8S Axe Kick',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D6D', capture: false },
alertText: (data, _matches, output) => {
if (data.firstKick !== undefined) {
return output.outThenMiddle!();
}
data.firstKick = 'axe';
return output.out!();
},
outputStrings: {
outThenMiddle: {
en: 'Out => Middle',
de: 'Raus => Mitte',
fr: 'Extérieur => Milieu',
cn: '远离 => 中间',
ko: '밖 => 중앙',
},
out: Outputs.out,
},
},
{
id: 'E8S Scythe Kick',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D6E', capture: false },
alertText: (data, _matches, output) => {
if (data.firstKick !== undefined) {
if (data.mirrorMap['dirN'] === 'green')
return output.getUnderCards!();
if (data.mirrorMap['dirNE'] === 'green')
return output.getUnderIntercards!();
return output.getUnderUnknown!();
}
data.firstKick = 'scythe';
return output.getUnder!();
},
outputStrings: {
getUnderCards: {
en: 'Under => Under Cardinal Mirrors',
de: 'Unter den Boss => Unter Kardinal-Spiegel',
fr: 'Dessous => Sous les miroirs cardinaux',
cn: '下方 => 正点镜下方',
ko: '보스 밑 => 십자 방향 거울 밑',
},
getUnderIntercards: {
en: 'Under => Under Intercard Mirrors',
de: 'Unter den Boss => Unter Interkardinal-Spiegel',
fr: 'Dessous => Sous les miroirs intercardinaux',
cn: '下方 => 斜点镜下方',
ko: '보스 밑 => 대각선 방향 거울 밑',
},
getUnderUnknown: {
en: 'Under Boss => Under Mirrors',
de: 'Unter den Boss => Unter Spiegel',
fr: 'Sous le boss => Sous les miroirs',
cn: 'BOSS 下方 => 镜下方',
ko: '보스 밑 => 거울 밑',
},
getUnder: Outputs.getUnder,
},
},
{
id: 'E8S Light Rampant',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D73', capture: false },
response: Responses.aoe(),
},
{
id: 'E8S Refulgent Chain',
type: 'GainsEffect',
netRegex: { effectId: '8CD' },
condition: Conditions.targetIsYou(),
suppressSeconds: 1,
infoText: (_data, _matches, output) => output.text!(),
outputStrings: {
text: {
en: 'Chain on YOU',
de: 'Kette auf DIR',
fr: 'Chaîne sur VOUS',
ja: '自分に鎖',
cn: '连线点名',
ko: '사슬 대상자',
},
},
},
{
id: 'E8S Holy Light',
type: 'Tether',
netRegex: { id: '0002' },
condition: Conditions.targetIsYou(),
infoText: (_data, _matches, output) => output.text!(),
outputStrings: {
text: {
en: 'Orb on YOU',
de: 'Orb auf DIR',
fr: 'Orbe sur VOUS',
ja: '自分に玉',
cn: '拉球点名',
ko: '구슬 대상자',
},
},
},
{
id: 'E8S Path of Light Counter',
type: 'Ability',
// 4D63 = self-targeted path of light ability
netRegex: { source: 'Shiva', id: '4D63', capture: false },
run: (data) => data.pathOfLightCounter++,
},
{
id: 'E8S Light Rampant Final Tower',
type: 'GainsEffect',
// Wait until lightsteeped has been collected after the final path of light.
netRegex: { effectId: '8D1', capture: false },
condition: (data) => data.pathOfLightCounter === 2,
delaySeconds: 0.5,
suppressSeconds: 9999999,
response: (data, _matches, output) => {
// cactbot-builtin-response
output.responseOutputStrings = {
getFinalTower: {
en: 'Get Final Tower',
de: 'Nimm letzten Turm',
fr: 'Prenez la tour finale',
cn: '踩最后塔',
ko: '마지막 기둥 들어가기',
},
avoidFinalTower: {
en: 'Avoid Final Tower',
de: 'Vermeide letzten Turm',
fr: 'Évitez la tour finale',
cn: '躲最后塔',
ko: '마지막 기둥 피하기',
},
};
const light = data.lightsteepedCount[data.me];
if (light !== undefined && light >= 4)
return { infoText: output.avoidFinalTower!() };
return { alertText: output.getFinalTower!() };
},
},
{
id: 'E8S Banish III',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D80', capture: false },
response: Responses.stackMarker('info'),
},
{
id: 'E8S Banish III Divided',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D81', capture: false },
response: Responses.spread('alert'),
},
{
id: 'E8S Heart Asunder Side Tracker',
type: 'Ability',
netRegex: { id: '4DAC', source: 'MotherCrystal' },
condition: Conditions.targetIsYou(),
suppressSeconds: 999999,
run: (data, matches) => {
// The two sides are at roughly x=~70 and x=~130.
const x = parseFloat(matches.x);
data.asunderSide ??= x > 100 ? 'east' : 'west';
},
},
{
id: 'E8S Aqueous Aether',
type: 'Ability',
// On 4DAC Heart Asunder; both sides have Aqueuous on 1 + 3
netRegex: { id: '4DAC', source: 'MotherCrystal', capture: false },
preRun: (data) => data.asunderCount++,
suppressSeconds: 5,
alertText: (data, _matches, output) => {
if (!Util.canStun(data.job))
return;
if (data.asunderCount === 1 || data.asunderCount === 3)
return output.text!();
},
outputStrings: {
text: {
en: 'Stun Aqueous Aether',
de: 'Wasseräther unterbrechen',
fr: 'Étourdissez l\'ether aqueux',
cn: '眩晕水以太',
ko: '물 에테르 기절',
},
},
},
{
id: 'E8S Earthen Aether Stoneskin',
type: 'StartsUsing',
netRegex: { source: 'Earthen Aether', id: '4D85' },
condition: (data, matches) => {
if (!Util.canSilence(data.job))
return false;
const x = parseFloat(matches.x);
const side = x > 100 ? 'east' : 'west';
return side === data.asunderSide;
},
response: Responses.interrupt(),
},
{
id: 'E8S Akh Morn',
type: 'StartsUsing',
netRegex: { source: ['Shiva', 'Great Wyrm'], id: ['4D98', '4D79'] },
preRun: (data, matches) => {
data.akhMornTargets.push(matches.target);
},
response: (data, matches, output) => {
// cactbot-builtin-response
output.responseOutputStrings = {
akhMornOnYou: {
en: 'Akh Morn on YOU',
de: 'Akh Morn auf DIR',
fr: 'Akh Morn sur VOUS',
ja: '自分にアク・モーン',
cn: '死亡轮回点名',
ko: '아크몬 대상자',
},
akhMornOn: {
en: 'Akh Morn: ${players}',
de: 'Akh Morn: ${players}',
fr: 'Akh Morn : ${players}',
ja: 'アク・モーン: ${players}',
cn: '死亡轮回: ${players}',
ko: '아크몬 : ${players}',
},
};
if (data.me === matches.target) {
// It'd be nice to have this be an alert, but it mixes with a lot of
// other alerts (akh rhai "move" and worm's lament numbers).
return { [data.role === 'tank' ? 'infoText' : 'alarmText']: output.akhMornOnYou!() };
}
if (data.akhMornTargets.length !== 2)
return;
if (data.akhMornTargets.includes(data.me))
return;
const players = data.akhMornTargets.map((x) => data.party.member(x));
return { infoText: output.akhMornOn!({ players: players }) };
},
},
{
id: 'E8S Akh Morn Cleanup',
type: 'StartsUsing',
netRegex: { source: ['Shiva', 'Great Wyrm'], id: ['4D98', '4D79'], capture: false },
delaySeconds: 15,
run: (data) => data.akhMornTargets = [],
},
{
id: 'E8S Morn Afah',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D7B' },
alertText: (data, matches, output) => {
if (data.me === matches.target)
return output.mornAfahOnYou!();
return output.mornAfahOn!({ player: data.party.member(matches.target) });
},
outputStrings: {
mornAfahOnYou: {
en: 'Morn Afah on YOU',
de: 'Morn Afah auf DIR',
fr: 'Morn Afah sur VOUS',
ja: '自分にモーン・アファー',
cn: '无尽顿悟点名',
ko: '몬아파 대상자',
},
mornAfahOn: {
en: 'Morn Afah on ${player}',
de: 'Morn Afah auf ${player}',
fr: 'Morn Afah sur ${player}',
ja: '${player}にモーン・アファー',
cn: '无尽顿悟点 ${player}',
ko: '"${player}" 몬 아파',
},
},
},
{
id: 'E8S Mirror Mirror 3 Directions',
comment: {
en: `Fast means you can go from the 1st to the 3rd safe spot directly.
Slow means you need to go 1 => 2 => 3 without skipping 2.
This is for casters who may not want to move as much.`,
de: `Schnell bedeutet, dass man direkt vom 1. zum 3. sicheren Punkt gehen kann.
Langsam bedeutet, dass man 1 => 2 => 3 gehen muss, ohne 2 zu überspringen.
Dies ist für Magier, die sich vielleicht nicht so viel bewegen wollen.`,
fr: `Rapide signifie que vous pouvez passer directement du premier au troisième point sûr.
Lent signifie que vous devez aller de 1 => 2 => 3 sans omettre 2.
C'est pour les lanceurs de sorts qui ne veulent pas se déplacer autant.`,
cn: `快指你可以从第 1 个安全点直接到达第 3 个安全点。
慢指你需要走 1 => 2 => 3 ,不跳过 2。
适用于不想移动太多的读条职业。`,
ko: `빠름은 첫 번째 안전 지점에서 세 번째 안전 지점으로 바로 이동할 수 있음을 의미합니다.
느림은 2번을 건너뛰지 않고 1번 => 2번 => 3번으로 이동해야 한다는 뜻입니다.
많이 움직이고 싶지 않은 캐스터를 위한 옵션입니다.`,
},
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: ['4D75', '4D76'] },
condition: (data) => data.wyrmsLament === 0,
// She teleports to face north, then turns when she starts the cast.
delaySeconds: 0.5,
durationSeconds: 10,
suppressSeconds: 15,
promise: async (data, matches) => {
data.combatantData = [];
data.combatantData = (await callOverlayHandler({
call: 'getCombatants',
ids: [parseInt(matches.sourceId, 16)],
})).combatants;
},
// sound: '',
infoText: (data, matches, output) => {
const isLeftCleave = matches.id === '4D75';
const [shiva] = data.combatantData;
if (shiva === undefined || data.combatantData.length !== 1)
return;
// north = pi or -pi, and anywhere else consider her turned/turning south
const isFacingNorth = Math.abs(shiva.Heading) > 3;
// There are three mirrors. Green is north. Blue/Red are east/west.
// The order the mirrors go off is Shiva Cleave 1 + Blue -> Green -> Shiva Cleave 2 + Red.
// If the Blue mirror is west, we are rotating clockwise.
const isClockwise = data.mirrorMap['dirW'] === 'blue';
const isFirstSafeWest = isFacingNorth && !isLeftCleave || !isFacingNorth && isLeftCleave;
const isFirstSafeNorth = isClockwise && !isLeftCleave || !isClockwise && isLeftCleave;
const dirClock: readonly MirrorThreeDir[] = ['dirNW', 'dirNE', 'dirSE', 'dirSW'] as const;
const dir1 = isFirstSafeNorth
? (isFirstSafeWest ? 'dirNW' : 'dirNE')
: (isFirstSafeWest ? 'dirSW' : 'dirSE');
// Find next two directions by rotating.
const rotAdjust = isClockwise ? 1 : -1;
const idx1 = dirClock.indexOf(dir1);
const idx2 = (idx1 + rotAdjust + 4) % 4;
const idx3 = (idx2 + rotAdjust + 4) % 4;
const dir2 = dirClock[idx2];
const dir3 = dirClock[idx3];
if (dir2 === undefined || dir3 === undefined)
return;
data.mirrorThreeDirs = [dir1, dir2, dir3];
const isFast = dir1 === 'dirNW' && isClockwise || dir1 === 'dirNE' && !isClockwise ||
dir1 === 'dirSE' && isClockwise || dir1 === 'dirSW' && !isClockwise;
const params = { dir1: output[dir1]!(), dir2: output[dir2]!(), dir3: output[dir3]!() };
return isFast ? output.fastText!(params) : output.slowText!(params);
},
outputStrings: {
slowText: {
en: '${dir1} => ${dir2} => ${dir3} (slow)',
de: '${dir1} => ${dir2} => ${dir3} (langsam)',
fr: '${dir1} => ${dir2} => ${dir3} (lent)',
cn: '${dir1} => ${dir2} => ${dir3} (慢)',
ko: '${dir1} => ${dir2} => ${dir3} (느림)',
},
fastText: {
en: '${dir1} => ${dir2} => ${dir3} (fast)',
de: '${dir1} => ${dir2} => ${dir3} (schnell)',
fr: '${dir1} => ${dir2} => ${dir3} (rapide)',
cn: '${dir1} => ${dir2} => ${dir3} (快)',
ko: '${dir1} => ${dir2} => ${dir3} (빠름)',
},
dirNW: Outputs.dirNW,
dirNE: Outputs.dirNE,
dirSE: Outputs.dirSE,
dirSW: Outputs.dirSW,
},
},
{
id: 'E8S Mirror Mirror 3 Dir 1',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: ['4D75', '4D76'] },
condition: (data) => data.wyrmsLament === 0,
// TODO: this is maybe one case where having one trigger cause two outputs would be helpful
// as this can't be a response as you want different durations on the initial alert and
// the infotext that stays up.
delaySeconds: 0.6,
suppressSeconds: 15,
alertText: (data, matches, output) => {
const dir = data.mirrorThreeDirs.shift();
if (dir === undefined) {
const isLeftCleave = matches.id === '4D75';
return isLeftCleave ? output.right!() : output.left!();
}
return output[dir]!();
},
outputStrings: {
dirNW: Outputs.dirNW,
dirNE: Outputs.dirNE,
dirSE: Outputs.dirSE,
dirSW: Outputs.dirSW,
left: Outputs.left,
right: Outputs.right,
},
},
{
id: 'E8S Mirror Mirror 3 Dir 2',
type: 'Ability',
netRegex: { source: 'Frozen Mirror', id: ['4D90', '4D91'], capture: false },
condition: (data) => data.wyrmsLament === 0,
alertText: (data, _matches, output) => {
const dir = data.mirrorThreeDirs.shift();
if (dir === undefined)
return;
return output[dir]!();
},
outputStrings: {
dirNW: Outputs.dirNW,
dirNE: Outputs.dirNE,
dirSE: Outputs.dirSE,
dirSW: Outputs.dirSW,
},
},
{
id: 'E8S Mirror Mirror 3 Dir 3',
type: 'Ability',
netRegex: { source: 'Frozen Mirror', id: ['4DBB', '4DBC'], capture: false },
condition: (data) => data.wyrmsLament === 0,
alertText: (data, _matches, output) => {
const dir = data.mirrorThreeDirs.shift();
if (dir === undefined)
return;
return output[dir]!();
},
outputStrings: {
dirNW: Outputs.dirNW,
dirNE: Outputs.dirNE,
dirSE: Outputs.dirSE,
dirSW: Outputs.dirSW,
},
},
{
id: 'E8S Hallowed Wings Knockback',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D77', capture: false },
condition: (data) => data.triggerSetConfig.uptimeKnockbackStrat === true,
// This gives a warning within 1.4 seconds, so you can hit arm's length.
delaySeconds: 8.6,
durationSeconds: 1.4,
response: Responses.knockback(),
},
{
id: 'E8S Wyrm\'s Lament',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D7C', capture: false },
response: Responses.aoe(),
},
{
id: 'E8S Wyrm\'s Lament Counter',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: '4D7C', capture: false },
run: (data) => data.wyrmsLament++,
},
{
id: 'E8S Wyrm\'s Lament Mirror',
type: 'StartsUsing',
netRegex: { source: 'Shiva', id: ['4D75', '4D76'] },
condition: (data) => data.wyrmsLament === 1,
infoText: (_data, matches, output) => {
const isLeftCleave = matches.id === '4D75';