From 67030e38a44f355a0131cff5250ed45c7b01dc53 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 1 Jul 2020 14:30:03 -0400 Subject: [PATCH] slayer plugin: better support multikills This observes deaths of tagged npcs each tick and will use those if available instead of assuming one kill per xpdrop --- .../client/plugins/slayer/SlayerPlugin.java | 77 +++++++++++++++---- .../plugins/slayer/SlayerPluginTest.java | 71 ++++++++++++++++- 2 files changed, 132 insertions(+), 16 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java index 6b8ade7b65..4148822fc6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java @@ -30,11 +30,14 @@ import com.google.inject.Provides; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; +import static java.lang.Integer.max; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,18 +47,22 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.GameState; +import net.runelite.api.Hitsplat; import net.runelite.api.ItemID; import net.runelite.api.MessageNode; import net.runelite.api.NPC; import net.runelite.api.NPCComposition; import static net.runelite.api.Skill.SLAYER; import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ActorDeath; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; import net.runelite.api.events.StatChanged; @@ -174,6 +181,10 @@ public class SlayerPlugin extends Plugin @Getter(AccessLevel.PACKAGE) private List highlightedTargets = new ArrayList<>(); + private final Set taggedNpcs = new HashSet<>(); + private int taggedNpcsDiedPrevTick; + private int taggedNpcsDiedThisTick; + @Getter(AccessLevel.PACKAGE) @Setter(AccessLevel.PACKAGE) private int amount; @@ -237,6 +248,7 @@ public class SlayerPlugin extends Plugin overlayManager.remove(targetMinimapOverlay); removeCounter(); highlightedTargets.clear(); + taggedNpcs.clear(); cachedXp = -1; chatCommandManager.unregisterCommand(TASK_COMMAND_STRING); @@ -260,6 +272,7 @@ public class SlayerPlugin extends Plugin amount = 0; loginFlag = true; highlightedTargets.clear(); + taggedNpcs.clear(); break; case LOGGED_IN: if (config.amount() != -1 @@ -299,6 +312,7 @@ public class SlayerPlugin extends Plugin public void onNpcDespawned(NpcDespawned npcDespawned) { NPC npc = npcDespawned.getNpc(); + taggedNpcs.remove(npc); highlightedTargets.remove(npc); } @@ -391,6 +405,9 @@ public class SlayerPlugin extends Plugin removeCounter(); } } + + taggedNpcsDiedPrevTick = taggedNpcsDiedThisTick; + taggedNpcsDiedThisTick = 0; } @Subscribe @@ -541,20 +558,49 @@ public class SlayerPlugin extends Plugin return; } - final Task task = Task.getTask(taskName); - - // null tasks are technically valid, it only means they arent explicitly defined in the Task enum - // allow them through so that if there is a task capture failure the counter will still work - final int taskKillExp = task != null ? task.getExpectedKillExp() : 0; - - // Only count exp gain as a kill if the task either has no expected exp for a kill, or if the exp gain is equal - // to the expected exp gain for the task. - if (taskKillExp == 0 || taskKillExp == slayerExp - cachedXp) - { - killedOne(); - } - + final int delta = slayerExp - cachedXp; cachedXp = slayerExp; + + log.debug("Slayer xp change delta: {}, killed npcs: {}", delta, taggedNpcsDiedPrevTick); + + final Task task = Task.getTask(taskName); + if (task != null && task.getExpectedKillExp() > 0) + { + // Only decrement a kill if the xp drop matches the expected drop. This is just for Tzhaar tasks. + if (task.getExpectedKillExp() == delta) + { + killed(1); + } + } + else + { + // This is at least one kill, but if we observe multiple tagged NPCs dieing on the previous tick, count them + // instead. + killed(max(taggedNpcsDiedPrevTick, 1)); + } + } + + @Subscribe + public void onHitsplatApplied(HitsplatApplied hitsplatApplied) + { + Actor actor = hitsplatApplied.getActor(); + Hitsplat hitsplat = hitsplatApplied.getHitsplat(); + if (hitsplat.getHitsplatType() == Hitsplat.HitsplatType.DAMAGE_ME && highlightedTargets.contains(actor)) + { + // If the actor is in highlightedTargets it must be an NPC and also a task assignment + taggedNpcs.add((NPC) actor); + } + } + + @Subscribe + public void onActorDeath(ActorDeath actorDeath) + { + Actor actor = actorDeath.getActor(); + if (taggedNpcs.contains(actor)) + { + log.debug("Tagged NPC {} has died", actor.getName()); + ++taggedNpcsDiedThisTick; + } } @Subscribe @@ -576,16 +622,17 @@ public class SlayerPlugin extends Plugin } @VisibleForTesting - void killedOne() + void killed(int amt) { if (amount == 0) { return; } - amount--; + amount -= amt; if (doubleTroubleExtraKill()) { + assert amt == 1; amount--; } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java index 1c0bbb67f9..4cd9bec315 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java @@ -28,19 +28,25 @@ import com.google.inject.Guice; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; import java.io.IOException; +import java.util.Arrays; import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; import net.runelite.api.ChatMessageType; import static net.runelite.api.ChatMessageType.GAMEMESSAGE; import net.runelite.api.Client; import net.runelite.api.GameState; +import net.runelite.api.Hitsplat; import net.runelite.api.MessageNode; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; import net.runelite.api.Player; import net.runelite.api.Skill; import net.runelite.api.coords.LocalPoint; +import net.runelite.api.events.ActorDeath; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; import net.runelite.api.events.StatChanged; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; @@ -771,7 +777,7 @@ public class SlayerPluginTest slayerPlugin.onChatMessage(chatMessage); assertEquals("Suqahs", slayerPlugin.getTaskName()); - slayerPlugin.killedOne(); + slayerPlugin.killed(1); assertEquals(30, slayerPlugin.getAmount()); } @@ -863,4 +869,67 @@ public class SlayerPluginTest verify(infoBoxManager, never()).addInfoBox(any()); } + + @Test + public void testMultikill() + { + final Player player = mock(Player.class); + when(player.getLocalLocation()).thenReturn(new LocalPoint(0, 0)); + when(client.getLocalPlayer()).thenReturn(player); + + // Setup xp cache + StatChanged statChanged = new StatChanged( + Skill.SLAYER, + 0, + 1, + 1 + ); + slayerPlugin.onStatChanged(statChanged); + + NPCComposition npcComposition = mock(NPCComposition.class); + when(npcComposition.getActions()).thenReturn(new String[]{"Attack"}); + + NPC npc1 = mock(NPC.class); + when(npc1.getName()).thenReturn("Suqah"); + when(npc1.getTransformedComposition()).thenReturn(npcComposition); + + NPC npc2 = mock(NPC.class); + when(npc2.getName()).thenReturn("Suqah"); + when(npc2.getTransformedComposition()).thenReturn(npcComposition); + + when(client.getNpcs()).thenReturn(Arrays.asList(npc1, npc2)); + + // Set task + Widget npcDialog = mock(Widget.class); + when(npcDialog.getText()).thenReturn(TASK_NEW); + when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); + slayerPlugin.onGameTick(new GameTick()); + + // Damage both npcs + Hitsplat hitsplat = new Hitsplat(Hitsplat.HitsplatType.DAMAGE_ME, 1, 1); + HitsplatApplied hitsplatApplied = new HitsplatApplied(); + hitsplatApplied.setHitsplat(hitsplat); + hitsplatApplied.setActor(npc1); + slayerPlugin.onHitsplatApplied(hitsplatApplied); + + hitsplatApplied.setActor(npc2); + slayerPlugin.onHitsplatApplied(hitsplatApplied); + + // Kill both npcs + slayerPlugin.onActorDeath(new ActorDeath(npc1)); + slayerPlugin.onActorDeath(new ActorDeath(npc2)); + + slayerPlugin.onGameTick(new GameTick()); + + statChanged = new StatChanged( + Skill.SLAYER, + 105, + 2, + 2 + ); + slayerPlugin.onStatChanged(statChanged); + + assertEquals("Suqahs", slayerPlugin.getTaskName()); + assertEquals(229, slayerPlugin.getAmount()); // 2 kills + } }