diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java index 946c4d2cde..c33384f7fb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.EquipmentInventorySlot; @@ -40,6 +41,7 @@ import net.runelite.api.NPC; import net.runelite.api.VarPlayer; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.InteractingChanged; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.VarbitChanged; import net.runelite.client.callback.ClientThread; @@ -57,11 +59,13 @@ import net.runelite.client.ws.WSClient; tags = {"combat", "npcs", "overlay"}, enabledByDefault = false ) +@Slf4j public class SpecialCounterPlugin extends Plugin { - private int currentWorld = -1; - private int specialPercentage = -1; - private NPC specedNPC; + private int currentWorld; + private int specialPercentage; + private Actor lastSpecTarget; + private int lastSpecTick; private SpecialWeapon specialWeapon; private final Set interactedNpcIds = new HashSet<>(); @@ -89,6 +93,11 @@ public class SpecialCounterPlugin extends Plugin protected void startUp() { wsClient.registerMessage(SpecialCounterUpdate.class); + currentWorld = -1; + specialPercentage = -1; + lastSpecTarget = null; + lastSpecTick = -1; + interactedNpcIds.clear(); } @Override @@ -115,6 +124,20 @@ public class SpecialCounterPlugin extends Plugin } } + @Subscribe + public void onInteractingChanged(InteractingChanged interactingChanged) + { + Actor source = interactingChanged.getSource(); + Actor target = interactingChanged.getTarget(); + if (lastSpecTick != client.getTickCount() || source != client.getLocalPlayer() || target == null) + { + return; + } + + log.debug("Updating last spec target to {} (was {})", target.getName(), lastSpecTarget); + lastSpecTarget = target; + } + @Subscribe public void onVarbitChanged(VarbitChanged event) { @@ -129,11 +152,13 @@ public class SpecialCounterPlugin extends Plugin this.specialPercentage = specialPercentage; this.specialWeapon = usedSpecialWeapon(); - Actor interacting = client.getLocalPlayer().getInteracting(); - if (interacting instanceof NPC) - { - specedNPC = (NPC) interacting; - } + log.debug("Special attack used - percent: {} weapon: {}", specialPercentage, specialWeapon); + + // spec was used; since the varbit change event fires before the interact change event, + // this will be specing on the target of interact changed *if* it fires this tick, + // otherwise it is what we are currently interacting with + lastSpecTarget = client.getLocalPlayer().getInteracting(); + lastSpecTick = client.getTickCount(); } @Subscribe @@ -141,7 +166,25 @@ public class SpecialCounterPlugin extends Plugin { Actor target = hitsplatApplied.getActor(); Hitsplat hitsplat = hitsplatApplied.getHitsplat(); - if (hitsplat.getHitsplatType() != Hitsplat.HitsplatType.DAMAGE_ME || !(target instanceof NPC)) + Hitsplat.HitsplatType hitsplatType = hitsplat.getHitsplatType(); + // Ignore all hitsplats other than mine + if ((hitsplatType != Hitsplat.HitsplatType.DAMAGE_ME && hitsplatType != Hitsplat.HitsplatType.BLOCK_ME) || target == client.getLocalPlayer()) + { + return; + } + + log.debug("Hitsplat target: {} spec target: {}", target, lastSpecTarget); + + // If waiting for a spec, ignore hitsplats not on the actor we specced + if (lastSpecTarget != null && lastSpecTarget != target) + { + return; + } + + boolean wasSpec = lastSpecTarget != null; + lastSpecTarget = null; + + if (!(target instanceof NPC)) { return; } @@ -156,22 +199,17 @@ public class SpecialCounterPlugin extends Plugin addInteracting(interactingId); } - if (specedNPC == hitsplatApplied.getActor()) + if (wasSpec && specialWeapon != null && hitsplat.getAmount() > 0) { - specedNPC = null; + int hit = getHit(specialWeapon, hitsplat); - if (specialWeapon != null) + updateCounter(specialWeapon, null, hit); + + if (!party.getMembers().isEmpty()) { - int hit = getHit(specialWeapon, hitsplat); - - updateCounter(specialWeapon, null, hit); - - if (!party.getMembers().isEmpty()) - { - final SpecialCounterUpdate specialCounterUpdate = new SpecialCounterUpdate(interactingId, specialWeapon, hit); - specialCounterUpdate.setMemberId(party.getLocalMember().getMemberId()); - wsClient.send(specialCounterUpdate); - } + final SpecialCounterUpdate specialCounterUpdate = new SpecialCounterUpdate(interactingId, specialWeapon, hit); + specialCounterUpdate.setMemberId(party.getLocalMember().getMemberId()); + wsClient.send(specialCounterUpdate); } } } @@ -193,10 +231,9 @@ public class SpecialCounterPlugin extends Plugin { NPC actor = npcDespawned.getNpc(); - // if the NPC despawns before the hitsplat is shown - if (specedNPC == actor) + if (lastSpecTarget == actor) { - specedNPC = null; + lastSpecTarget = null; } if (actor.isDead() && interactedNpcIds.contains(actor.getId())) diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java new file mode 100644 index 0000000000..c090e7e865 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2020, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.specialcounter; + +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.EquipmentInventorySlot; +import net.runelite.api.Hitsplat; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemContainer; +import net.runelite.api.ItemID; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.VarPlayer; +import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.InteractingChanged; +import net.runelite.api.events.VarbitChanged; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.ws.PartyService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.Mock; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SpecialCounterPluginTest +{ + @Mock + @Bind + private Client client; + + @Mock + @Bind + private InfoBoxManager infoBoxManager; + + @Mock + @Bind + private PartyService partyService; + + @Mock + @Bind + private ItemManager itemManager; + + @Inject + private SpecialCounterPlugin specialCounterPlugin; + + @Before + public void before() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + + // Set up spec weapon + ItemContainer equipment = mock(ItemContainer.class); + Item[] items = new Item[EquipmentInventorySlot.WEAPON.getSlotIdx() + 1]; + items[EquipmentInventorySlot.WEAPON.getSlotIdx()] = new Item(ItemID.BANDOS_GODSWORD, 1); + when(equipment.getItems()).thenReturn(items); + when(client.getItemContainer(InventoryID.EQUIPMENT)).thenReturn(equipment); + + // Set up special attack energy + when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(100); + specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + + } + + private static HitsplatApplied hitsplat(Actor target, Hitsplat.HitsplatType type) + { + Hitsplat hitsplat = new Hitsplat(type, type == Hitsplat.HitsplatType.DAMAGE_ME ? 1 : 0, 42); + HitsplatApplied hitsplatApplied = new HitsplatApplied(); + hitsplatApplied.setActor(target); + hitsplatApplied.setHitsplat(hitsplat); + return hitsplatApplied; + } + + @Test + public void testSpecDamage() + { + NPC target = mock(NPC.class); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + // spec npc + when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(50); + specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + lenient().when(player.getInteracting()).thenReturn(target); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, target)); + + // hit 1 + specialCounterPlugin.onHitsplatApplied(hitsplat(target, Hitsplat.HitsplatType.DAMAGE_ME)); + + verify(infoBoxManager).addInfoBox(any(SpecialCounter.class)); + } + + @Test + public void testSpecBlock() + { + NPC target = mock(NPC.class); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + // spec npc + when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(50); + specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + lenient().when(player.getInteracting()).thenReturn(target); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, target)); + + // block 0 + specialCounterPlugin.onHitsplatApplied(hitsplat(target, Hitsplat.HitsplatType.BLOCK_ME)); + + // hit 1 + specialCounterPlugin.onHitsplatApplied(hitsplat(target, Hitsplat.HitsplatType.DAMAGE_ME)); + + verify(infoBoxManager, never()).addInfoBox(any(SpecialCounter.class)); + } + + @Test + public void testUnaggro() + { + NPC target = mock(NPC.class); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + // tick 1: attack npc + when(player.getInteracting()).thenReturn(target); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, target)); + + // tick 2: spec fires and un-interact npc + when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(50); + specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + lenient().when(player.getInteracting()).thenReturn(null); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, null)); + + // tick 3: hit 1 + specialCounterPlugin.onHitsplatApplied(hitsplat(target, Hitsplat.HitsplatType.DAMAGE_ME)); + + verify(infoBoxManager).addInfoBox(any(SpecialCounter.class)); + } + + @Test + public void testSameTick() + { + NPC targetA = mock(NPC.class); + NPC targetB = mock(NPC.class); + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + // tick 1: attack npc A + when(player.getInteracting()).thenReturn(targetA); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, targetA)); + + // tick 2: spec npc B + when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(50); + specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + lenient().when(player.getInteracting()).thenReturn(targetB); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, targetB)); + + // tick 3: hitsplat A, hitsplat B + specialCounterPlugin.onHitsplatApplied(hitsplat(targetA, Hitsplat.HitsplatType.DAMAGE_ME)); + verify(infoBoxManager, never()).addInfoBox(any(SpecialCounter.class)); + + specialCounterPlugin.onHitsplatApplied(hitsplat(targetB, Hitsplat.HitsplatType.DAMAGE_ME)); + verify(infoBoxManager).addInfoBox(any(SpecialCounter.class)); + } + + @Test + public void testReset() + { + NPC targetA = mock(NPC.class); + NPC targetB = mock(NPC.class); + when(targetB.getId()).thenReturn(1); // a different npc type + + Player player = mock(Player.class); + when(client.getLocalPlayer()).thenReturn(player); + + // spec npc + when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(50); + specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + lenient().when(player.getInteracting()).thenReturn(targetA); + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, targetA)); + + // hit 1 + specialCounterPlugin.onHitsplatApplied(hitsplat(targetA, Hitsplat.HitsplatType.DAMAGE_ME)); + + verify(infoBoxManager).addInfoBox(any(SpecialCounter.class)); + + // attack npc 2 + specialCounterPlugin.onInteractingChanged(new InteractingChanged(player, targetB)); + + // hit 1 + specialCounterPlugin.onHitsplatApplied(hitsplat(targetB, Hitsplat.HitsplatType.DAMAGE_ME)); + + verify(infoBoxManager).removeInfoBox(any(SpecialCounter.class)); + } +} \ No newline at end of file