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 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 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"); + + }