From 99d8c99583e0506203cfcf9336646126b2ba8127 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 31 Jul 2024 21:49:07 -0700 Subject: [PATCH] M4S triggers --- .../xp/xivsupport/triggers/Arcadion/M2S.java | 12 +- .../xp/xivsupport/triggers/Arcadion/M3S.java | 65 ++- .../xp/xivsupport/triggers/Arcadion/M4S.java | 496 ++++++++++++++++++ .../actlines/parsers/Line264Parser.java | 1 + .../src/main/resources/te_changelog.html | 5 + .../events/actlines/Line263Test.java | 34 ++ 6 files changed, 597 insertions(+), 16 deletions(-) create mode 100644 triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M4S.java diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M2S.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M2S.java index 4de98da7f8d1..fc237dc83811 100644 --- a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M2S.java +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M2S.java @@ -97,9 +97,9 @@ private int getPlayerHeartStacks() { } private final ModifiableCallout temptingTwistInitial = ModifiableCallout.durationBasedCall("Tempting Twist: Initial", "In"); -// private final ModifiableCallout temptingTwistAvoidBlobs = new ModifiableCallout<>("Tempting Twist: Initial", "Avoid Blobs"); + // private final ModifiableCallout temptingTwistAvoidBlobs = new ModifiableCallout<>("Tempting Twist: Initial", "Avoid Blobs"); private final ModifiableCallout beelineInitial = ModifiableCallout.durationBasedCall("Beeline: Initial", "Out of Middle"); -// private final ModifiableCallout beelineAvoidBlobs = new ModifiableCallout<>("Tempting Twist: Initial", "In - Avoid Blobs"); + // private final ModifiableCallout beelineAvoidBlobs = new ModifiableCallout<>("Tempting Twist: Initial", "In - Avoid Blobs"); private final ModifiableCallout spread = new ModifiableCallout<>("Poison: Spread", "Spread", 10_000); private final ModifiableCallout buddies = new ModifiableCallout<>("Poison: Stack", "Buddy", 10_000); @@ -114,6 +114,7 @@ private int getPlayerHeartStacks() { // Wait PoisonBuff pb = getPoisonBuff(); // s.waitMs(1000); + // TODO: reports of this being broken if (pb == PoisonBuff.SPREAD) { s.updateCall(spread); } @@ -147,7 +148,7 @@ private int getPlayerHeartStacks() { private final ModifiableCallout beeSting = ModifiableCallout.durationBasedCall("Bee Sting", "Light Parties"); private final ModifiableCallout outCards = new ModifiableCallout<>("Center/Outer Stage: Out+Cardinals", "Out+Cardinals"); -// private final ModifiableCallout outInter = new ModifiableCallout<>("Center/Outer Stage: Out+Intercards", "Out+Intercards"); + // private final ModifiableCallout outInter = new ModifiableCallout<>("Center/Outer Stage: Out+Intercards", "Out+Intercards"); // private final ModifiableCallout inCards = new ModifiableCallout<>("Center/Outer Stage: In+Cardinals", "In+Cardinals"); private final ModifiableCallout inInter = new ModifiableCallout<>("Center/Outer Stage: In+Intercards", "In+Intercards"); private final ModifiableCallout cross = new ModifiableCallout<>("Center/Outer Stage: Cross", "Out+Intercards"); @@ -185,7 +186,6 @@ private int getPlayerHeartStacks() { }); private static final Duration hblOffset = Duration.ofMillis(8200); - // TODO: these have extra cast time private final ModifiableCallout hbl1Initial = ModifiableCallout.durationBasedCallWithOffset("Honey B. Live: 1st Beat", "Raidwide", hblOffset); private final ModifiableCallout hb1towers = new ModifiableCallout<>("Honey B. Live: 1st Beat: Take Towers", "Take {towers} Towers"); private final ModifiableCallout hb1noTowers = new ModifiableCallout<>("Honey B. Live: 1st Beat: Avoid Towers", "Avoid Towers"); @@ -300,7 +300,7 @@ private int getPlayerHeartStacks() { // Given that there is a magic vuln, I do not see how there can be different strategies for this mech // 4 people have short defa, 4 have long defa int defa = 0xF5E; - List all = s.waitEventsQuickSuccession(8, BuffApplied.class, ba -> ba.buffIdMatches(defa)); +// List all = s.waitEventsQuickSuccession(8, BuffApplied.class, ba -> ba.buffIdMatches(defa)); BuffApplied playerBuff = buffs.findStatusOnTarget(state.getPlayer(), defa); // List shorts = all.stream().filter(ba -> ba.getInitialDuration().toSeconds() < 30).toList(); // List longs = all.stream().filter(ba -> ba.getInitialDuration().toSeconds() > 30).toList(); @@ -359,7 +359,7 @@ else if (playerBuff.getInitialDuration().toSeconds() < 30) { .disabledByDefault() .extendedDescription("This is an optional callout which will call out every pair to pop."); - private int nisiBuffGroup(BuffApplied buff) { + private static int nisiBuffGroup(BuffApplied buff) { return ((int) buff.getInitialDuration().toSeconds() + 4) / 16; } 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 4792055bd2b1..4e627b24f0db 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 @@ -13,18 +13,18 @@ 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 gg.xp.xivsupport.models.ArenaSector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + @CalloutRepo(name = "M3S", duty = KnownDuty.M3S) public class M3S extends AutoChildEventHandler implements FilteredEventHandler { private static final Logger log = LoggerFactory.getLogger(M3S.class); @@ -47,10 +47,14 @@ public boolean enabled(EventContext context) { private static final long TODO = 0; @NpcCastCallout(0x93EB) - private final ModifiableCallout quadrupleLariat = ModifiableCallout.durationBasedCall("Quadruple Lariat", "In and Partners"); + private final ModifiableCallout quadrupleLariatIn = ModifiableCallout.durationBasedCall("Quadruple Lariat In", "In and Partners"); + @NpcCastCallout(0x93EA) + private final ModifiableCallout quadrupleLariatOut = ModifiableCallout.durationBasedCall("Quadruple Lariat Out", "Out and Partners"); // Boss cast is 93D8 but it is shorter duration + @NpcCastCallout(0x93E9) + private final ModifiableCallout octupleLariatIn = ModifiableCallout.durationBasedCall("Octuple Lariat In", "In and Spread"); @NpcCastCallout(0x93E8) - private final ModifiableCallout octupleLariat = ModifiableCallout.durationBasedCall("Octuple Lariat", "Out and Spread"); + private final ModifiableCallout octupleLariatOut = ModifiableCallout.durationBasedCall("Octuple Lariat Out", "Out and Spread"); @NpcCastCallout(0x9425) private final ModifiableCallout brutalImpact = ModifiableCallout.durationBasedCall("Brutal Impact", "Raidwide - Multi Hit"); @NpcCastCallout(0x9423) @@ -67,18 +71,52 @@ public boolean enabled(EventContext context) { @NpcCastCallout(0x93ED) private final ModifiableCallout octoboomDiveKb = ModifiableCallout.durationBasedCall("Quadroboom Dive (KB)", "Knockback into Spreads"); + private final ModifiableCallout tagTeamSafeSpot = new ModifiableCallout<>("Tag Team 1: Safe Spot", "{safe} safe"); + @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()); + // The tether units do not move into place until after the "Chain Deathmatch" cast starts +// var tether = s.waitEvent(TetherEvent.class, te -> te.eitherTargetMatches(XivCombatant::isThePlayer)); +// var tetherAdd = tether.getTargetMatching(cbt -> !cbt.isThePlayer()); +// log.info("Tag team: Tethered to: {}", tetherAdd); +// var otherAdd = state.npcsById(tetherAdd.getbNpcId()).stream().filter(cbt -> !(cbt.equals(tetherAdd))).findFirst().orElseThrow(() -> new RuntimeException("Could not find other add")); +// log.info("Tag team: Other add: {} at {}", otherAdd, otherAdd.getPos()); + + // The tether comes out earlier, but we can't do anything with the information anyway, so just use the + // buff instead since we don't have to figure out what the real unit is. + BuffApplied myBuff = s.waitEvent(BuffApplied.class, ba -> ba.buffIdMatches(0xFB3) && ba.getTarget().isThePlayer()); + List casts = s.waitEventsQuickSuccession(2, AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9b2c, 0x9b3e)); + + // The one that we want to get hit by + AbilityCastStart goodCast = casts.stream().filter(acs -> acs.getSource().equals(myBuff.getSource())).findFirst().orElseThrow(); + // The one that we don't want to get hit by + AbilityCastStart badCast = casts.stream().filter(acs -> !acs.getSource().equals(myBuff.getSource())).findFirst().orElseThrow(); + + ArenaSector goodArea = ap.forCombatant(goodCast.getSource()).plusQuads( + // If the enemy is hitting the right, that is 1 quarter CCW + goodCast.abilityIdMatches(0x9b2c) ? -1 : 1 + ); + ArenaSector badArea = ap.forCombatant(badCast.getSource()).plusQuads( + // If the enemy is hitting the right, that is 1 quarter CCW + badCast.abilityIdMatches(0x9b2c) ? -1 : 1 + ); + + // If good is N, and bad is E, then the spot we want is W, so we take the arc from good to bad, and + // go the opposite way. + ArenaSector neededSpot = goodArea.plusEighths(-goodArea.eighthsTo(badArea)); + + s.setParam("safe", neededSpot); + s.updateCall(tagTeamSafeSpot); // Next part is to avoid both hits - is this always the spot that got double hit initially? + // The hits aren't obvious - maybe it's 9B34 lariat combo? + // 9b34/9b2c might just be right? + // 9b35/9b2e might be left? + // At least one seems to be missing its cast location data + // 2c/2e seem to come from the same units that the buff comes from }, (e1, s) -> { // Double tether version @@ -165,6 +203,13 @@ public boolean enabled(EventContext context) { */ /* Spinny mechanic TBD + need to look at rotation and initial position, plus the cross-tthrough + */ + + /* + TODO: bombarian specials + + There is a towers + kb mechanic, but you dodge a side cleave instead of 270 at the end */ } diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M4S.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M4S.java new file mode 100644 index 000000000000..7550a00521ac --- /dev/null +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/Arcadion/M4S.java @@ -0,0 +1,496 @@ +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.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.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.events.triggers.support.PlayerStatusCallout; +import gg.xp.xivsupport.models.ArenaPos; +import gg.xp.xivsupport.models.ArenaSector; +import gg.xp.xivsupport.models.XivCombatant; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@CalloutRepo(name = "M4S", duty = KnownDuty.M4S) +public class M4S extends AutoChildEventHandler implements FilteredEventHandler { + + private static final Logger log = LoggerFactory.getLogger(M4S.class); + + public M4S(XivState state, StatusEffectRepository buffs) { + this.state = state; + this.buffs = buffs; + } + + private XivState state; + private StatusEffectRepository buffs; + private static final ArenaPos ap = new ArenaPos(100, 100, 5, 5); + private static final ArenaPos apOuterCorners = new ArenaPos(100, 100, 12, 12); + + @Override + public boolean enabled(EventContext context) { + return state.dutyIs(KnownDuty.M4S); + } + + @NpcCastCallout(0x95EF) + private final ModifiableCallout wrathOfZeus = ModifiableCallout.durationBasedCall("Wrath of Zeus", "Raidwide"); + // 95EF wrath of zeus raidwide + + // TODO: more IDs? + // 9671 is "inside" + @NpcCastCallout({0x8DEF, 0x9671}) + private final ModifiableCallout bewitchingFlight = ModifiableCallout.durationBasedCall("Betwitching Flight", "Avoid Lines"); + @NpcCastCallout(0x92C2) + private final ModifiableCallout wickedBolt = ModifiableCallout.durationBasedCall("Wicked Bolt", "Stack, Multiple Hits"); + // bewitching flight (0x8DEF): avoid lines + // stay in, then move out, avoid horizontal lines + // bait + + // TODO: witch hunt + // electrifying witch hunt 95e5: ? + // This puts stuff on 4 people + // other 4 have to bait + // bait near/far based on buff + + // widening witch hunt: 95e0: out first + // alternates between close/far + // narrowing witch hunt: 95e1: in first + // alternates between close/far + private final ModifiableCallout wideningOut = new ModifiableCallout<>("Widening/Narrowing Witch Hunt: Out", "Out"); + private final ModifiableCallout wideningIn = new ModifiableCallout<>("Widening/Narrowing Witch Hunt: In", "In"); + + @AutoFeed + private final SequentialTrigger wideningNarrowing = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95e0, 0x95e1), + (e1, s) -> { + // TODO: read the buff + boolean widening = e1.abilityIdMatches(0x95e0); + if (widening) { + s.updateCall(wideningOut); + } + else { + s.updateCall(wideningIn); + } + }); + + @NpcCastCallout(0x95c6) + private final ModifiableCallout witchgleamBasic = ModifiableCallout.durationBasedCall("Witchgleam: Basic", "Stand on Cardinals, Multiple Hits"); + @NpcCastCallout(0x95f0) + private final ModifiableCallout wickedJolt = ModifiableCallout.durationBasedCall("Wicked Jolt", "Tank Buster on {event.target}"); + + private final ModifiableCallout electropeEdgeInitial = ModifiableCallout.durationBasedCall("Electrope Edge", "Clock Positions"); + private final ModifiableCallout electropeEdgeFail = new ModifiableCallout<>("Electrope Edge: Fail/Invalid", "Fail"); + private final ModifiableCallout electropeEdge1long = new ModifiableCallout<>("Electrope Edge: 1 Long", "1 Long"); + private final ModifiableCallout electropeEdge2long = new ModifiableCallout<>("Electrope Edge: 2 Long", "2 Long"); + private final ModifiableCallout electropeEdge2short = new ModifiableCallout<>("Electrope Edge: 2 Short", "2 Short"); + private final ModifiableCallout electropeEdge3short = new ModifiableCallout<>("Electrope Edge: 3 Short", "3 Short"); + + private final ModifiableCallout electropeSafeSpot = new ModifiableCallout<>("Electrope Edge: Nothing", "{safe} Safe"); + private final ModifiableCallout electropeSides = new ModifiableCallout<>("Electrope Edge: Spark II (Sides)", "Spark 2 - {sides}"); + private final ModifiableCallout electropeCorners = new ModifiableCallout<>("Electrope Edge: Spark III (Far Corners)", "Spark 3 - {corners}"); + + private final ModifiableCallout sparkBuddies = ModifiableCallout.durationBasedCall("Sidewise Spark + Buddies", "Buddies {safeSide}"); + private final ModifiableCallout sparkSpread = ModifiableCallout.durationBasedCall("Sidewise Spark + Spread", "Spread {safeSide}"); + + private enum SparkMech { + Buddies, + Spread + } + + private @Nullable SparkMech getSparkMech() { + XivCombatant boss = state.npcById(17322); + if (boss == null) { + log.error("No boss!"); + return null; + } + var buff = buffs.findStatusOnTarget(boss, 0xB9A); + if (buff == null) { + log.error("No buff!"); + return null; + } + switch ((int) buff.getRawStacks()) { + case 752 -> { + return SparkMech.Buddies; + } + case 753 -> { + return SparkMech.Spread; + } + default -> { + log.error("Unknown buff stacks: {}", buff.getRawStacks()); + return null; + } + } + } + + //95c8 symphoniy fantastique + @AutoFeed + private final SequentialTrigger symphonyFantastique = SqtTemplates.sq(30_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95c8), + (e1, s) -> { + // The cross call is handled elsewhere + // Gather Spark II casts + var spark2s = s.waitEvents(2, AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95CA)); + // Gather sidewise spark cast + AbilityCastStart sidewiseSpark = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95ED, 0x95EC)); + // Compute safe + var safeSide = sidewiseSpark.abilityIdMatches(0x95EC) ? ArenaSector.WEST : ArenaSector.EAST; + Set possibleSafe = EnumSet.of(safeSide.plusEighths(-1), safeSide.plusEighths(1)); + spark2s.stream().map(AbilityCastStart::getSource).map(ap::forCombatant).forEach(possibleSafe::remove); + if (possibleSafe.size() == 1) { + s.setParam("safeSide", possibleSafe.iterator().next()); + } + else { + s.setParam("safeSide", ArenaSector.UNKNOWN); + } + SparkMech sparkMech = getSparkMech(); + if (sparkMech == SparkMech.Buddies) { + s.updateCall(sparkBuddies, sidewiseSpark); + } + else if (sparkMech == SparkMech.Spread) { + s.updateCall(sparkSpread, sidewiseSpark); + } + else { + log.error("Sparkmech null!"); + } + + }); + + @AutoFeed + private final SequentialTrigger electropeEdge = SqtTemplates.multiInvocation(120_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95c5), + (e1, s) -> { + // TODO + // or is this handled by other triggers already? + }, + (e1, s) -> { + s.updateCall(electropeEdgeInitial, e1); + var myBuff = s.waitEvent(BuffApplied.class, ba -> ba.buffIdMatches(0xF9F) && ba.getTarget().isThePlayer()); + // Collect hits, stop when we see lightning cage cast + List hits = s.waitEventsUntil(99, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9786), + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95CE)); + int myCount = (int) hits.stream().filter(hit -> hit.getTarget().isThePlayer()).count(); + // 22 short, 42 long + boolean playerIsLong = myBuff.getInitialDuration().toSeconds() > 30; + log.info("Electrope {} {}", myCount, playerIsLong); + if (playerIsLong) { + s.updateCall(switch (myCount) { + case 1 -> electropeEdge1long; + case 2 -> electropeEdge2long; + default -> electropeEdgeFail; + }); + } + else { + s.updateCall(switch (myCount) { + case 2 -> electropeEdge2short; + case 3 -> electropeEdge3short; + default -> electropeEdgeFail; + }); + } + + { + var lightningCageCasts = s.waitEventsQuickSuccession(12, AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95CF)); + s.waitMs(50); + // try cast positions first + var unsafeCorners = lightningCageCasts.stream() + .map(AbilityCastStart::getLocationInfo) + .filter(Objects::nonNull) + .map(DescribesCastLocation::getPos) + .filter(Objects::nonNull) + .map(apOuterCorners::forPosition) + .filter(ArenaSector::isIntercard) + .toList(); + int limit = 5; + while (unsafeCorners.size() != 2) { + s.waitThenRefreshCombatants(100); + // The safe spot is always between the unsafe corners + unsafeCorners = lightningCageCasts.stream() + .map(AbilityCastStart::getSource) + .map(state::getLatestCombatantData) + .map(apOuterCorners::forCombatant) + .filter(ArenaSector::isIntercard) + .toList(); + if (limit-- < 0) { + log.error("unsafeCorners fail!"); + break; + } + } + if (unsafeCorners.size() == 2) { + ArenaSector safe = ArenaSector.tryCombineTwoQuadrants(unsafeCorners); + if (safe == null) { + log.error("Safe fail! unsafeCorners: {}", unsafeCorners); + } + else { + + s.setParam("safe", safe); + s.setParam("sides", List.of(safe.plusEighths(-2), safe.plusEighths(2))); + s.setParam("corners", List.of(safe.plusEighths(-3), safe.plusEighths(3))); + if (playerIsLong) { + // Call safe spot + s.updateCall(electropeSafeSpot); + } + else { + if (myCount == 2) { + s.updateCall(electropeSides); + } + else { + s.updateCall(electropeCorners); + } + } + } + } + else { + // This should be fixed now + log.error("unsafeCorners bad! {} {}", unsafeCorners, lightningCageCasts); + } + } + + // The boss also does a sidewise spark 95ED (cleaving left) or 95EC (cleaving right) + + AbilityCastStart sidewiseSpark = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95ED, 0x95EC)); + s.setParam("safeSide", sidewiseSpark.abilityIdMatches(0x95EC) ? ArenaSector.WEST : ArenaSector.EAST); + SparkMech sparkMech = getSparkMech(); + if (sparkMech == SparkMech.Buddies) { + s.updateCall(sparkBuddies, sidewiseSpark); + } + else if (sparkMech == SparkMech.Spread) { + s.updateCall(sparkSpread, sidewiseSpark); + + } + else { + log.error("Sparkmech null!"); + } + + { + var lightningCageCasts = s.waitEventsQuickSuccession(12, AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95CF)); + s.waitThenRefreshCombatants(100); + // The safe spot is always between the unsafe corners + var unsafeCorners = lightningCageCasts.stream() + .map(AbilityCastStart::getSource) + .map(state::getLatestCombatantData) + .map(apOuterCorners::forCombatant) + .filter(ArenaSector::isIntercard) + .toList(); + if (unsafeCorners.size() == 2) { + ArenaSector safe = ArenaSector.tryCombineTwoQuadrants(unsafeCorners); + s.setParam("safe", safe); + s.setParam("sides", List.of(safe.plusQuads(-1), safe.plusQuads(1))); + s.setParam("corners", List.of(safe.plusQuads(-3), safe.plusQuads(3))); + if (!playerIsLong) { + // Call safe spot + s.updateCall(electropeSafeSpot); + } + else { + if (myCount == 1) { + s.updateCall(electropeSides); + } + else { + s.updateCall(electropeCorners); + } + } + + } + } + + // stack marker handled elsewhere + + + }); + + private final ModifiableCallout westSafe = ModifiableCallout.durationBasedCall("Stampeding Thunder: West Safe", "West"); + private final ModifiableCallout eastSafe = ModifiableCallout.durationBasedCall("Stampeding Thunder: East Safe", "East"); + + @AutoFeed + private final SequentialTrigger stampedingThunderSq = SqtTemplates.sq(30_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x8E2F), + (e1, s) -> { + s.waitThenRefreshCombatants(200); + boolean westHit = state.getLatestCombatantData(e1.getTarget()).getPos().x() < 100; + if (westHit) { + s.updateCall(eastSafe, e1); + } + else { + s.updateCall(westSafe, e1); + } + }); + + private final ModifiableCallout positronStream = ModifiableCallout.durationBasedCall("Positron", "Go {positive}"); + private final ModifiableCallout negatronStream = ModifiableCallout.durationBasedCall("Negatron", "Go {negative}"); + + @PlayerStatusCallout(0xFA2) + private final ModifiableCallout remote = ModifiableCallout.durationBasedCall("Remote Current", "Remote Current").autoIcon(); + @PlayerStatusCallout(0xFA3) + private final ModifiableCallout proximate = ModifiableCallout.durationBasedCall("Proximate Current", "Proximate Current").autoIcon(); + @PlayerStatusCallout(0xFA4) + private final ModifiableCallout spinning = ModifiableCallout.durationBasedCall("Spinning Conductor", "Spinning").autoIcon(); + @PlayerStatusCallout(0xFA5) + private final ModifiableCallout roundhouse = ModifiableCallout.durationBasedCall("Roundhouse Conductor", "Roundhouse - Spread").autoIcon(); + @PlayerStatusCallout(0xFA6) + private final ModifiableCallout collider = ModifiableCallout.durationBasedCall("Collider Conductor", "Get Hit by Protean").autoIcon(); + + @AutoFeed + private final SequentialTrigger electronStream = SqtTemplates.sq(120_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95D7), + (e1, s) -> { +// hits with positron stream (95d8) and negatron stream (95d9) +// get hit by opposite color +// applies to 1 of each group: +// Collider Conductor (7s, FA6) +// 2x Spinning Conductor (5s, FA4) OR ??? +// Remote Conductor (5s, FA2) OR ??? (FA3 far tether) +// You also get 2 stacks of the opposite of what you got hit by (e.g. positron gets negatron): +// Positron FA0 +// Negatron FA1 +// +// Collider (FA6) - need to get hit by protean +// Close tether (FA3) or far tether (FA2) - will shoot protean when tether condition satisfied +// Spinning (FA4) - dynamo +// ? (FA5) - tiny chariot + + // do it again 2 more times + int pos = 0xFA0; + int neg = 0xFA1; + + + for (int i = 0; i < 3; i++) { + var posCast = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95D8)); + s.waitMs(100); + ArenaSector positiveSide = ArenaPos.combatantFacing(state.getLatestCombatantData(posCast.getSource())); + s.setParam("positive", positiveSide); + s.setParam("negative", positiveSide.opposite()); + + var playerBuff = buffs.findStatusOnTarget(state.getPlayer(), ba -> ba.buffIdMatches(pos, neg)); + if (playerBuff == null) { + log.error("Player has no buff!"); + } + else if (playerBuff.buffIdMatches(pos)) { + // get hit by pos + s.updateCall(negatronStream, e1); + } + else { + // get hit by neg + s.updateCall(positronStream, e1); + } + } + }); + + private final ModifiableCallout transplantCast = ModifiableCallout.durationBasedCall("Electrope Transplant: Casted", "Dodge Proteans"); + private final ModifiableCallout transplantMove = new ModifiableCallout<>("Electrope Transplant: Instant", "Move"); + + @AutoFeed + private final SequentialTrigger electropeTransplant = SqtTemplates.sq(120_000, + (AbilityCastStart.class), acs -> acs.abilityIdMatches(0x98D3), + (e1, s) -> { + log.info("Electrope Transplant: Start"); + AbilityCastStart cast = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x90FE)); + s.updateCall(transplantCast, cast); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x90FE)); + s.updateCall(transplantMove); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD)); + s.updateCall(transplantMove); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD)); + s.updateCall(transplantMove); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD)); + s.updateCall(transplantMove); + List playersThatGotHit = s.waitEventsQuickSuccession(8, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CE)) + .stream() + .map(AbilityUsedEvent::getTarget) + .toList(); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD)); + s.updateCall(transplantMove); + }); + + // @NpcCastCallout() // TODO there is no cast for this - need to implement the mechanic before this + private final ModifiableCallout transition = ModifiableCallout.durationBasedCall("Transition", "Multiple Raidwides"); + + //95c6 witchgleam + + // There's a thing where you have to avoid cardinals + + + /* + + // TODO: what is the tell for stack/spread here? + // Maybe buff B9A on the boss indicates pairs? + // Seems the raw stack count matters? + // 752 = pairs? + // 753 = spread? + // wicked bolt 92c2 = stack + from the stack/spread, you get an additional point, + do the pattern again + stack at end of mechanic + + lightning cage (95CF) marks unsafe squares on the 5x5 grid + */ + + /* + Sidewise spark: 95EC cleaving right + */ + + //95c6 witchgleam blind hits intercards + + //95c8 symphoniy fantastique + /* + Spark II 95CA casts in two corners. Those are completely unsafe. + Spark 95C9 casts in two other corners. Those are only unsafe in the spot where spark is casting. + The boss also does a sidewise spark 95ED (cleaving left) or 95EC (cleaving right) + */ + /* + electron stream = wild charge + 95D7 + + */ + + /* + Transitionproteans + alternate/spread + */ + + /* + Electrope transplant 98D3 + need to block for whoever got hit + transition: + multiple raidwides + get knocked south + */ + + /* + p2 + cross trail switch 95F3 - multiple hits + + */ + /* + Buff b9a for witch hunt + These all apply at the start, so need to collect them + 759 bait far? + 758 bait close? + */ + + // POST TRANSITION + @NpcCastCallout(0x95F2) + private final ModifiableCallout crossTailSwitch = ModifiableCallout.durationBasedCall("Cross Tail Switch", "Multiple Raidwides"); + // The two people that did nothing need to grab the tethers + @NpcCastCallout(0x961E) + private final ModifiableCallout mustardBomb = ModifiableCallout.durationBasedCall("Mustard Bombs", "Tethers"); + + // azure thunmder 962f + @NpcCastCallout(0x962F) + private final ModifiableCallout azureThunder = ModifiableCallout.durationBasedCall("Azure Thunder", "Raidwide"); +} diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java index 721a1ddcb731..8bd635603998 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java @@ -29,6 +29,7 @@ enum Fields { @HandleEvents public void consumeAbilityUse(AbilityUsedEvent aue) { + // TODO: this has an issue, where multiple hits of an aoe do not all receive the location info if (aue.isFirstTarget()) { buffer.write(aue); } diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index f7c46954d316..0921bc5ad84a 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -1,8 +1,13 @@ +

