Skip to content

Commit

Permalink
M4S improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
xpdota committed Aug 5, 2024
1 parent 2b5462f commit 43f273d
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import gg.xp.reevent.events.SystemEvent;
import gg.xp.xivsupport.callouts.ModifiableCallout;
import gg.xp.xivsupport.callouts.RawModifiedCallout;
import gg.xp.xivsupport.events.actlines.events.AbilityCastCancel;
import gg.xp.xivsupport.events.actlines.events.AbilityCastStart;
import gg.xp.xivsupport.events.actlines.events.AbilityUsedEvent;
import gg.xp.xivsupport.events.actlines.events.BuffApplied;
Expand Down Expand Up @@ -459,6 +460,15 @@ public BuffApplied findOrWaitForBuff(StatusEffectRepository repo, Predicate<Buff
}
}

/**
* Find an active cast, or wait for the matching cast to start.
*
* @param repo The ActiveCastRepository
* @param condition The condition for the cast, i.e. the same thing you would feed to {@link #waitEvent}
* and similar methods
* @param includeExpired Whether to allow already-completed casts.
* @return The cast.
*/
public AbilityCastStart findOrWaitForCast(ActiveCastRepository repo, Predicate<AbilityCastStart> condition, boolean includeExpired) {
var castMaybe = repo.getAll().stream().filter(ct -> {
if (!includeExpired && ct.getResult() != CastResult.IN_PROGRESS) {
Expand All @@ -474,6 +484,27 @@ public AbilityCastStart findOrWaitForCast(ActiveCastRepository repo, Predicate<A
}
}

/**
* Wait for a cast to finish, or return immediately if it already has finished.
*
* @param repo The ActiveCastRepository
* @param cast The cast whose finish you wish to wait for.
* @return The event that ended the cast. Will usually be an {@link AbilityUsedEvent}, but can also be other
* event types such as {@link AbilityCastCancel} for when the cast is interrupted.
*/
public BaseEvent waitCastFinished(ActiveCastRepository repo, AbilityCastStart cast) {
var castMaybe = repo.getAll().stream().filter(ct -> ct.getCast() == cast).findFirst().orElse(null);
if (castMaybe != null) {
BaseEvent end = castMaybe.getEnd();
if (end != null) {
return end;
}
}
return waitEvent(AbilityUsedEvent.class, aue -> aue.getPrecursor() == cast);


}


public List<AbilityUsedEvent> collectAoeHits(Predicate<AbilityUsedEvent> condition) {
List<AbilityUsedEvent> out = new ArrayList<>(8);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import gg.xp.xivsupport.models.ArenaSector;
import gg.xp.xivsupport.models.Position;
import gg.xp.xivsupport.models.XivCombatant;
import gg.xp.xivsupport.models.XivPlayerCharacter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
Expand Down Expand Up @@ -77,15 +78,6 @@ public boolean enabled(EventContext context) {
@NpcCastCallout(0x95EF)
private final ModifiableCallout<AbilityCastStart> wrathOfZeus = ModifiableCallout.durationBasedCall("Wrath of Zeus", "Raidwide");

// TODO: there's another mechanic after this (electrifying witch hunt)
/*
Electrifying:
Outside safe:
3x 95EA burst then 95E5 electrifying
Inside safe:
2x95EA burst then 95E5 electrifying
*/

private final ModifiableCallout<AbilityCastStart> electrifyingInsideSafe = ModifiableCallout.durationBasedCall("Electrifying Witch Hunt: Inside Safe", "Inside");
private final ModifiableCallout<AbilityCastStart> electrifyingOutsideSafe = ModifiableCallout.durationBasedCall("Electrifying Witch Hunt: Outside Safe", "Outside");
@AutoFeed
Expand Down Expand Up @@ -220,17 +212,6 @@ else if (rawStacks == 758) {
@NpcCastCallout(0x95f0)
private final ModifiableCallout<AbilityCastStart> wickedJolt = ModifiableCallout.durationBasedCall("Wicked Jolt", "Tank Buster on {event.target}");

private final ModifiableCallout<AbilityCastStart> 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<AbilityCastStart> sparkBuddies = ModifiableCallout.durationBasedCall("Sidewise Spark + Buddies", "Buddies {safeSide}");
private final ModifiableCallout<AbilityCastStart> sparkSpread = ModifiableCallout.durationBasedCall("Sidewise Spark + Spread", "Spread {safeSide}");

Expand Down Expand Up @@ -297,6 +278,20 @@ else if (sparkMech == SparkMech.Spread) {

});

private final ModifiableCallout<AbilityCastStart> 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<AbilityCastStart> electropeBuddies = ModifiableCallout.durationBasedCall("Sidewise Spark + Buddies", "Buddies {safeSide} with {buddies}");
private final ModifiableCallout<AbilityCastStart> electropeSpread = ModifiableCallout.durationBasedCall("Sidewise Spark + Spread", "Spread {safeSide}");

@AutoFeed
private final SequentialTrigger<BaseEvent> electropeEdge = SqtTemplates.multiInvocation(120_000,
AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95c5),
Expand All @@ -306,7 +301,8 @@ else if (sparkMech == SparkMech.Spread) {
},
(e1, s) -> {
s.updateCall(electropeEdgeInitial, e1);
var myBuff = s.waitEvent(BuffApplied.class, ba -> ba.buffIdMatches(0xF9F) && ba.getTarget().isThePlayer());
int condenserBuffId = 0xF9F;
var myBuff = s.waitEvent(BuffApplied.class, ba -> ba.buffIdMatches(condenserBuffId) && ba.getTarget().isThePlayer());
// Collect hits, stop when we see lightning cage cast
List<AbilityUsedEvent> hits = s.waitEventsUntil(99, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9786),
AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95CE));
Expand Down Expand Up @@ -392,10 +388,35 @@ else if (sparkMech == SparkMech.Spread) {
s.setParam("safeSide", sidewiseSpark.abilityIdMatches(0x95EC) ? ArenaSector.WEST : ArenaSector.EAST);
SparkMech sparkMech = getSparkMech();
if (sparkMech == SparkMech.Buddies) {
s.updateCall(sparkBuddies, sidewiseSpark);
List<XivPlayerCharacter> buddies;
Job playerJob = state.getPlayerJob();
boolean playerIsDps = playerJob.isDps();
if (playerIsLong) {
// look for buddies with no debuff and same role
buddies = state.getPartyList()
.stream()
.filter(pc -> pc.getJob().isDps() == playerIsDps)
.filter(pc -> buffs.isStatusOnTarget(pc, condenserBuffId))
// Prioritize same role first
.sorted(Comparator.comparing(pc -> pc.getJob().getCategory() == playerJob.getCategory() ? 0 : 1))
.toList();
}
else {
// look for buddies with debuff and same role
buddies = buffs.findBuffs(ba -> ba.buffIdMatches(condenserBuffId))
.stream()
.map(BuffApplied::getTarget)
.map(XivPlayerCharacter.class::cast)
.filter(pc -> pc.getJob().isDps() == playerIsDps)
// Prioritize same role first
.sorted(Comparator.comparing(pc -> pc.getJob().getCategory() == playerJob.getCategory() ? 0 : 1))
.toList();
}
s.setParam("buddies", buddies);
s.updateCall(electropeBuddies, sidewiseSpark);
}
else if (sparkMech == SparkMech.Spread) {
s.updateCall(sparkSpread, sidewiseSpark);
s.updateCall(electropeSpread, sidewiseSpark);

}
else {
Expand Down Expand Up @@ -513,7 +534,7 @@ else if (sparkMech == SparkMech.Spread) {


for (int i = 0; i < 3; i++) {
var posCast = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x95D8));
var posCast = s.findOrWaitForCast(casts, acs -> acs.abilityIdMatches(0x95D8), false);
// TODO: use positions if this continues to be flaky

s.waitThenRefreshCombatants(100);
Expand All @@ -533,6 +554,9 @@ else if (playerBuff.buffIdMatches(positronBuff)) {
// get hit by neg
s.updateCall(positronStream, posCast);
}
// delay so we don't immediately re-capture the same event
s.waitCastFinished(casts, posCast);
s.waitMs(1_000);
}
});

Expand All @@ -546,6 +570,7 @@ else if (playerBuff.buffIdMatches(positronBuff)) {
private final SequentialTrigger<BaseEvent> electropeTransplant = SqtTemplates.sq(120_000,
(AbilityCastStart.class), acs -> acs.abilityIdMatches(0x98D3),
(e1, s) -> {
int waitTime = 200;
log.info("Electrope Transplant: Start");
for (int i = 0; i < 2; i++) {

Expand All @@ -554,16 +579,16 @@ else if (playerBuff.buffIdMatches(positronBuff)) {
s.updateCall(transplantCast, cast);
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x90FE));
s.updateCall(transplantMove);
s.waitMs(50);
s.waitMs(waitTime);
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD));
s.updateCall(transplantMove);
s.waitMs(50);
s.waitMs(waitTime);
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD));
s.updateCall(transplantMove);
s.waitMs(50);
s.waitMs(waitTime);
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD));
s.updateCall(transplantMove);
s.waitMs(50);
s.waitMs(waitTime);
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD));
List<XivCombatant> playersThatGotHit = s.waitEventsQuickSuccession(8, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CE))
.stream()
Expand All @@ -575,12 +600,12 @@ else if (playerBuff.buffIdMatches(positronBuff)) {
else {
s.updateCall(transplantMoveFront);
}
s.waitMs(50);
s.waitMs(waitTime);
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x98CD));
s.updateCall(transplantMove);
s.waitMs(50);
s.waitMs(waitTime);
}
s.waitMs(2000);
s.waitMs(1800);
s.updateCall(transition);
});

