diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java
index 87c2bdae86ec..f246eb8bc1f7 100644
--- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java
+++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java
@@ -384,7 +384,7 @@ public
, Y> Map
> groupEvents(int limit
* @param limit Number of events
* @param timeoutMs Timeout in ms to wait for events
* @param eventClass Class of event
- * @param exclusive false if you would like an event to be allowed to match multiple filters, rather than movingn
+ * @param exclusive false if you would like an event to be allowed to match multiple filters, rather than moving
* on to the next event after a single match.
* @param collectors The list of collectors.
* @param The type of event.
diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/AnyPlayerStatusAdapter.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/AnyPlayerStatusAdapter.java
new file mode 100644
index 000000000000..e716b6d05c67
--- /dev/null
+++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/AnyPlayerStatusAdapter.java
@@ -0,0 +1,49 @@
+package gg.xp.xivsupport.events.triggers.support;
+
+import gg.xp.reevent.events.EventContext;
+import gg.xp.reevent.events.TypedEventHandler;
+import gg.xp.reevent.scan.FeedHandlerChildInfo;
+import gg.xp.reevent.scan.FeedHelperAdapter;
+import gg.xp.reevent.scan.ScanMe;
+import gg.xp.xivsupport.callouts.ModifiableCallout;
+import gg.xp.xivsupport.events.actlines.events.BuffApplied;
+import gg.xp.xivsupport.events.triggers.util.RepeatSuppressor;
+
+import java.time.Duration;
+
+@ScanMe
+public class AnyPlayerStatusAdapter implements FeedHelperAdapter> {
+
+ @Override
+ public Class eventType() {
+ return BuffApplied.class;
+ }
+
+ @Override
+ public TypedEventHandler makeHandler(FeedHandlerChildInfo> info) {
+ long[] castIds = info.getAnnotation().value();
+ long suppMs = info.getAnnotation().suppressMs();
+ RepeatSuppressor supp;
+ if (suppMs >= 0) {
+ supp = new RepeatSuppressor(Duration.ofMillis(suppMs));
+ }
+ else {
+ supp = RepeatSuppressor.noOp();
+ }
+ return new TypedEventHandler<>() {
+ @Override
+ public Class extends BuffApplied> getType() {
+ return BuffApplied.class;
+ }
+
+ @Override
+ public void handle(EventContext context, BuffApplied event) {
+ if (event.buffIdMatches(castIds)) {
+ if (supp.check(event)) {
+ context.accept(info.getHandlerFieldValue().getModified(event));
+ }
+ }
+ }
+ };
+ }
+}
diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/AnyPlayerStatusCallout.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/AnyPlayerStatusCallout.java
new file mode 100644
index 000000000000..8c1248efbc15
--- /dev/null
+++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/AnyPlayerStatusCallout.java
@@ -0,0 +1,20 @@
+package gg.xp.xivsupport.events.triggers.support;
+
+import gg.xp.reevent.scan.FeedHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callout adapter for local player gaining a buff
+ */
+@FeedHelper(PlayerStatusAdapter.class)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AnyPlayerStatusCallout {
+ /**
+ * @return Which status IDs to trigger on
+ */
+ long[] value();
+
+ long suppressMs() default 0;
+}
diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java
index 198090c4702c..96074321fd2e 100644
--- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java
+++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java
@@ -29,7 +29,6 @@ public Class extends BuffApplied> getType() {
public void handle(EventContext context, BuffApplied event) {
if (event.getTarget().isThePlayer() && event.buffIdMatches(castIds)) {
context.accept(info.getHandlerFieldValue().getModified(event));
- return;
}
}
};
diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx1.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx1.java
index 617f3f5b2700..aed86ee4bd12 100644
--- a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx1.java
+++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx1.java
@@ -1,21 +1,55 @@
package gg.xp.xivsupport.triggers.dtex;
+import gg.xp.reevent.events.BaseEvent;
import gg.xp.reevent.events.EventContext;
import gg.xp.reevent.scan.AutoChildEventHandler;
+import gg.xp.reevent.scan.AutoFeed;
import gg.xp.reevent.scan.FilteredEventHandler;
+import gg.xp.reevent.scan.HandleEvents;
+import gg.xp.xivdata.data.*;
import gg.xp.xivdata.data.duties.*;
import gg.xp.xivsupport.callouts.CalloutRepo;
import gg.xp.xivsupport.callouts.ModifiableCallout;
import gg.xp.xivsupport.events.actlines.events.AbilityCastStart;
+import gg.xp.xivsupport.events.actlines.events.AbilityUsedEvent;
+import gg.xp.xivsupport.events.actlines.events.BuffApplied;
+import gg.xp.xivsupport.events.actlines.events.DescribesCastLocation;
+import gg.xp.xivsupport.events.actlines.events.MapEffectEvent;
import gg.xp.xivsupport.events.state.XivState;
+import gg.xp.xivsupport.events.state.combatstate.ActiveCastRepository;
+import gg.xp.xivsupport.events.state.combatstate.CastTracker;
+import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository;
+import gg.xp.xivsupport.events.triggers.seq.SequentialTrigger;
+import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerController;
+import gg.xp.xivsupport.events.triggers.seq.SqtTemplates;
+import gg.xp.xivsupport.events.triggers.support.NpcAbilityUsedCallout;
import gg.xp.xivsupport.events.triggers.support.NpcCastCallout;
+import gg.xp.xivsupport.events.triggers.util.RepeatSuppressor;
+import gg.xp.xivsupport.models.ArenaPos;
+import gg.xp.xivsupport.models.ArenaSector;
+import gg.xp.xivsupport.models.Position;
+import gg.xp.xivsupport.models.XivCombatant;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiConsumer;
@CalloutRepo(name = "EX1", duty = KnownDuty.DtEx1)
public class DTEx1 extends AutoChildEventHandler implements FilteredEventHandler {
- private final XivState state;
- public DTEx1(XivState state) {
+ private static final Logger log = LoggerFactory.getLogger(DTEx1.class);
+
+ private XivState state;
+ private ActiveCastRepository acr;
+ private StatusEffectRepository buffs;
+
+ public DTEx1(XivState state, ActiveCastRepository acr, StatusEffectRepository buffs) {
this.state = state;
+ this.acr = acr;
+ this.buffs = buffs;
}
@Override
@@ -24,16 +58,360 @@ public boolean enabled(EventContext context) {
}
- @NpcCastCallout(0x9008)
+ @NpcCastCallout({0x8FD6, 0x8FD8, 0x8FDA})
private final ModifiableCallout disasterZone = ModifiableCallout.durationBasedCall("Disaster Zone", "Raidwide");
- // TODO: this might be the fake ID. Find real ID.
- @NpcCastCallout(0x8fcc)
- private final ModifiableCallout actualize = ModifiableCallout.durationBasedCall("Sliterhing Strike", "Out");
-
@NpcCastCallout(0x9008)
private final ModifiableCallout tulidisaster = ModifiableCallout.durationBasedCall("Tulidisaster", "Multiple Raidwides");
+ @NpcCastCallout(0x95C4)
+ private final ModifiableCallout skyruinFire = ModifiableCallout.durationBasedCall("Skyruin Fire", "Raidwide with Bleed");
+
+ @NpcCastCallout(0x8FD1)
+ private final ModifiableCallout skyruinIce = ModifiableCallout.durationBasedCallWithOffset("Skyruin Ice", "Raidwide with Bleed", Duration.ofSeconds(6));
+
+// @NpcCastCallout(0x8FC7)
+// private final ModifiableCallout coneAndBuddies = ModifiableCallout.durationBasedCall("Cone+Buddies", "Front Corners and Partners");
+
+// @NpcCastCallout(0x8FCB)
+// private final ModifiableCallout outAndBuddies = ModifiableCallout.durationBasedCall("Out+Buddies", "Out and Partners");
+
+// @NpcCastCallout(0x8FCF)
+// private final ModifiableCallout middleAndBuddies = ModifiableCallout.durationBasedCall("In Middle+Buddies", "Middle and Partners");
+
+ @NpcCastCallout(0x8FC5)
+ private final ModifiableCallout coneAndEruption = ModifiableCallout.durationBasedCall("Cone+Twister", "Front Corners and Bait Twister");
+
+ @NpcCastCallout(0x8FC9)
+ private final ModifiableCallout outAndEruption = ModifiableCallout.durationBasedCall("Out+Twister", "Out and Bait Twister");
+
+ @NpcCastCallout(0x8FCD)
+ private final ModifiableCallout middleAndEruption = ModifiableCallout.durationBasedCall("In Middle+Twister", "Middle and Bait Twister");
+
+ @NpcAbilityUsedCallout(value = 0x8FEF, suppressMs = 200)
+ private final ModifiableCallout eruptionMove = new ModifiableCallout<>("Eruption: Move", "Move");
+
+ private final ModifiableCallout lightning = ModifiableCallout.durationBasedCall("Lightning Soon", "Spread Soon");
+
+ @AutoFeed
+ private final SequentialTrigger lightningDebuffHandler = SqtTemplates.callWhenDurationIs(
+ BuffApplied.class, ba -> ba.buffIdMatches(0xEEF), lightning, Duration.ofSeconds(8));
+
+ private final ModifiableCallout freezingSoon = ModifiableCallout.durationBasedCall("Freeze Soon", "Move Soon").autoIcon();
+ private final ModifiableCallout freezingVerySoon = ModifiableCallout.durationBasedCall("Freeze Very Soon", "Move!").autoIcon();
+
+ @AutoFeed
+ private final SequentialTrigger freezingHandler = SqtTemplates.sq(30_000,
+ BuffApplied.class, ba -> ba.buffIdMatches(0xEEE),
+ (e1, s) -> {
+ s.waitDuration(e1.remainingDurationPlus(Duration.ofSeconds(-10)));
+ s.updateCall(freezingSoon, e1);
+ s.waitDuration(e1.remainingDurationPlus(Duration.ofSeconds(-2)));
+ s.updateCall(freezingVerySoon, e1);
+ }
+ );
+
+ private final ModifiableCallout inferno = ModifiableCallout.durationBasedCall("Inferno Soon", "Light Party Stacks then Eruptions");
+
+ @AutoFeed
+ private final SequentialTrigger infernoHandler = SqtTemplates.callWhenDurationIs(
+ BuffApplied.class, ba -> ba.buffIdMatches(0xEEA), inferno, Duration.ofSeconds(8)
+ );
+
+ private final ModifiableCallout mountainFireInitial = ModifiableCallout.durationBasedCall("Mountain Fire Initial", "Tank Tower and Cleaves");
+ private final ModifiableCallout mountainFireCleave = new ModifiableCallout<>("Mountain Fire Cleave", "{safe}");
+
+ @AutoFeed
+ private final SequentialTrigger mountainFireSq = SqtTemplates.sq(60_000,
+ AbilityCastStart.class, acs -> acs.abilityIdMatches(0x900C),
+ (e1, s) -> {
+ s.updateCall(mountainFireInitial);
+ var e2 = s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x900C));
+ s.setParam("safe", ArenaSector.CENTER);
+ s.updateCall(mountainFireCleave, e2);
+ // TODO: end condition
+ while (true) {
+ var nextEvent = s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x900D, 0x900E, 0x900F, 0x9010, 0x9011, 0x9012));
+ var safe = switch ((int) nextEvent.getAbility().getId()) {
+ case 0x900E, 0x9010 -> ArenaSector.WEST;
+ case 0x900F, 0x9011 -> ArenaSector.CENTER;
+ case 0x900D, 0x9012 -> ArenaSector.EAST;
+ default -> ArenaSector.UNKNOWN;
+ };
+ s.setParam("safe", safe);
+ s.updateCall(mountainFireCleave, nextEvent);
+ }
+ });
+
+ private final ModifiableCallout calamitousInitialTank = new ModifiableCallout<>("Calamitous Cry: Tank", "Line Stacks, In Front of Party");
+ private final ModifiableCallout calamitousInitialNonTank = new ModifiableCallout<>("Calamitous Cry: Non-Tank", "Line Stacks, Behind Tank");
+
+ @AutoFeed
+ private final SequentialTrigger calamitousCry = SqtTemplates.sq(60_000,
+ AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9002),
+ (e1, s) -> {
+ if (state.playerJobMatches(Job::isTank)) {
+ s.updateCall(calamitousInitialTank, e1);
+ }
+ else {
+ s.updateCall(calamitousInitialNonTank, e1);
+ }
+ });
+
+ private final ModifiableCallout stormIceCall = ModifiableCallout.durationBasedCall("Calamity's Frost", "Spread, Up");
+ private final ModifiableCallout stormLightningCall = ModifiableCallout.durationBasedCall("Calamity's Fulgur", "Spread, Down");
+
+ @AutoFeed
+ private final SequentialTrigger stormIceSq = SqtTemplates.callWhenDurationIs(
+ BuffApplied.class, ba -> ba.getTarget().isThePlayer() && ba.buffIdMatches(0xEEC),
+ stormIceCall, Duration.ofSeconds(8));
+
+ @AutoFeed
+ private final SequentialTrigger stormLightningSq = SqtTemplates.callWhenDurationIs(
+ BuffApplied.class, ba -> ba.getTarget().isThePlayer() && ba.buffIdMatches(0xEF0),
+ stormLightningCall, Duration.ofSeconds(8));
+
+ private Position bestLocationFor(AbilityCastStart event) {
+ DescribesCastLocation li = event.getLocationInfo();
+ if (li != null) {
+ return li.getPos();
+ }
+ else {
+ return state.getLatestCombatantData(event.getSource()).getPos();
+ }
+ }
+
+ private final ArenaPos arena = new ArenaPos(100, 100, 3, 3);
+ private final ModifiableCallout hailOfFeathersStart = ModifiableCallout.durationBasedCall("Hail of Feathers: Start", "Start {start}, Rotate {{ clockwise ? 'Clockwise' : 'Counter-clockwise' }}");
+ private final ModifiableCallout> hailOfFeathersEnd = new ModifiableCallout<>("Hail of Feathers: End", "Kill {end} feather");
+
+ @AutoFeed
+ private final SequentialTrigger hailOfFeathersSq = SqtTemplates.sq(30_000,
+ // Boss cast is 8FDD
+ // Other casts are 901D - 9022, based on duration. 901D is the shortest duration, so we care about that one.
+ AbilityCastStart.class, acs -> acs.abilityIdMatches(0x8FDD),
+ (e1, s) -> {
+ AbilityCastStart firstCast;
+ AbilityCastStart finalCast;
+ while (true) {
+ firstCast = acr.getActiveCastById(0x901D).map(CastTracker::getCast).orElse(null);
+ finalCast = acr.getActiveCastById(0x9022).map(CastTracker::getCast).orElse(null);
+ if (firstCast == null || finalCast == null) {
+ s.waitMs(100);
+ }
+ else {
+ break;
+ }
+ }
+ ArenaSector first = arena.forPosition(bestLocationFor(firstCast));
+ ArenaSector last = arena.forPosition(bestLocationFor(finalCast));
+ // e.g. if first is west and last is northwest, then first.eightsTo(last) == 1, but we are rotating ccw
+ boolean clockwise = first.eighthsTo(last) < 0;
+
+ s.setParam("first", first);
+ s.setParam("start", first.opposite());
+
+ s.setParam("last", last);
+ s.setParam("clockwise", clockwise);
+ s.setParam("end", last.opposite());
+
+
+ s.updateCall(hailOfFeathersStart, firstCast);
+
+ s.waitMs(12_000);
+
+ s.updateCall(hailOfFeathersEnd);
+ });
+
+ @NpcCastCallout(value = 0x8FC1, suppressMs = 3000)
+ private final ModifiableCallout cracklingCataclysm = ModifiableCallout.durationBasedCall("Crackling Cataclysm", "Move");
+
+ private final ModifiableCallout thunderousBreath = ModifiableCallout.durationBasedCall("Thunderous Breath Safe Row", "Row {safeRow} Safe - Go Up");
+ private final ModifiableCallout thunderousBreathError = ModifiableCallout.durationBasedCall("Thunderous Breath (Error)", "Find Safe Row, Go Up");
+ private final ModifiableCallout thunderousBreathSpread = new ModifiableCallout<>("Thunderous Breath Spread", "Spread");
+
+ @AutoFeed
+ private final SequentialTrigger thunderousBreathSq = SqtTemplates.sq(10_000,
+ AbilityCastStart.class, acs -> acs.abilityIdMatches(0x8FE2),
+ (e1, s) -> {
+ List orbs;
+ do {
+ s.waitThenRefreshCombatants(100);
+ orbs = state.npcsById(16770);
+ } while (orbs.size() < 5);
+ List yCoords = orbs.stream().map(cbt -> state.getLatestCombatantData(cbt).getPos().y()).toList();
+ // The orb positions are 87.5, 92.5, ... 112.5
+ Integer safeRow = null;
+ for (int i = 1; i <= 6; i++) {
+ double rowStart = 82 + 5 * i;
+ double rowEnd = rowStart + 1;
+ if (yCoords.stream().noneMatch(yc -> yc >= rowStart && yc <= rowEnd)) {
+ safeRow = i;
+ }
+ }
+ if (safeRow != null) {
+ s.setParam("safeRow", safeRow);
+ s.updateCall(thunderousBreath, e1);
+ }
+ else {
+ s.updateCall(thunderousBreathError, e1);
+ }
+ var afterOrb = s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x985A));
+ s.updateCall(thunderousBreathSpread, afterOrb);
+ });
+
+ private final ModifiableCallout ruinfallTowerTank = ModifiableCallout.durationBasedCall("Ruinfall Tower: Tank", "Tank Tower, Knockback");
+ private final ModifiableCallout ruinfallTowerNonTank = ModifiableCallout.durationBasedCall("Ruinfall Tower: Non-Tank", "Avoid Tower, Knockback");
+
+ @AutoFeed
+ private final SequentialTrigger ruinfallTower = SqtTemplates.sq(30_000,
+ AbilityCastStart.class, acs -> acs.abilityIdMatches(0x8FFD),
+ (e1, s) -> {
+ if (state.playerJobMatches(Job::isTank)) {
+ s.updateCall(ruinfallTowerTank, e1);
+ }
+ else {
+ s.updateCall(ruinfallTowerNonTank, e1);
+ }
+ }
+ );
+
+ private final ModifiableCallout> calamityTankAway = new ModifiableCallout<>("Calamity's Embers - Tank", "Tank Buster - Avoid Party");
+ private final ModifiableCallout> calamityHealerStacks = new ModifiableCallout<>("Calamity's Embers - Non-Tank", "Healer Stacks - Avoid Tanks");
+ private final ModifiableCallout> chillingCataclysmSafe = new ModifiableCallout<>("Chilling Cataclysm - Ice Safe Spot", "{safe} safe");
+
+ private void chillingCataclysmHandler(SequentialTriggerController> s) {
+ Position northernMost;
+ do {
+ s.waitThenRefreshCombatants(100);
+ // https://github.com/OverlayPlugin/cactbot/blob/main/ui/raidboss/data/07-dt/trial/valigarmanda-ex.ts#L694
+ northernMost = state.npcsById(16667).stream().map(XivCombatant::getPos).filter(Objects::nonNull).filter(pos -> pos.y() <= 90).findFirst().orElse(null);
+ } while (northernMost == null);
+ ArenaSector safe = northernMost.x() > 100 ? ArenaSector.NORTHWEST : ArenaSector.NORTHEAST;
+ s.setParam("safe", safe);
+ s.updateCall(chillingCataclysmSafe);
+
+ }
+
+ @AutoFeed
+ private final SequentialTrigger calamitysEmbers = SqtTemplates.sq(150_000,
+ BuffApplied.class, ba -> ba.buffIdMatches(0xEED),
+ (e1, s) -> {
+ s.waitMs(4_000);
+ if (buffs.isStatusOnTarget(state.getPlayer(), 0xEED)) {
+ s.updateCall(calamityTankAway);
+ }
+ else {
+ s.updateCall(calamityHealerStacks);
+ }
+ s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x8FC8, 0x8FCC, 0x8FD0));
+ chillingCataclysmHandler(s);
+
+ });
+
+ private final ModifiableCallout iceCone = ModifiableCallout.durationBasedCall("Cone", "Cone, North Corners");
+ private final ModifiableCallout iceIn = ModifiableCallout.durationBasedCall("In", "In");
+ private final ModifiableCallout iceOut = ModifiableCallout.durationBasedCall("Out", "Out");
+ private final ModifiableCallout avalancheWithCone = ModifiableCallout.durationBasedCall("Avalanche + Cone", "Cone, {safe}");
+ private final ModifiableCallout avalancheWithIn = ModifiableCallout.durationBasedCall("Avalanche + In", "In, {safe}");
+ private final ModifiableCallout avalancheWithOut = ModifiableCallout.durationBasedCall("Avalanche + Out", "Out, {safe}");
+
+ @AutoFeed
+ private final SequentialTrigger calamitysEmbersSafeSpots = SqtTemplates.sq(150_000,
+ BuffApplied.class, ba -> ba.buffIdMatches(0xEED),
+ (e1, s) -> {
+ var mapEffect = s.waitEvent(MapEffectEvent.class, mee -> (mee.getFlags() == 0x0002_0001 || mee.getFlags() == 0x00200010) && mee.getLocation() == 3);
+ var cast = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x8FC8, 0x8FCC, 0x8FD0));
+ ArenaSector avalancheSafe;
+ if (mapEffect.getFlags() == 0x0002_0001) {
+ avalancheSafe = ArenaSector.NORTHEAST;
+ }
+ else {
+ avalancheSafe = ArenaSector.SOUTHWEST;
+ }
+ s.setParam("avalancheSafe", avalancheSafe);
+ ArenaSector safe;
+ if (cast.abilityIdMatches(0x8FC8) && avalancheSafe == ArenaSector.SOUTHWEST) {
+ safe = ArenaSector.NORTHWEST;
+ }
+ else {
+ safe = avalancheSafe;
+ }
+ s.setParam("safe", safe);
+ switch ((int) cast.getAbility().getId()) {
+ case 0x8FC8 -> s.updateCall(avalancheWithCone, cast);
+ case 0x8FCC -> s.updateCall(avalancheWithOut, cast);
+ case 0x8FD0 -> s.updateCall(avalancheWithIn, cast);
+ }
+ });
+
+ private static final BiConsumer> noop = (a, b) -> {
+ };
+
+ @AutoFeed
+ private final SequentialTrigger iceAoeIntoChillingCataclysm = SqtTemplates.multiInvocation(30_000,
+ AbilityCastStart.class, acs -> acs.abilityIdMatches(0x8FC8, 0x8FCC, 0x8FD0),
+ noop,
+ noop,
+ noop,
+ noop,
+ noop,
+ (cast, s) -> {
+ if (calamitysEmbersSafeSpots.isActive()) {
+ return;
+ }
+ // This is for only one specific instance of the fight
+ switch ((int) cast.getAbility().getId()) {
+ case 0x8FC8 -> s.updateCall(iceCone, cast);
+ case 0x8FCC -> s.updateCall(iceOut, cast);
+ case 0x8FD0 -> s.updateCall(iceIn, cast);
+ }
+ chillingCataclysmHandler(s);
+ });
+
+ @NpcCastCallout(0x8FF0)
+ private final ModifiableCallout freezingDust = ModifiableCallout.durationBasedCall("Freezing Dust", "Move");
+
+ @NpcCastCallout(0x995)
+ private final ModifiableCallout wrathUnfurled = ModifiableCallout.durationBasedCall("Wrath Unfurled", "Raidwide");
+
+ /*
+ Notes
+ Spikesickle 8FF2 => Start West
+ */
+
+ private final ModifiableCallout spikesicleWest = new ModifiableCallout<>("Spikesicle: Start West", "Start West");
+ private final ModifiableCallout spikesicleEast = new ModifiableCallout<>("Spikesicle: Start East", "Start East");
+ private final ModifiableCallout eruptionWest = new ModifiableCallout<>("Eruption: West Safe", "West Safe");
+ private final ModifiableCallout eruptionEast = new ModifiableCallout<>("Eruption: East Safe", "East Safe");
+
+ private final RepeatSuppressor mapEffectSupp = new RepeatSuppressor(Duration.ofSeconds(4));
+
+ @HandleEvents
+ public void mapEffects(EventContext context, MapEffectEvent event) {
+ // Spikecicle
+ if (event.getFlags() == 0x00020004L) {
+ if (!mapEffectSupp.check(event)) {
+ return;
+ }
+ switch ((int) event.getLocation()) {
+ case 4 -> context.accept(spikesicleWest.getModified(event));
+ case 5 -> context.accept(spikesicleEast.getModified(event));
+ default -> log.warn("Unknown spikecicle: {}", event);
+ }
+ }
+ // Volcano
+ else if (event.getFlags() == 0x00200010) {
+ if (!mapEffectSupp.check(event)) {
+ return;
+ }
+ switch ((int) event.getLocation()) {
+ case 0xE -> context.accept(eruptionWest.getModified(event));
+ case 0xF -> context.accept(eruptionEast.getModified(event));
+ default -> log.warn("Unknown Volcano: {}", event);
+ }
+ }
+ }
}
diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx2.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx2.java
index 0f20f4da00b0..01bb9f02ac11 100644
--- a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx2.java
+++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/dtex/DTEx2.java
@@ -50,4 +50,6 @@ public boolean enabled(EventContext context) {
@NpcCastCallout(0x9a88)
private final ModifiableCallout projectionOfTurmoil = ModifiableCallout.durationBasedCall("Projection of Turmoil", "Take Stacks Sequentially");
+
+
}