July 31, 2024

+
    +
  • Mostly complete triggers for M2S, a few more M3S, some triggers for M4S first half.
  • +

July 30, 2024

  • Game updates for 7.05.
  • +
  • Mostly complete triggers for M1S, some triggers for M2S and M3S.

July 19, 2024

    diff --git a/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line263Test.java b/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line263Test.java index 7faaad8e4836..d49f148530e7 100644 --- a/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line263Test.java +++ b/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line263Test.java @@ -3,6 +3,7 @@ import gg.xp.reevent.events.EventDistributor; import gg.xp.reevent.events.TestEventCollector; import gg.xp.xivsupport.events.ACTLogLineEvent; +import gg.xp.xivsupport.events.actlines.events.AbilityCastStart; import gg.xp.xivsupport.events.actlines.events.CastLocationDataEvent; import gg.xp.xivsupport.sys.XivMain; import org.picocontainer.MutablePicoContainer; @@ -40,5 +41,38 @@ public void positiveTest() { Assert.assertEquals(event.getPos().z(), -10.010); Assert.assertEquals(event.getPos().heading(), 1.57); Assert.assertNull(event.getHeadingOnly()); + // TODO: finish this + } + + @Test + public void testOutOfOrder() { + // These lines can come out-of-order in this sense: + // 1. Cast A + // 2. Cast B + // 3. Extra A + // 4. Extra B + MutablePicoContainer container = XivMain.testingMasterInit(); + TestEventCollector coll = new TestEventCollector(); + EventDistributor dist = container.getComponent(EventDistributor.class); + dist.registerHandler(coll); + String lines = """ + 20|2024-07-31T17:18:09.3820000-05:00|400066A6|Brute Distortion|9B34|Lariat Combo|400066A6|Brute Distortion|5.800|100.00|85.00|0.00|0.00|e4ec9c3ed023ed70 + 20|2024-07-31T17:18:09.3820000-05:00|400066A8|Brute Distortion|9B34|Lariat Combo|400066A8|Brute Distortion|5.800|85.00|100.00|0.00|1.57|930830aa0fddcd3f + 263|2024-07-31T17:18:09.3820000-05:00|400066A6|9B34|88.015|65.004|0.000|0.000|4fda2cc7f09b31e2 + 263|2024-07-31T17:18:09.3820000-05:00|400066A8|9B34|65.004|112.003|0.000|1.571|3c597a02894ae464 + """; + lines.lines().filter(s -> !s.isBlank()).forEach(line -> dist.acceptEvent(new ACTLogLineEvent(line))); + List events = coll.getEventsOf(AbilityCastStart.class); + var firstEvent = events.get(0); + Assert.assertEquals(firstEvent.getLocationInfo().getPos().x(), 88.015); + Assert.assertEquals(firstEvent.getLocationInfo().getPos().y(), 65.004); + Assert.assertEquals(firstEvent.getLocationInfo().getPos().z(), 0.0); + Assert.assertEquals(firstEvent.getLocationInfo().getPos().heading(), 0.0); + var secondEvent = events.get(1); + Assert.assertEquals(secondEvent.getLocationInfo().getPos().x(), 65.004); + Assert.assertEquals(secondEvent.getLocationInfo().getPos().y(), 112.003); + Assert.assertEquals(secondEvent.getLocationInfo().getPos().z(), 0.0); + Assert.assertEquals(secondEvent.getLocationInfo().getPos().heading(), 1.571); + } }