Expand Down Expand Up @@ -819,8 +844,8 @@ else if (f.vfxIdMatches(794)) {

private final ModifiableCallout<?> midnightSabbathSpreadCardinal = new ModifiableCallout<>("Midnight Sabbath: Spread in Cardinals", "Spread in Cardinals");
private final ModifiableCallout<?> midnightSabbathSpreadIntercards = new ModifiableCallout<>("Midnight Sabbath: Spread in Intercards", "Spread in Intercards");
private final ModifiableCallout<?> midnightSabbathBuddyCardinal = new ModifiableCallout<>("Midnight Sabbath: Spread in Cardinals", "Buddy in Cardinals");
private final ModifiableCallout<?> midnightSabbathBuddyIntercards = new ModifiableCallout<>("Midnight Sabbath: Spread in Intercards", "Buddy in Intercards");
private final ModifiableCallout<?> midnightSabbathBuddyCardinal = new ModifiableCallout<>("Midnight Sabbath: Buddy in Cardinals", "Buddy in Cardinals");
private final ModifiableCallout<?> midnightSabbathBuddyIntercards = new ModifiableCallout<>("Midnight Sabbath: Buddy in Intercards", "Buddy in Intercards");

private record MidnightSabbathMechanic(boolean isDonut, boolean isCardinal, boolean spread) {
boolean cardinalSafe() {
Expand Down Expand Up @@ -885,8 +910,8 @@ If wings, go into the first active set ((all cardinals or all intercardinals fir
if (event.getCategory() == orderCategory) {
// First/second set
switch ((int) event.getData0()) {
case 0x11D3 -> firstSet = true;
case 0x11D4 -> firstSet = false;
case 0x11D3, 0x11D1 -> firstSet = true;
case 0x11D4, 0x11D2 -> firstSet = false;
default -> log.error("Unrecognized: {}", event.getPrimaryValue());
}
}
Expand Down Expand Up @@ -948,7 +973,7 @@ else if (event.getCategory() == weaponCategory) {

Runnable wickedCall = delayedCallWickedThunder(s);
// Wait for 'thundering' cast to finish
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9627));
s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9627, 0x962E));
wickedCall.run();
});

