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 f246eb8bc1f7..a45a85600cdd 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 @@ -14,6 +14,7 @@ import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository; import gg.xp.xivsupport.speech.CalloutEvent; import gg.xp.xivsupport.speech.HasCalloutTrackingKey; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,9 +115,7 @@ public void refreshCombatants(long delay) { public void forceExpire() { synchronized (lock) { - // TODO: expire on wipe? // Also make it configurable as to whether or not a wipe ends the trigger -// if (event.getHappenedAt().isAfter(expiresAt)) { log.info("Sequential trigger force expired"); die = true; lock.notifyAll(); @@ -245,12 +244,16 @@ public void updateCall(ModifiableCallout call) { * NOT supplying an event. * * @param call The callout + * @return The modified call. */ - public void updateCall(ModifiableCallout call, C event) { + @Contract("null, _ -> null; !null, _ -> !null") + public @Nullable RawModifiedCallout updateCall(ModifiableCallout call, C event) { if (call == null) { - return; + return null; } - updateCall(call.getModified(event, getParams())); + RawModifiedCallout out = call.getModified(event, getParams()); + updateCall(out); + return out; } /** @@ -262,12 +265,16 @@ public void updateCall(ModifiableCallout call, C event) { * supplying an event. * * @param call The callout + * @return The modified call */ - public void call(ModifiableCallout call) { + @Contract("null -> null; !null -> !null") + public @Nullable RawModifiedCallout call(ModifiableCallout call) { if (call == null) { - return; + return null; } - accept(call.getModified(getParams())); + RawModifiedCallout out = call.getModified(getParams()); + accept(out); + return out; } /** @@ -279,12 +286,16 @@ public void call(ModifiableCallout call) { * NOT supplying an event. * * @param call The callout + * @return The modified call */ - public void call(ModifiableCallout call, C event) { + @Contract("null, _ -> null; !null, _ -> !null") + public @Nullable RawModifiedCallout call(ModifiableCallout call, C event) { if (call == null) { - return; + return null; } - accept(call.getModified(event, getParams())); + RawModifiedCallout out = call.getModified(event, getParams()); + accept(out); + return out; } /** diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M3S.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M3S.java index d7e28cab2f71..4792055bd2b1 100644 --- a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M3S.java +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M3S.java @@ -1,12 +1,27 @@ package gg.xp.xivsupport.triggers.Arcadion; +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.xivdata.data.*; import gg.xp.xivdata.data.duties.*; import gg.xp.xivsupport.callouts.CalloutRepo; +import gg.xp.xivsupport.callouts.ModifiableCallout; +import gg.xp.xivsupport.callouts.RawModifiedCallout; +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.BuffRemoved; +import gg.xp.xivsupport.events.actlines.events.TetherEvent; import gg.xp.xivsupport.events.state.XivState; import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository; +import gg.xp.xivsupport.events.triggers.seq.SequentialTrigger; +import gg.xp.xivsupport.events.triggers.seq.SqtTemplates; +import gg.xp.xivsupport.events.triggers.support.NpcCastCallout; +import gg.xp.xivsupport.models.ArenaPos; +import gg.xp.xivsupport.models.XivCombatant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,11 +36,136 @@ public M3S(XivState state, StatusEffectRepository buffs) { private XivState state; private StatusEffectRepository buffs; + private static final ArenaPos ap = new ArenaPos(100, 100, 5, 5); @Override public boolean enabled(EventContext context) { return state.dutyIs(KnownDuty.M3S); } + // TODO + private static final long TODO = 0; + @NpcCastCallout(0x93EB) + private final ModifiableCallout quadrupleLariat = ModifiableCallout.durationBasedCall("Quadruple Lariat", "In and Partners"); + // Boss cast is 93D8 but it is shorter duration + @NpcCastCallout(0x93E8) + private final ModifiableCallout octupleLariat = ModifiableCallout.durationBasedCall("Octuple Lariat", "Out and Spread"); + @NpcCastCallout(0x9425) + private final ModifiableCallout brutalImpact = ModifiableCallout.durationBasedCall("Brutal Impact", "Raidwide - Multi Hit"); + @NpcCastCallout(0x9423) + private final ModifiableCallout knuckleSandwich = ModifiableCallout.durationBasedCall("Knuckle Sandwich", "Tank Buster - Multi Hit"); + + // Real is 93E0 + @NpcCastCallout(0x93F5) + private final ModifiableCallout quadroboomDiveOut = ModifiableCallout.durationBasedCall("Quadroboom Dive (Out)", "Out into Role Pairs"); + @NpcCastCallout(0x93F6) + private final ModifiableCallout quadroboomDiveKb = ModifiableCallout.durationBasedCall("Quadroboom Dive (KB)", "Knockback into Role Pairs"); + // Real is 93EF + @NpcCastCallout(0x93EC) + private final ModifiableCallout octoboomDiveOut = ModifiableCallout.durationBasedCall("Quadroboom Dive (Out)", "Out into Spreads"); + @NpcCastCallout(0x93ED) + private final ModifiableCallout octoboomDiveKb = ModifiableCallout.durationBasedCall("Quadroboom Dive (KB)", "Knockback into Spreads"); + + @AutoFeed + private final SequentialTrigger tagTeam = SqtTemplates.multiInvocation(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x93E7), + (e1, s) -> { + log.info("Tag team: start"); + var tether = s.waitEvent(TetherEvent.class, te -> te.eitherTargetMatches(XivCombatant::isThePlayer)); + var tetherAdd = tether.getTargetMatching(cbt -> !cbt.isThePlayer()); + log.info("Tethered to: {} at {}", tetherAdd, tetherAdd.getPos()); + var otherAdd = state.npcsById(tetherAdd.getbNpcId()).stream().filter(cbt -> !(cbt.equals(tetherAdd))).findFirst().orElseThrow(() -> new RuntimeException("Could not find other add")); + log.info("Other add: {} at {}", otherAdd, otherAdd.getPos()); + + // Next part is to avoid both hits - is this always the spot that got double hit initially? + }, (e1, s) -> { + // Double tether version + + }); + + @AutoFeed + private final SequentialTrigger finalFusedown = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9406), + (e1, s) -> { + log.info("Final fusedown: start"); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.getPrecursor() == e1); + s.waitMs(1000); + var playerBuff = buffs.findStatusOnTarget(state.getPlayer(), ba -> ba.buffIdMatches(0xFAF, 0xFB0)); + if (playerBuff == null) { + throw new RuntimeException("No debuff on player!"); + } + // Player has long + boolean playerLong = playerBuff.buffIdMatches(0xFB0); + if (state.playerJobMatches(Job::isDps)) { + // DPS has long + log.info("DPS long"); + } + else { + // Supports have long + log.info("Supports long"); + } + // TODO: determine safe spot + if (playerLong) { + + } + }); + + private final ModifiableCallout fuseFieldInitial = ModifiableCallout.durationBasedCall("Fuse Field: Initial", "Pop Fuses Sequentially"); + private final ModifiableCallout fuseFieldShort = ModifiableCallout.durationBasedCall("Fuse Field: Short", "Short Fuse").autoIcon(); + private final ModifiableCallout fuseFieldLong = ModifiableCallout.durationBasedCall("Fuse Field: Long", "Long Fuse").autoIcon(); + @AutoFeed + private final SequentialTrigger fuseField = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x93EE), + (e1, s) -> { + s.updateCall(fuseFieldInitial, e1); + var buff = s.waitEvent(BuffApplied.class, ba -> ba.getTarget().isThePlayer() && ba.buffIdMatches(0xFB4)); + RawModifiedCallout call; + if (buff.getInitialDuration().toSeconds() > 30) { + call = s.updateCall(fuseFieldLong, buff); + } + else { + call = s.updateCall(fuseFieldShort, buff); + } + s.waitBuffRemoved(buffs, buff); + call.forceExpire(); + }); + + // Bombs: role spread + avoid bombs + + @AutoFeed + private final SequentialTrigger quadroboomSpecial = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x940A), + (e1, s) -> { + + }); + + + // debuffs + bombs + // seems to be role assigned + // FB1, FB2 and FBA, FBB debuffs seem to be related + // FAF = 5 second FB1 on NPC + // FB0 = 10 second FB2 on NPC + // FB8 = 5 second FBA on player + // FB9 = 10 second FBB on player + // short debuff to short fuse + // It's curtain call but with 2 debuff timers instead of four + + + /* + Knockback towers: + Some towers go off earlier and you need to get knocked into the next set of towers + Boss does a 270% cleave, behind him is safe? so get knocked back to him + Pattern 1: sides, corners, center + Pattern 2: + */ + /* + Second chain - chained to boss and add, so need to get hit by boss's 270 degree cleave (then still dodge second set) + + */ + /* + Spinny mechanic TBD + */ } + + diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java b/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java index 7a4650672a96..5e09b5274dfa 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java @@ -35,6 +35,7 @@ public class RawModifiedCallout extends BaseEvent implements HasCalloutTracki private final CalloutTrackingKey key = new CalloutTrackingKey(); private static final int maxErrors = 10; private int errorCount; + private volatile boolean forceExpired; public RawModifiedCallout(String description, String tts, String text, @Nullable String sound, @Nullable X event, Map arguments, Function guiProvider, Predicate> expiry, @Nullable Color colorOverride, @Nullable ModifiedCalloutHandle handle) { this.description = description; @@ -70,7 +71,7 @@ public Map getArguments() { } public BooleanSupplier getExpiry() { - return () -> expiry.test(this); + return () -> expiry.test(this) || this.forceExpired; } public @Nullable HasCalloutTrackingKey getReplaces() { @@ -130,6 +131,10 @@ public String getDescription() { return description; } + public void forceExpire() { + this.forceExpired = true; + } + @Override public String toString() { return "RawModifiedCallout{" + diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java b/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java index db50c69b7d8e..d87adfcd2312 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java @@ -81,6 +81,7 @@ public final boolean isExpired() { public abstract boolean isNaturallyExpired(); + @SuppressWarnings("unused") // used in scripts public void forceExpire() { this.forceExpire = true; }