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 96074321fd2e..e495ee341e46 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 @@ -6,11 +6,23 @@ import gg.xp.reevent.scan.FeedHelperAdapter; import gg.xp.reevent.scan.ScanMe; import gg.xp.xivsupport.callouts.ModifiableCallout; +import gg.xp.xivsupport.callouts.RawModifiedCallout; import gg.xp.xivsupport.events.actlines.events.BuffApplied; +import gg.xp.xivsupport.events.state.combatstate.CastTracker; +import gg.xp.xivsupport.events.state.combatstate.StatusEffectCurrentStatus; +import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository; + +import java.util.Optional; @ScanMe public class PlayerStatusAdapter implements FeedHelperAdapter> { + private final StatusEffectRepository buffs; + + public PlayerStatusAdapter(StatusEffectRepository buffs) { + this.buffs = buffs; + } + @Override public Class eventType() { return BuffApplied.class; @@ -19,6 +31,7 @@ public Class eventType() { @Override public TypedEventHandler makeHandler(FeedHandlerChildInfo> info) { long[] castIds = info.getAnnotation().value(); + PlayerStatusCallout ann = info.getAnnotation(); return new TypedEventHandler<>() { @Override public Class getType() { @@ -28,7 +41,14 @@ public Class getType() { @Override public void handle(EventContext context, BuffApplied event) { if (event.getTarget().isThePlayer() && event.buffIdMatches(castIds)) { - context.accept(info.getHandlerFieldValue().getModified(event)); + RawModifiedCallout modified = info.getHandlerFieldValue().getModified(event); + if (ann.cancellable()) { + modified.addExpiryCondition(() -> { + StatusEffectCurrentStatus cs = buffs.statusOf(event); + return cs != StatusEffectCurrentStatus.ACTIVE; + }); + } + context.accept(modified); } } }; diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java index 99a073ea4e88..2e820e8d93a9 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java @@ -15,4 +15,11 @@ * @return Which status IDs to trigger on */ long[] value(); + + /** + * @return Whether the callout should be removed if the buff is removed. A buff being replaced/refreshed + * counts as being removed. + */ + boolean cancellable() default false; + } diff --git a/triggers/titan-jails/src/main/java/gg/xp/xivsupport/events/triggers/jails/JailSolver.java b/triggers/titan-jails/src/main/java/gg/xp/xivsupport/events/triggers/jails/JailSolver.java index 4860a2372470..4052f592a659 100644 --- a/triggers/titan-jails/src/main/java/gg/xp/xivsupport/events/triggers/jails/JailSolver.java +++ b/triggers/titan-jails/src/main/java/gg/xp/xivsupport/events/triggers/jails/JailSolver.java @@ -63,8 +63,7 @@ public JailSolver(PersistenceProvider persistence, XivState state) { @Override public boolean enabled(EventContext context) { -// return true; - return overrideZoneLock.get() || context.getStateInfo().get(XivState.class).zoneIs(0x309L); + return overrideZoneLock.get() || state.dutyIs(KnownDuty.UWU); } @HandleEvents @@ -145,8 +144,7 @@ public void jailedPlayerDied(EventContext context, EntityKilledEvent event) { @HandleEvents public void handleJailCast(EventContext context, AbilityUsedEvent event) { // Check ability ID - we only care about these two - long id = event.getAbility().getId(); - if (id != 0x2B6B && id != 0x2B6C) { + if (!event.abilityIdMatches(0x2B6B, 0x2B6C)) { return; } XivCombatant target = event.getTarget(); diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCar.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCar.java new file mode 100644 index 000000000000..f653f1422707 --- /dev/null +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCar.java @@ -0,0 +1,441 @@ +package gg.xp.xivsupport.triggers.car; + +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.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.HeadMarkerEvent; +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.SqtTemplates; +import gg.xp.xivsupport.events.triggers.support.NpcCastCallout; +import gg.xp.xivsupport.events.triggers.support.PlayerHeadmarker; +import gg.xp.xivsupport.events.triggers.support.PlayerStatusCallout; +import gg.xp.xivsupport.models.ArenaSector; +import gg.xp.xivsupport.models.XivCombatant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.List; + +@CalloutRepo(name = "CoD (CAR) Triggers", duty = KnownDuty.CodCar) +public class CodCar extends AutoChildEventHandler implements FilteredEventHandler { + + private static final Logger log = LoggerFactory.getLogger(CodCar.class); + private final XivState state; + private final StatusEffectRepository buffs; + private ActiveCastRepository casts; + + public CodCar(XivState state, StatusEffectRepository buffs, ActiveCastRepository casts) { + this.state = state; + this.buffs = buffs; + this.casts = casts; + } + + public CodCarSection getPlayerSection() { + if (buffs.isStatusOnTarget(state.getPlayer(), 0x1051)) { + log.info("My area: INSIDE"); + return CodCarSection.INSIDE; + } + else if (buffs.isStatusOnTarget(state.getPlayer(), 0x1052)) { + var mySection = CodCarSection.forPos(state.getPlayer().getPos()); + log.info("My area: {}", mySection); + return mySection; + } + else { + return null; + } + } + + @Override + public boolean enabled(EventContext context) { + return state.dutyIs(KnownDuty.CodCar); + } + + // Intro + @NpcCastCallout(0x9DFC) + private final ModifiableCallout bladeOfDarknessRight = ModifiableCallout.durationBasedCall("Blade of Darkness: Right Safe", "Right"); + @NpcCastCallout(0x9DFE) + private final ModifiableCallout bladeOfDarknessLeft = ModifiableCallout.durationBasedCall("Blade of Darkness: Left Safe", "Left"); + @NpcCastCallout(0x9E00) + private final ModifiableCallout bladeOfDarknessOut = ModifiableCallout.durationBasedCall("Blade of Darkness: Out", "Out"); + + // P1 + + private final ModifiableCallout grimEmbraceFrontInitial = new ModifiableCallout("Grim Embrace: Initial Front Dodge", "Later: Dodge Forwards").autoIcon(); + private final ModifiableCallout grimEmbraceRearInitial = new ModifiableCallout("Grim Embrace: Initial Rear Dodge", "Later: Dodge Backwards").autoIcon(); + private final ModifiableCallout grimEmbraceFrontSoon = new ModifiableCallout("Grim Embrace: Front Dodge Soon", "Dodge Forwards").autoIcon(); + private final ModifiableCallout grimEmbraceRearSoon = new ModifiableCallout("Grim Embrace: Rear Dodge Soon", "Dodge Backwards").autoIcon(); + private final ModifiableCallout grimEmbraceFrontMove = new ModifiableCallout<>("Grim Embrace: Rear Dodge Now", "Move"); + private final ModifiableCallout grimEmbraceRearMove = new ModifiableCallout<>("Grim Embrace: Rear Dodge Now", "Move"); + + @AutoFeed + private final SequentialTrigger grimEmbrace = SqtTemplates.sq(180_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E39, 0x9E3A), + (e1, s) -> { + boolean isFront = e1.abilityIdMatches(0x9E3A); + List buffs = s.waitEventsQuickSuccession(12, BuffApplied.class, ba -> ba.buffIdMatches(0x1055)); + buffs.stream().filter(ba -> ba.getTarget().isThePlayer()) + .findAny() + .ifPresent(b -> { + s.updateCall(isFront ? grimEmbraceFrontInitial : grimEmbraceRearInitial, b); + s.waitDuration(b.remainingDurationPlus(Duration.ofSeconds(-5))); + s.updateCall(isFront ? grimEmbraceFrontSoon : grimEmbraceRearSoon, b); + s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E3C)); + s.updateCall(isFront ? grimEmbraceFrontMove : grimEmbraceRearMove); + }); + }); + + // Real death IV: + // Boss casts 0x9E43 (5.3s, no aoe) BEFORE the others + // Ball of Naught casts 0x9E46 (5.7s, donut) + // Fake boss casts 0x9E45 (3.7s, circle) + // Fake boss casts 0x9E44 (vortex, 1.7s, pull-in) + // KB seems to be scripted? + // Endeath IV: + // Boss casts 9E53 beforehand, giving the 0x1056 status + // Ball casts 9E49 (4.7s, donut) + // Fake boss casts 9E48 (2.7s, circle) + // Fake boss casts 9E47 (vortex, 0.7s, pull-in) + + @NpcCastCallout(0x9E4C) + private final ModifiableCallout aeroIV = new ModifiableCallout<>("Aero IV", "Knockback"); + + private final ModifiableCallout deathIV = new ModifiableCallout<>("Death IV", "Out of Middle"); + private final ModifiableCallout deathIVin = ModifiableCallout.durationBasedCall("Death IV: Inside", "In"); + + @AutoFeed + private final SequentialTrigger deathIVsq = SqtTemplates.sq(15_000, + // Start on the real cast + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E43), + (e1, s) -> { + s.updateCall(deathIV); + // Wait for inner circle to pop + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9E45)); + // Use the donut cast as the time basis + var e2 = s.findOrWaitForCast(casts, acs -> acs.abilityIdMatches(0x9E46), false); + s.updateCall(deathIVin, e2); + }); + + private static final long[] ALL_BLADE_IDS = {0x9DFC, 0x9DFE, 0x9E00}; + + private final ModifiableCallout enaeroInitial = ModifiableCallout.durationBasedCall("Enaero: Initial", "Stocking Aero"); + private final ModifiableCallout endeathInitial = ModifiableCallout.durationBasedCall("Endeath: Initial", "Stocking Death"); + + @AutoFeed + private final SequentialTrigger enaeroSq = SqtTemplates.sq(120_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E54), + (e1, s) -> { + s.updateCall(enaeroInitial, e1); + s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(ALL_BLADE_IDS)); + // Don't talk over the other call + s.waitMs(3_000); + s.updateCall(aeroIV); + }); + + @AutoFeed + private final SequentialTrigger endeathSq = SqtTemplates.sq(120_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E53), + (e1, s) -> { + s.updateCall(endeathInitial, e1); + s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(ALL_BLADE_IDS)); + // Don't talk over the other call + s.waitMs(3_000); + s.updateCall(deathIV); + // Wait for inner circle to pop + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9E48)); + // Use the donut cast as the time basis + var e2 = s.findOrWaitForCast(casts, acs -> acs.abilityIdMatches(0x9E49), false); + s.updateCall(deathIVin, e2); + }); + + @NpcCastCallout(0x9E3E) + private final ModifiableCallout floodOfDarkness = ModifiableCallout.durationBasedCall("Flood of Darkness", "Raidwide"); + + @NpcCastCallout(0x9E0D) + private final ModifiableCallout wildCharge = ModifiableCallout.durationBasedCall("Wild Charge", "Stacks, Tanks in Front"); + + private final ModifiableCallout flare = ModifiableCallout.durationBasedCallWithOffset("Flare", "Flare", Duration.ofMillis(9_300)); + private final ModifiableCallout flareOnYou = ModifiableCallout.durationBasedCallWithOffset("Flare on You", "Out", Duration.ofMillis(9_300)); + private final ModifiableCallout flareNotOnYou = ModifiableCallout.durationBasedCallWithOffset("Flare on You", "Middle", Duration.ofMillis(9_300)); + + @AutoFeed + private final SequentialTrigger flareSq = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E58), + (e1, s) -> { + s.updateCall(flare, e1); + // This fight does not have consistent marker offsets + s.waitCastFinished(casts, e1); + List headmarkers = s.waitEventsQuickSuccession(3, HeadMarkerEvent.class, hme -> true); + headmarkers.stream().filter(hm -> hm.getTarget().isThePlayer()).findAny() + .ifPresentOrElse(h -> s.updateCall(flareOnYou, e1), + () -> s.updateCall(flareNotOnYou, e1)); + }); + + @NpcCastCallout(0xA12D) + private final ModifiableCallout unholyDarkness = ModifiableCallout.durationBasedCallWithOffset("Unholy Darkness", "Light Party Stacks", Duration.ofMillis(9_100)); + @NpcCastCallout(0x9E40) + private final ModifiableCallout rapidSequence = ModifiableCallout.durationBasedCall("Rapid-sequence Particule Beam", "Wild Charges"); + + @NpcCastCallout(0x9E50) + private final ModifiableCallout breakIV = ModifiableCallout.durationBasedCall("Break IV", "Look Away"); + + @NpcCastCallout(value = 0x9E3F, suppressMs = 30_000) + private final ModifiableCallout razingVolley = ModifiableCallout.durationBasedCall("Razing Volley", "Dodge Lasers"); + + + // P2 + + @NpcCastCallout(0x9E10) + private final ModifiableCallout diffusive = ModifiableCallout.durationBasedCall("Diffusive-force Particle Beam", "Spread"); + + @NpcCastCallout(0x9E0B) + private final ModifiableCallout ghastlyGloomDonut = ModifiableCallout.durationBasedCall("Ghastly Gloom (Huge Donut)", "In"); + + @NpcCastCallout(0x9E09) + private final ModifiableCallout ghastlyGloomCross = ModifiableCallout.durationBasedCall("Ghastly Gloom (Cross)", "Corners"); + + private final ModifiableCallout curseOfDarkness = ModifiableCallout.durationBasedCall("Curse of Darkness", "Look Out"); + + @NpcCastCallout({0x9E01, 0x9E3D}) + private final ModifiableCallout deluge = ModifiableCallout.durationBasedCall("Deluge of Darkness", "Raidwide with Bleed"); + + + @AutoFeed + private final SequentialTrigger curseOfDarknessSq = SqtTemplates.callWhenDurationIs( + BuffApplied.class, + ba -> ba.buffIdMatches(0x953) && ba.getTarget().isThePlayer(), + curseOfDarkness, + Duration.ofSeconds(3)); + + @NpcCastCallout(value = 0xA2C9, suppressMs = 1000) + private final ModifiableCallout loomingChaos = ModifiableCallout.durationBasedCall("Looming Chaos", "Prepare for Swaps"); + + @NpcCastCallout(0x9E08) + private final ModifiableCallout darkDominion = ModifiableCallout.durationBasedCall("Dark Dominion", "Raidwide"); + + + // TODO: wild charge + + /* + * Third Art of Darkness (cleaves, buddies/spread) + * Two initial casts: 9E20 and 9E23 + * Headmarkers happen during the cast + * + * Example 1: + * 9E20 -> F0 F1 F0, east boss, facing east + * North Buddies North + * + * Example 2: + * 9E23 -> EF F1 EF, west boss, facing west + * North Buddies North + * + * Example 3: + * 9E20 -> F0 F1 EF, west boss, facing west + * South Buddies North + * + * Example 4: + * 9E23 -> EF F0 F1, east boss, facing east + * South North Buddies + * + * Exampel 5: + * 9E23 -> EF F0 F2, east boss, facing east + * South North Spread + * + * Seems to be: + * EF: Cleaving left + * F0: Cleaving right + * + */ + + private enum ArtOfDarknessMech { + CLEAVE_LEFT, + CLEAVE_RIGHT, + BUDDIES, + PROTEANS; + + private static ArtOfDarknessMech forHm(HeadMarkerEvent hme) { + return switch ((int) hme.getMarkerId()) { + case 0xEF -> CLEAVE_LEFT; + case 0xF0 -> CLEAVE_RIGHT; + case 0xF1 -> BUDDIES; + case 0xF2 -> PROTEANS; + default -> null; + }; + } + } + + private final ModifiableCallout artOfDarknessWestCleaveLeft = new ModifiableCallout<>("Third Art of Darkness: West Add Cleaving Left", "North"); + private final ModifiableCallout artOfDarknessWestCleaveRight = new ModifiableCallout<>("Third Art of Darkness: West Add Cleaving Right", "South"); + private final ModifiableCallout artOfDarknessEastCleaveLeft = new ModifiableCallout<>("Third Art of Darkness: East Add Cleaving Left", "South"); + private final ModifiableCallout artOfDarknessEastCleaveRight = new ModifiableCallout<>("Third Art of Darkness: East Add Cleaving Right", "North"); + private final ModifiableCallout artOfDarknessBuddies = new ModifiableCallout<>("Third Art of Darkness: Partner Stacks", "Buddies"); + private final ModifiableCallout artOfDarknessProteans = new ModifiableCallout<>("Third Art of Darkness: Proteans", "Proteans"); + + private ModifiableCallout calloutForMech(ArtOfDarknessMech artOfDarknessMech, boolean east) { + return switch (artOfDarknessMech) { + case CLEAVE_LEFT -> east ? artOfDarknessEastCleaveLeft : artOfDarknessWestCleaveLeft; + case CLEAVE_RIGHT -> east ? artOfDarknessEastCleaveRight : artOfDarknessWestCleaveRight; + case BUDDIES -> artOfDarknessBuddies; + case PROTEANS -> artOfDarknessProteans; + }; + } + + private static final long[] ALL_ART_IDS = {0x9E22, 0x9E25, 0x9E26, 0x9E28}; + + private boolean doWeCareAboutStygian(XivCombatant combatant) { + // Check that this one is the one that the player is near + // TODO: what if someone is walking on outer ring? + boolean isEast = combatant.getPos().x() > 0; + var mySection = getPlayerSection(); + if (isEast) { + return mySection == CodCarSection.EAST_OUTSIDE; + } + else { + return mySection == CodCarSection.WEST_OUTSIDE; + } + + } + + @AutoFeed + private final SequentialTrigger sq = SqtTemplates.sq(30_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E20, 0x9E23), + (e1, s) -> { + XivCombatant npc = e1.getSource(); + boolean isEast = npc.getPos().x() > 0; + if (!doWeCareAboutStygian(npc)) { + return; + } + + var mech1 = ArtOfDarknessMech.forHm(s.waitEvent(HeadMarkerEvent.class, hme -> hme.getTarget().equals(npc))); + ModifiableCallout mc1 = calloutForMech(mech1, isEast); + var call1 = s.call(mc1); + + var mech2 = ArtOfDarknessMech.forHm(s.waitEvent(HeadMarkerEvent.class, hme -> hme.getTarget().equals(npc))); + ModifiableCallout mc2 = calloutForMech(mech2, isEast); + var call2 = s.call(mc2); + + var mech3 = ArtOfDarknessMech.forHm(s.waitEvent(HeadMarkerEvent.class, hme -> hme.getTarget().equals(npc))); + ModifiableCallout mc3 = calloutForMech(mech3, isEast); + var call3 = s.call(mc3); + + // Wait so we don't talk over the call + s.waitMs(1_500); + + RawModifiedCallout mc1a = s.call(mc1); + mc1a.setReplaces(call1); + + s.waitEventsQuickSuccession(2, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(ALL_ART_IDS)); + s.waitMs(300); + + mc1a.forceExpire(); + RawModifiedCallout mc2a = s.call(mc2); + mc2a.setReplaces(call2); + + s.waitEventsQuickSuccession(2, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(ALL_ART_IDS)); + s.waitMs(300); + + mc2a.forceExpire(); + RawModifiedCallout mc3a = s.call(mc3); + mc3a.setReplaces(call3); + + // TODO: tower call? + + }); + + private final ModifiableCallout evilSeedOnYou = new ModifiableCallout<>("Evil Seed: On You", "Drop Bramble"); + private final ModifiableCallout evilSeedNotOnYou = new ModifiableCallout<>("Evil Seed: Not On You", "Avoid Brambles"); + + @AutoFeed + private final SequentialTrigger evilSeed = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E2A), + (e1, s) -> { + s.waitEventsQuickSuccession(8, HeadMarkerEvent.class, hme -> hme.getMarkerId() == 0x227) + .stream() + .filter(hme -> hme.getTarget().isThePlayer()) + .findAny() + .ifPresentOrElse(h -> { + s.updateCall(evilSeedOnYou, h); + }, () -> s.updateCall(evilSeedNotOnYou)); + }); + + @PlayerStatusCallout(value = 0x1BD, cancellable = true) + private final ModifiableCallout thornyVine = ModifiableCallout.durationBasedCall("Thorny Vine", "Break Tether"); + + private final ModifiableCallout lateralCore = ModifiableCallout.durationBasedCallWithOffset("Lateral-Core Phaser", "Sides then In", Duration.ofMillis(3300)); + private final ModifiableCallout coreLateral = ModifiableCallout.durationBasedCallWithOffset("Core-Lateral Phaser", "In then Sides", Duration.ofMillis(3300)); + private final ModifiableCallout coreWithTower = new ModifiableCallout<>("Lateral-Core: Follow-Up with Tower", "In then Tower"); + private final ModifiableCallout lateralWithTower = new ModifiableCallout<>("Core-Lateral: Follow-Up with Tower", "Out and Tower"); + private final ModifiableCallout coreWithPivot = new ModifiableCallout<>("Lateral-Core: Follow-Up with Pivot", "In then {rotationSafe}"); + private final ModifiableCallout lateralWithPivot = new ModifiableCallout<>("Core-Lateral: Follow-Up with Pivot", "Out and {rotationSafe}"); + + @AutoFeed + private final SequentialTrigger phaser = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E2F, 0x9E30), + (e1, s) -> { + if (!doWeCareAboutStygian(e1.getSource())) { + return; + } + boolean sidesFirst = e1.abilityIdMatches(0x9E2F); + AbilityCastStart pivotCast = casts.getActiveCastById(0x9E13, 0x9E15).stream().map(CastTracker::getCast).findFirst() + .orElse(null); + if (pivotCast == null) { + s.updateCall(sidesFirst ? lateralCore : coreLateral, e1); + s.waitCastFinished(casts, e1); + s.updateCall(sidesFirst ? coreWithTower : lateralWithTower); + } + else { + boolean cw = pivotCast.abilityIdMatches(0x9E13); + var mySection = getPlayerSection(); + if (mySection == CodCarSection.EAST_OUTSIDE) { + s.setParam("rotationSafe", cw ? ArenaSector.NORTHEAST : ArenaSector.SOUTHEAST); + } + else if (mySection == CodCarSection.WEST_OUTSIDE) { + s.setParam("rotationSafe", cw ? ArenaSector.SOUTHWEST : ArenaSector.NORTHWEST); + } + else { + throw new IllegalStateException("How?"); + } + s.updateCall(sidesFirst ? lateralCore : coreLateral, e1); + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9E31)); + s.updateCall(sidesFirst ? coreWithPivot : lateralWithPivot); + } + }); + + private final ModifiableCallout pivotCW = ModifiableCallout.durationBasedCall("Pivot: Clockwise", "Clockwise"); + private final ModifiableCallout pivotCCW = ModifiableCallout.durationBasedCall("Pivot: Counter-Clockwise", "Counter-Clockwise"); + + @AutoFeed + private final SequentialTrigger pivot = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E13, 0x9E15), + (e1, s) -> { + var mySection = getPlayerSection(); + if (mySection == CodCarSection.INSIDE) { + s.updateCall(e1.abilityIdMatches(0x9E13) ? pivotCW : pivotCCW); + } + }); + + private final ModifiableCallout excruciate = ModifiableCallout.durationBasedCall("Excruciate on You", "Tank Buster"); + + @AutoFeed + private final SequentialTrigger excuciateSq = SqtTemplates.sq(10_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E36) && acs.getTarget().isThePlayer(), + (e1, s) -> { + s.updateCall(excruciate, e1); + }); + + @PlayerHeadmarker(0xC5) + private final ModifiableCallout chaser = new ModifiableCallout<>("Feint Particle Beam", "Chasing AoE"); +} diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCarSection.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCarSection.java new file mode 100644 index 000000000000..b6f06ff6e876 --- /dev/null +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCarSection.java @@ -0,0 +1,27 @@ +package gg.xp.xivsupport.triggers.car; + +import gg.xp.xivsupport.models.ArenaPos; +import gg.xp.xivsupport.models.ArenaSector; +import gg.xp.xivsupport.models.Position; + +public enum CodCarSection { + INSIDE, + WEST_OUTSIDE, + EAST_OUTSIDE; + + private static final ArenaPos ap = new ArenaPos(100, 100, 18.5, 10); + + public static CodCarSection forPos(Position pos) { + double dist = pos.distanceFrom2D(Position.of2d(100, 100)); + // Outer ring + if (dist > 33) { + return pos.x() < 100 ? WEST_OUTSIDE : EAST_OUTSIDE; + } + ArenaSector sect = ap.forPosition(pos); + return switch (sect) { + case WEST -> WEST_OUTSIDE; + case EAST -> EAST_OUTSIDE; + default -> INSIDE; + }; + } +} diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/CastType.java b/xivdata/src/main/java/gg/xp/xivdata/data/CastType.java deleted file mode 100644 index 291a309bc3d3..000000000000 --- a/xivdata/src/main/java/gg/xp/xivdata/data/CastType.java +++ /dev/null @@ -1,9 +0,0 @@ -package gg.xp.xivdata.data; - -// TODO: figure out actual values, since changing enum names is harder to do later -public enum CastType { - UNKNOWN_0, - UNKNOWN_1, - CIRCLE, - CONE, -} diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java b/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java index 8f63612424dd..fee5d0ee17ee 100644 --- a/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java +++ b/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java @@ -51,7 +51,7 @@ public XivMap(int offsetX, int offsetY, int scaleFactor, @Nullable String filena if (parts.length == 2) { String stub = parts[0]; String index = parts[1]; - String urlStr = String.format("https://xivapi.com/m/%s/%s.%s.jpg", stub, stub, index); + String urlStr = String.format("https://beta.xivapi.com/api/1/asset/map/%s/%s", stub, index); URL url; try { url = new URL(urlStr); diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java b/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java index 7fad9ac83b64..1396f4a5e231 100644 --- a/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java +++ b/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java @@ -11,7 +11,8 @@ public enum DutyType { HUNT("Hunt"), SOLO_INSTANCE("Solo Instance"), OPEN_WORLD("Eureka-like"), - ALLIANCE_RAID("Alliance Raid"); + ALLIANCE_RAID("Alliance Raid"), + CAR("Chaotic AR"); private final String name; diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java b/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java index f5621e22b067..43bafec8ee36 100644 --- a/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java +++ b/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java @@ -67,6 +67,7 @@ public enum KnownDuty implements Duty { M4S("M4S", 1232,Expansion.DT, DutyType.SAVAGE_RAID), FRU("FRU", 1238, Expansion.DT, DutyType.ULTIMATE), + CodCar("CoD CAR", 1241, Expansion.DT, DutyType.CAR), ; private final String name; diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/events/HasTargetIndex.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/events/HasTargetIndex.java index 1c9caeb15abf..95198cdb0c75 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/events/HasTargetIndex.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/events/HasTargetIndex.java @@ -15,6 +15,10 @@ public interface HasTargetIndex { long getTargetIndex(); + /** + * @return The number of targets hit by this action. Each event represents one target getting hit. This may be + * zero if no targets were hit, as there still must be an event generated to indicate such. + */ long getNumberOfTargets(); default boolean isFirstTarget() { diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java index 603176c38260..1d8636596ff1 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java @@ -310,4 +310,5 @@ public List getCombatantsToUpdate() { public void flushStateOverrides() { state.flushProvidedValues(); } + } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java index fb5fab163454..672f4c62d2ca 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java @@ -81,7 +81,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim // of them changes does not work on an 'add' line, and the 'add' line will not write 'default' // values. So if there is a Z or heading of 0 (very likely), those will not be written. // To work around this, we just assume those are 0. - state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.getOrDefault(PosKeys.PosZ, 0.0), pos.getOrDefault(PosKeys.Heading, 0.0))); + state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.getOrDefault(PosKeys.PosZ, 0.0), pos.getOrDefault(PosKeys.Heading, 0.0)), true); } else { log.trace("Incomplete position info for 0x{}", Long.toString(existing.getId(), 16)); @@ -89,7 +89,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim } } else { - state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.get(PosKeys.PosZ), pos.get(PosKeys.Heading))); + state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.get(PosKeys.PosZ), pos.get(PosKeys.Heading)), true); } } else { @@ -98,7 +98,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim pos.getOrDefault(PosKeys.PosY, existingPos.y()), pos.getOrDefault(PosKeys.PosZ, existingPos.z()), pos.getOrDefault(PosKeys.Heading, existingPos.heading()) - )); + ), true); } } } 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 e0b8f7d87d82..3ec1641a7abe 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 @@ -71,7 +71,6 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim // Animation Target Id // TODO: backwards compat? make sure this doesn't catch the hash XivCombatant animationTarget = fields.getOptionalEntity(Fields.animationTargetId); - LoggerFactory.getLogger(Line264Parser.class).info("animation target: {}", animationTarget); // Cast location/angle // First, check if the line indicates that such data is actually present. diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java index b78d32036891..ae277dbef0f4 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java @@ -32,7 +32,7 @@ enum Fields { fields.getDouble(Fields.x), fields.getDouble(Fields.y), fields.getDouble(Fields.z), - fields.getDouble(Fields.heading))); + fields.getDouble(Fields.heading)), true); return null; } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line38Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line38Parser.java index 8e19c0adf900..4a40817747fa 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line38Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line38Parser.java @@ -16,7 +16,6 @@ @SuppressWarnings("unused") public class Line38Parser extends AbstractACTLineParser { - private static final boolean enableStatusEffectParsing = true; private final StatusEffectRepository buffs; public Line38Parser(PicoContainer container, StatusEffectRepository buffs) { @@ -37,7 +36,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim XivCombatant target = fields.getEntity(Fields.id, Fields.name, Fields.targetCurHp, Fields.targetMaxHp, Fields.targetCurMp, Fields.targetMaxMp, Fields.targetX, Fields.targetY, Fields.targetZ, Fields.targetHeading, Fields.targetShieldPct); // To save processing time, only bother with this if the target has no buffs whatsoever currently on them // TODO: is this the best way of doing this? - if (enableStatusEffectParsing && !buffs.targetHasAnyStatus(target)) { + if (!buffs.targetHasAnyStatus(target)) { List split = fields.getRawLineSplit(); // Last field is hash List remaining = split.subList(Fields.firstFlag.ordinal() + 2, split.size() - 1); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java index bf3b46dbdfaa..70020d458943 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java @@ -72,7 +72,11 @@ default boolean dutyIs(Duty duty) { void provideCombatantMP(XivCombatant target, @NotNull ManaPoints manaPoints); - void provideCombatantPos(XivCombatant target, Position newPos); + default void provideCombatantPos(XivCombatant target, Position newPos) { + provideCombatantPos(target, newPos, false); + } + + void provideCombatantPos(XivCombatant target, Position newPos, boolean trusted); void provideActFallbackCombatant(XivCombatant cbt); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java index b99edea411e7..2a0a5da706a2 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java @@ -116,7 +116,7 @@ public void provideCombatantMP(XivCombatant target, @NotNull ManaPoints manaPoin } @Override - public void provideCombatantPos(XivCombatant target, Position newPos) { + public void provideCombatantPos(XivCombatant target, Position newPos, boolean trusted) { throw new UnsupportedOperationException("not supported"); } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java index 8c3c1fdb2dba..64330f2d1b6e 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory; import java.lang.ref.WeakReference; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -48,6 +49,7 @@ public class XivStateImpl implements XivState { private static final Logger log = LoggerFactory.getLogger(XivStateImpl.class); + private static final int POSITION_TRUST_BUFFER_MS = 200; private final EventMaster master; private final PartySortOrder pso; private final @Nullable CurrentTimeSource fakeTimeSource; @@ -444,8 +446,8 @@ public void provideCombatantMP(XivCombatant target, @NotNull ManaPoints manaPoin } @Override - public void provideCombatantPos(XivCombatant target, Position newPos) { - getOrCreateData(target.getId()).setPosOverride(newPos); + public void provideCombatantPos(XivCombatant target, Position newPos, boolean trusted) { + getOrCreateData(target.getId()).setPosOverride(newPos, trusted); dirtyOverrides = true; } @@ -620,6 +622,21 @@ private CombatantData getOrCreateData(long cbtId) { } } + private record TimedPosition(Position position, long whenEpochMs, CurrentTimeSource cts) { + long getMsSince() { + return cts.now().toEpochMilli() - whenEpochMs; + } + + boolean isExpired() { + return getMsSince() > POSITION_TRUST_BUFFER_MS; + } + } + + private TimedPosition makeTimedPosition(Position position) { + CurrentTimeSource cts = fakeTimeSource == null ? Instant::now : fakeTimeSource; + return new TimedPosition(position, cts.now().toEpochMilli(), cts); + } + // private final class CombatantData { // Cached computed result @@ -628,6 +645,7 @@ private final class CombatantData { private final long id; private @Nullable RawXivCombatantInfo raw; private @Nullable Position posOverride; + private @Nullable TimedPosition posOverrideTrusted; private @Nullable HitPoints hpOverride; private @Nullable ManaPoints mpOverride; private @Nullable XivCombatant fromOtherActLine; @@ -684,10 +702,24 @@ public void setFromPartyInfo(RawXivPartyInfo fromPartyInfo) { dirty = true; } - public void setPosOverride(@Nullable Position posOverride) { - if (!Objects.equals(this.posOverride, posOverride)) { - this.posOverride = posOverride; - dirty = true; + public void setPosOverride(@Nullable Position posOverride, boolean trusted) { + // Ignore the concept of trusted positions for PCs, it really only makes sense for NPCs, especially fakes + if (trusted && computeRawType() != 1) { + TimedPosition pot = this.posOverrideTrusted; + if (pot == null || !Objects.equals(pot.position, posOverride)) { + posOverrideTrusted = makeTimedPosition(posOverride); + dirty = true; + } + } + else { + if (!Objects.equals(this.posOverride, posOverride)) { + this.posOverride = posOverride; + TimedPosition pot = this.posOverrideTrusted; + if (pot != null && pot.isExpired()) { + posOverrideTrusted = null; + } + dirty = true; + } } } @@ -768,6 +800,13 @@ public boolean recomputeIfDirty() { return false; } + private long computeRawType() { + // Trust an explicit type override the most + // Then, check raw data (03-line or getCombatants) + // Finally, assume type 2 (NPC) for >=4xxx IDs or type 1 (PC) otherwise + return typeOverride != null ? typeOverride : raw != null ? raw.getRawType() : (id >= 0x4000_0000 ? 2 : 1); + } + private synchronized void recompute() { RawXivCombatantInfo raw = this.raw; // Each data element has a different "priority" for each field @@ -776,15 +815,15 @@ private synchronized void recompute() { String name = raw != null ? raw.getName() : (fromOther != null ? fromOther.getName() : (fromPartyInfo != null ? fromPartyInfo.getName() : "???")); long jobId = raw != null ? raw.getJobId() : (fromPartyInfo != null ? fromPartyInfo.getJobId() : 0); XivWorld world = XivWorld.of(); - // Trust an explicit type override the most - // Then, check raw data (03-line or getCombatants) - // Finally, assume type 2 (NPC) for >=4xxx IDs or type 1 (PC) otherwise - long rawType = typeOverride != null ? typeOverride : raw != null ? raw.getRawType() : (id >= 0x4000_0000 ? 2 : 1); + long rawType = computeRawType(); // HP prefers trusted ACT hp lines HitPoints hp = hpOverride != null ? hpOverride : raw != null ? raw.getHP() : null; ManaPoints mp = mpOverride != null ? mpOverride : raw != null ? raw.getMP() : null; - Position pos = posOverride != null ? posOverride : raw != null ? raw.getPos() : fromOther != null ? fromOther.getPos() : null; + // We want to look at the position of the + TimedPosition pot = posOverrideTrusted; + Position po = posOverride; + Position pos = pot != null ? pot.position : po != null ? po : raw != null ? raw.getPos() : fromOther != null ? fromOther.getPos() : null; XivCombatant computed; long bnpcId = raw != null ? raw.getBnpcId() : 0; diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java index 98013d81cdde..fedc09a615c4 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java @@ -215,6 +215,7 @@ public void removeCombatant(EventContext context, RawRemoveCombatantEvent event) } } + // TODO: this has a performance impact @HandleEvents public void workaroundForActNotRemovingCombatants(EventContext context, XivStateRecalculatedEvent event) { Set combatantsThatExist = state.getCombatants().keySet(); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java index 967be7fd5e39..625521c23a5a 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java @@ -740,14 +740,23 @@ private JPanel getActLogPanel() { TableWithFilterAndDetails table = TableWithFilterAndDetails.builder("ACT Log", () -> rawStorage.getEventsOfType(ACTLogLineEvent.class), GroovyColumns::getValues) - .addMainColumn(new CustomColumn<>("Line", ACTLogLineEvent::getLogLine)) + .addMainColumn(new CustomColumn<>("Line", actLogLineEvent -> { + String line = actLogLineEvent.getLogLine(); + if (actLogLineEvent.getLineNumber() < 100) { + return ' ' + line; + } + return line; + })) .apply(GroovyColumns::addDetailColumns) .withRightClickRepo(rightClicks) .addFilter(ActLineFilter::new) .addWidget(replayNextPseudoFilter(ACTLogLineEvent.class)) .setAppendOrPruneOnly(true) .build(); - table.getMainTable().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + JTable mt = table.getMainTable(); + Font oldFont = mt.getFont(); + mt.setFont(new Font(Font.MONOSPACED, oldFont.getStyle(), oldFont.getSize())); + mt.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); master.getDistributor().registerHandler(ACTLogLineEvent.class, (ctx, e) -> { table.signalNewData(); }); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapPanel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapPanel.java index ba5d1358eaf7..8d10e73df1b1 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapPanel.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapPanel.java @@ -359,15 +359,19 @@ private double translateDist(double originalDist) { return translateDistScrn(translateDistMap(originalDist)); } + private boolean dragActive; + @Override public void mouseDragged(MouseEvent e) { - Point curPoint = e.getLocationOnScreen(); - double xDiff = curPoint.x - dragPoint.x; - double yDiff = curPoint.y - dragPoint.y; - curXpan += xDiff; - curYpan += yDiff; - dragPoint = curPoint; - triggerRefresh(); + if (dragActive) { + Point curPoint = e.getLocationOnScreen(); + double xDiff = curPoint.x - dragPoint.x; + double yDiff = curPoint.y - dragPoint.y; + curXpan += xDiff; + curYpan += yDiff; + dragPoint = curPoint; + triggerRefresh(); + } } @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NumericCastThatLosesPrecision"}) @@ -418,13 +422,24 @@ public void mouseClicked(MouseEvent e) { } } + private static boolean isValidDragBtn(MouseEvent e) { + int btn = e.getButton(); + return (btn == MouseEvent.BUTTON1 || btn == MouseEvent.BUTTON3 || btn == MouseEvent.BUTTON2); + } + @Override public void mousePressed(MouseEvent e) { - dragPoint = MouseInfo.getPointerInfo().getLocation(); + if (isValidDragBtn(e)) { + dragActive = true; + dragPoint = MouseInfo.getPointerInfo().getLocation(); + } } @Override public void mouseReleased(MouseEvent e) { + if (isValidDragBtn(e)) { + dragActive = false; + } triggerRefresh(); } @@ -555,6 +570,8 @@ public Rectangle getBounds() { */ private class EntityDoohickey extends JPanel { + private static final BasicStroke omenOutline = new BasicStroke(2); + private static final BasicStroke omenOutlinePre = new BasicStroke(2, omenOutline.getEndCap(), omenOutline.getLineJoin(), omenOutline.getMiterLimit(), new float[]{3.0f, 6.0f}, 0); private final JLabel defaultLabel; private final XivCombatant cbt; // This red should never actually show up @@ -910,55 +927,62 @@ else if (cbt.getType() == CombatantType.NPC) { */ // TODO: some of these are wrong due to lack of hitbox size info AffineTransform transform = g2d.getTransform(); + boolean isCast = omen.type().isInProgress(); + Stroke outlineStroke = isCast ? omenOutlinePre : omenOutline; switch (oi.type().shape()) { case CIRCLE -> { - g2d.setStroke(new BasicStroke(3)); +// g2d.setStroke(omenOutline); g2d.setColor(fillColor); g2d.fillOval((int) (xCenter - radius), (int) (yCenter - radius), (int) (radius * 2.0), (int) (radius * 2.0)); + g2d.setStroke(outlineStroke); g2d.setColor(outlineColor); g2d.drawOval((int) (xCenter - radius), (int) (yCenter - radius), (int) (radius * 2.0), (int) (radius * 2.0)); } case DONUT -> { - g2d.setStroke(new BasicStroke(3)); +// g2d.setStroke(omenOutline); // g2d.setColor(fillColor); // g2d.fillOval((int) (xCenter - radius), (int) (yCenter - radius), (int) (radius * 2.0), (int) (radius * 2.0)); + g2d.setStroke(outlineStroke); g2d.setColor(outlineColor); g2d.drawOval((int) (xCenter - radius), (int) (yCenter - radius), (int) (radius * 2.0), (int) (radius * 2.0)); } case RECTANGLE -> { - g2d.setStroke(new BasicStroke(3)); +// g2d.setStroke(omenOutline); transform.translate(xCenter, yCenter); transform.rotate(-omenPos.getHeading()); g2d.setTransform(transform); g2d.setColor(fillColor); g2d.fillRect((int) -(xModif / 2.0), 0, (int) xModif, (int) radius); + g2d.setStroke(outlineStroke); g2d.setColor(outlineColor); g2d.drawRect((int) -(xModif / 2.0), 0, (int) xModif, (int) radius); } case RECTANGLE_CENTERED -> { - g2d.setStroke(new BasicStroke(3)); +// g2d.setStroke(omenOutline); transform.translate(xCenter, yCenter); transform.rotate(-omenPos.getHeading()); g2d.setTransform(transform); g2d.setColor(fillColor); g2d.fillRect((int) -(xModif / 2.0), (int) -radius, (int) xModif, (int) (2 * radius)); + g2d.setStroke(outlineStroke); g2d.setColor(outlineColor); g2d.drawRect((int) -(xModif / 2.0), (int) -radius, (int) xModif, (int) (2 * radius)); } case CROSS -> { - g2d.setStroke(new BasicStroke(3)); +// g2d.setStroke(omenOutline); transform.translate(xCenter, yCenter); transform.rotate(-omenPos.getHeading()); g2d.setTransform(transform); g2d.setColor(fillColor); g2d.fillRect((int) -(xModif / 2.0), (int) -radius, (int) xModif, (int) (2 * radius)); g2d.fillRect((int) -radius, (int) -(xModif / 2.0), (int) (2 * radius), (int) xModif); + g2d.setStroke(outlineStroke); g2d.setColor(outlineColor); g2d.drawRect((int) -(xModif / 2.0), (int) -radius, (int) xModif, (int) (2 * radius)); g2d.drawRect((int) -radius, (int) -(xModif / 2.0), (int) (2 * radius), (int) xModif); } case CONE -> { - g2d.setStroke(new BasicStroke(3)); +// g2d.setStroke(omenOutline); transform.translate(xCenter, yCenter); transform.rotate(-omenPos.getHeading() + Math.PI); g2d.setTransform(transform); @@ -968,6 +992,7 @@ else if (cbt.getType() == CombatantType.NPC) { Arc2D.Double arc = new Arc2D.Double(-radius, -radius, 2 * radius, 2 * radius, 90 - angleDegrees / 2.0f, angleDegrees, Arc2D.PIE); g2d.setColor(fillColor); g2d.fill(arc); + g2d.setStroke(outlineStroke); g2d.setColor(outlineColor); g2d.draw(arc); } @@ -978,6 +1003,7 @@ else if (cbt.getType() == CombatantType.NPC) { } } + @Override public Border getBorder() { if (isSelected()) { diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java index 8f825ef61b18..c345dfda09fa 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java @@ -9,6 +9,7 @@ import gg.xp.xivsupport.events.actlines.events.XivStateRecalculatedEvent; import gg.xp.xivsupport.groovy.GroovyManager; import gg.xp.xivsupport.gui.overlay.RefreshLoop; +import gg.xp.xivsupport.gui.tables.RightClickOptionRepo; import gg.xp.xivsupport.gui.tables.StandardColumns; import gg.xp.xivsupport.gui.tables.TableWithFilterAndDetails; import gg.xp.xivsupport.gui.tables.filters.EventEntityFilter; @@ -41,7 +42,7 @@ public class MapTab extends JPanel { private final JSplitPane split; private volatile boolean selectionRefreshPending; - public MapTab(GroovyManager mgr, MapDataController mdc, MapConfig config, MapDisplayConfig mapDisplayConfig, MapColorSettings mcs) { + public MapTab(GroovyManager mgr, MapDataController mdc, MapConfig config, MapDisplayConfig mapDisplayConfig, MapColorSettings mcs, RightClickOptionRepo rc) { // super("Map"); super(new BorderLayout()); split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); @@ -67,6 +68,7 @@ public MapTab(GroovyManager mgr, MapDataController mdc, MapConfig config, MapDis // .addMainColumn(StandardColumns.mpColumn) // .addMainColumn(StandardColumns.posColumn) .apply(GroovyColumns::addDetailColumns) + .withRightClickRepo(rc) .setSelectionEquivalence((a, b) -> a.getId() == b.getId()) .addFilter(EventEntityFilter::selfFilter) .addFilter(NonCombatEntityFilter::new) diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/CastFinishedOmen.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/CastFinishedOmen.java index a9a058319cd3..2af8635bb859 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/CastFinishedOmen.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/CastFinishedOmen.java @@ -46,24 +46,38 @@ public Instant happensAt() { @Override public @Nullable Position omenPosition(Function freshPosLookup) { - // TODO: this logic still misses some extreme edge cases, where a cast is targeted but - // still whiffs the target. - Position sourcePosSnapshot = source().getPos(); - DescribesCastLocation locInfo = aue.getLocationInfo(); - if (locInfo == null) { - locInfo = preCast.getLocationInfo(); + // Logic: use a priority: + // 1. Snapshot location + // 2. Cast location + // 3. Snapshot angle (+ animation target) + // 4. Cast angle + // 5. Caster position at time of snapshot + DescribesCastLocation snapLoc = aue.getLocationInfo(); + DescribesCastLocation castLoc = preCast.getLocationInfo(); + // 1. Snapshot location + if (snapLoc != null && snapLoc.getPos() != null) { + return snapLoc.getPos(); + } + // 2. Cast location + if (castLoc != null && castLoc.getPos() != null) { + return castLoc.getPos(); } - if (locInfo != null) { - Position pos = locInfo.getPos(); + Position sourcePosSnapshot = source().getPos(); + DescribesCastLocation bestLoc = snapLoc != null ? snapLoc : castLoc; + if (bestLoc != null) { + Position pos = bestLoc.getPos(); if (pos != null) { return pos; } - Double heading = locInfo.getHeadingOnly(); - Position basis = locInfo.getAnimationTarget().getPos(); + // 3. Snapshot angle (+ animation target) + // 4. Cast angle + Double heading = bestLoc.getHeadingOnly(); + Position basis = bestLoc.getAnimationTarget().getPos(); if (heading != null && basis != null) { return basis.facing(heading); } } + // 5. Caster position at time of snapshot if (info.type().locationType() == OmenLocationType.CASTER) { return sourcePosSnapshot; } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/OmenEventType.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/OmenEventType.java index 0110cdb0d8c7..ddc43d306cc6 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/OmenEventType.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/OmenEventType.java @@ -3,5 +3,9 @@ public enum OmenEventType { PRE_CAST, CAST_FINISHED, - INSTANT + INSTANT; + + public boolean isInProgress() { + return this == PRE_CAST; + } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/README.md b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/README.md new file mode 100644 index 000000000000..11f2ae3b0fa1 --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/omen/README.md @@ -0,0 +1,53 @@ +# About Cast Locations + +Cast locations are not particularly straightforward. + +## Instant Casts + +Let's take an instant cast skill. We get a 21/22-line, and a 264-line. + +If the skill is ground-targeted, we will get a 264-line with a specific cast location and rotation. +This is the best possible data and is the best case scenario. In this case, we need no other information +from the log lines. + +However, most skills will not have this. We use two other pieces of data: +- The cast angle +- The animation target + +The animation target is useful for determining, for example, whether an instant cast AoE is centered on +the caster, or the target (if one exists). For example, Sage's Phlegma is centered on the target, but +Dyskrasia is centered around the caster. + +If we do not have those pieces of information, we try to guess based on the CastType. + +## Non-Instant Casts + +We also want to display an omen on skills that are still casting. + +Like the instant cast skills, if we have a location and rotation, +that is the best case scenario. We need no other data. + +If the skill is not instant, we use the target of the cast and rotation +to try to determine it. We assume that if the skill is AoE, and has a target, +that it will use that target as the basis for the aoe. + +If the skill is not instant, and we can associate it back to a cast (20-line), +and the cast event has a target other than environment or the caster, +then we can assume that the targeted entity is the true aoe target. + +# Cast AoEs + +The actual cast shape and size is determined mainly by three properties of the action: +- CastType: determines shape of cast (see `OmenType.fromCastType(int)`) +- EffectRange: determines the primary size (e.g. circle radius or rectangle length) +- XAxisModifier: determines the secondary size (rectangle width, unused for circles and cones) + +Unfortunately, there are two data points that cannot be derived from this data alone: +- Inner radius of donuts +- Angle of cones + +For the inner donut radius, often times it is simply the caster's hitbox, but not always. +For the cone angle, if the skill has an omen (technical term for skill telegraph), it can +sometimes be derived from the name of the "omen" . For example, Action 21471 (Wingbeat) +has an omen named "er_gl_fan060_1bf". From the "060" part, we can assume that the angle is +60 degrees. \ No newline at end of file diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java index 7f7e63b77dbe..e552d48bcc07 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java @@ -1,6 +1,5 @@ package gg.xp.xivsupport.gui.tables.groovy; -import gg.xp.xivsupport.gui.groovy.GroovyPanel; import gg.xp.xivsupport.gui.tables.CustomColumn; import gg.xp.xivsupport.gui.tables.CustomTableModel; import gg.xp.xivsupport.gui.tables.TableWithFilterAndDetails; @@ -12,7 +11,6 @@ import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; import java.awt.*; import java.lang.reflect.Array; import java.util.ArrayList; @@ -52,7 +50,14 @@ public static List getValues(Object obj) { else { return DefaultGroovyMethods.getMetaPropertyValues(obj) .stream() - .filter(GroovyColumns::isReadable) + .filter(pv1 -> { + try { + return isReadable(pv1); + } + catch (Throwable t) { + return false; + } + }) .filter(pv -> !"serialVersionUID".equals(pv.getName())) .toList(); } diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 7697294d58c1..d6725003ea1d 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -1,5 +1,10 @@ +

December 27, 2024

+
    +
  • CoD Chaotic Alliance Raid initial triggers added.
  • +
  • Various behind-the-scenes improvements.
  • +

December 21, 2024

  • Game data updates for 7.15.
  • diff --git a/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line271Test.java b/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line271Test.java new file mode 100644 index 000000000000..2e2a857cdb33 --- /dev/null +++ b/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line271Test.java @@ -0,0 +1,36 @@ +package gg.xp.xivsupport.events.actlines; + +import gg.xp.reevent.events.EventDistributor; +import gg.xp.reevent.events.TestEventCollector; +import gg.xp.xivsupport.events.ACTLogLineEvent; +import gg.xp.xivsupport.events.state.XivState; +import gg.xp.xivsupport.models.Position; +import gg.xp.xivsupport.models.XivCombatant; +import gg.xp.xivsupport.sys.XivMain; +import org.picocontainer.MutablePicoContainer; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Line271Test { + @Test + void testPos() { + String line = "271|2024-12-26T13:15:21.8910000-08:00|4000249E|-1.5745|00|00|73.5000|100.0000|0.0000|4843ca62b169bc5a"; + String dummyLine = "21|2024-12-26T13:15:21.9360000-08:00|4000249E|Cloud of Darkness|9E22|_rsv_40482_-1_1_0_0_SE2DC5B04_EE2DC5B04|E0000000||0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|||||||||||44|44|0|10000|||100.00|100.00|0.00|2.76|0000B1C5|0|0|00||01|9E22|9E22|1.100|3FD9|47c31102dc03ad85"; + MutablePicoContainer container = XivMain.testingMasterInit(); + TestEventCollector coll = new TestEventCollector(); + EventDistributor dist = container.getComponent(EventDistributor.class); + dist.registerHandler(coll); + dist.acceptEvent(new ACTLogLineEvent(line)); + XivState state = container.getComponent(XivState.class); + { + XivCombatant cbt = state.getCombatant(0x4000249E); + Assert.assertEquals(cbt.getPos(), new Position(73.5000, 100.0000, 0.0000, -1.5745)); + } + // Now submit a 21-line with a stale position - it should not do anything + dist.acceptEvent(new ACTLogLineEvent(dummyLine)); + { + XivCombatant cbt = state.getCombatant(0x4000249E); + Assert.assertEquals(cbt.getPos(), new Position(73.5000, 100.0000, 0.0000, -1.5745)); + } + } +}