Expand All @@ -960,7 +985,8 @@ else if (event.getCategory() == weaponCategory) {
private final ModifiableCallout<BuffApplied> ionCluster2longPos = ModifiableCallout.<BuffApplied>durationBasedCall("Ion Cluster 2: Long Positron", "Long Positron").autoIcon();
private final ModifiableCallout<BuffApplied> ionCluster2longNeg = ModifiableCallout.<BuffApplied>durationBasedCall("Ion Cluster 2: Long Negatron", "Long Negatron").autoIcon();

private final ModifiableCallout<BuffApplied> ionCluster2baitFirstSet = ModifiableCallout.<BuffApplied>durationBasedCall("Ion Cluster 2: Bait First Set", "Bait {baitLocations}").autoIcon();
private final ModifiableCallout<BuffApplied> ionCluster2baitFirstSet = ModifiableCallout.<BuffApplied>durationBasedCall("Ion Cluster 2: Bait First Set", "Bait {baitLocations}").autoIcon()
.extendedDescription("These callouts sort the bait locations in clockwise order from north.");
private final ModifiableCallout<?> ionCluster2avoidFirstSet = new ModifiableCallout<>("Ion Cluster 2: Take First Tower", "Soak {towers} Towers");
private final ModifiableCallout<BuffApplied> ionCluster2baitSecondSet = ModifiableCallout.<BuffApplied>durationBasedCall("Ion Cluster 2: Bait Second Set", "Bait {baitLocations}").autoIcon();
private final ModifiableCallout<?> ionCluster2avoidSecondSet = new ModifiableCallout<>("Ion Cluster 2: Take Second Tower", "Soak {towers} Tower");
Expand All @@ -969,6 +995,9 @@ else if (event.getCategory() == weaponCategory) {
@NpcCastCallout(0x9614)
private final ModifiableCallout<AbilityCastStart> flameSlash = ModifiableCallout.durationBasedCall("Flame Slash", "Out of Middle, Arena Splitting");

@NpcCastCallout(value = 0x9617, suppressMs = 200)
private final ModifiableCallout<AbilityCastStart> rainingSwordSoakTower = ModifiableCallout.durationBasedCall("Raining Swords: Soak Tower", "Soak Tower");

private final ModifiableCallout<?> rainingSwordNorthmost = new ModifiableCallout<>("Raining Swords: Northmost Safe", "North");
private final ModifiableCallout<?> rainingSwordNorthmiddle = new ModifiableCallout<>("Raining Swords: North-middle Safe", "North-Middle");
private final ModifiableCallout<?> rainingSwordSouthmiddle = new ModifiableCallout<>("Raining Swords: South-middle Safe", "South-Middle");
Expand Down Expand Up @@ -1011,7 +1040,7 @@ public String getPrimaryValue() {
}
}

// This trigger is ONLY responsible for collecting - not callout out!
// This trigger is ONLY responsible for collecting - not any callouts!
@AutoFeed
private final SequentialTrigger<BaseEvent> rainingSwordsColl = SqtTemplates.sq(60_000,
AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9616),
Expand Down Expand Up @@ -1057,9 +1086,9 @@ public String getPrimaryValue() {
throw new RuntimeException("Safe: " + safe);
}
s.accept(new RainingSwordSafeSpotEvent(thisSideRight ? ArenaSector.EAST : ArenaSector.WEST, safe.iterator().next()));

}
});

// This trigger does the actual callouts
@AutoFeed
private final SequentialTrigger<BaseEvent> rainingSwordsCall = SqtTemplates.sq(60_000,
Expand All @@ -1075,6 +1104,9 @@ public String getPrimaryValue() {
// The exception is that if this is the first wave, fire the callout immediately
if (wave == 0) {
if (isMySide) {
// TODO: this callout is a little bit early relative to the "soak tower" call, but the
// delay can't be directly implemented here, as it would interfere with collecting
// the events.
s.updateCall(event.getCallout());
}
// Nothing to do
Expand Down Expand Up @@ -1188,6 +1220,9 @@ else if (towerLocations.equals(Set.of(ArenaSector.NORTH, ArenaSector.SOUTH))) {
.filter(ba -> ba.getRawStacks() == neededGun)
.map(BuffApplied::getTarget)
.map(finalAp::forCombatant)
// The arena sectors are defined north -> CW, so using the ordinal is an acceptable
// way to sort them in a north-first CW sort order.
.sorted(Comparator.comparing(Enum::ordinal))
.toList();
s.setParam("baitLocations", acceptableGuns);
s.updateCall(ionCluster2baitFirstSet, playerBuff);
Expand All @@ -1202,6 +1237,7 @@ else if (towerLocations.equals(Set.of(ArenaSector.NORTH, ArenaSector.SOUTH))) {
.filter(ba -> ba.getRawStacks() == neededGun)
.map(BuffApplied::getTarget)
.map(finalAp::forCombatant)
.sorted(Comparator.comparing(Enum::ordinal))
.toList();
s.setParam("baitLocations", acceptableGuns);
s.updateCall(ionCluster2baitSecondSet, playerBuff);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected List<CalloutInitialValues> getExpectedCalls() {
call(155365, "Clock Positions", "Clock Positions (2.7)"),
call(170825, "2 Short", "2 Short"),
call(174920, "Spark 2 - East, West", "Spark 2 - East, West"),
call(184014, "Buddies West", "Buddies West (6.7)"),
call(184014, "Buddies West with Hahaja Haja, Dedeke Deke", "Buddies West with Hahaja Haja, Dedeke Deke (6.7)"),
call(195328, "East Safe", "East Safe"),
call(204240, "Stack, Multiple Hits", "Stack, Multiple Hits (3.7)"),
call(234671, "West", "West (6.5)"),
Expand Down Expand Up @@ -73,6 +73,7 @@ protected List<CalloutInitialValues> getExpectedCalls() {
call(562598, "Raidwide", "Raidwide (4.7)"),
call(571821, "Later: Knockback West then East", "Later: Knockback West then East (6.7)"),
call(581982, "Out of Middle, Arena Splitting", "Out of Middle, Arena Splitting (5.7)"),
call(591118, "Soak Tower", "Soak Tower (2.7)"),
call(600201, "South-Middle", "South-Middle"),
call(617798, "South", "South"),
call(623192, "North", "North"),
Expand All @@ -85,7 +86,7 @@ protected List<CalloutInitialValues> getExpectedCalls() {
call(689905, "Long Negatron", "Long Negatron (38.0)"),
call(701753, "Soak West, East Towers", "Soak West, East Towers"),
call(710441, "Middle", "Middle (4.7)"),
call(718147, "Bait Northwest, Southwest", "Bait Northwest, Southwest (9.8)"),
call(718147, "Bait Southwest, Northwest", "Bait Southwest, Northwest (9.8)"),
call(726613, "Knockback West then East", "Knockback West then East (4.7)"),
call(742871, "Raidwides", "Raidwides (4.7)"),
call(752537, "Front/Middle", "Front/Middle"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gg.xp.xivsupport.events.state.combatstate;

import gg.xp.reevent.events.BaseEvent;
import gg.xp.reevent.events.Event;
import gg.xp.reevent.events.EventContext;
import gg.xp.reevent.scan.HandleEvents;
Expand Down Expand Up @@ -66,7 +67,7 @@ public void pullStartedEvent(EventContext ctx, PullStartedEvent event) {
}
}

private <X extends Event & HasSourceEntity & HasAbility & HasCastPrecursor> void doEnd(X event) {
private <X extends BaseEvent & HasSourceEntity & HasAbility & HasCastPrecursor> void doEnd(X event) {
CastTracker tracker;
synchronized (lock) {
tracker = cbtCasts.get(event.getSource());
Expand Down
Loading

0 comments on commit 43f273d

Please sign in to comment.