Repository: pajlads/DinkPlugin (java)

Issue:
Barbarian Assault Penance Queen Kill Screenshot
I help run a bingo event where automating the posting of a screenshot whenever someone kills the penance queen would be great to automatically track. This could be either when the queen dies or when the player is sent back to the lobby. Additional things that would be nice but not needed would be the names of teammates and the role the user was.

Pull request description:
feat: send penance queen kills as kc notification
Closes #232

![image](https://github.com/pajlads/DinkPlugin/assets/8106344/beb45c4d-86dd-4249-90d1-ca72b1348ded)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe056fc..301bc59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- Minor: Send kill count notifications for Penance Queen kills. (#304)
 - Minor: Include popup notification widget in collection log screenshots. (#309)
 - Minor: Support new thread creation for Discord forum channels. (#302)
 - Minor: Indicate whether pets are already owned in pet notifications. (#303, #307)
diff --git a/README.md b/README.md
index 39a61c9..ff17c57 100644
--- a/README.md
+++ b/README.md
@@ -424,6 +424,8 @@ Note: `killCount` is only specified for NPC loot with the base RuneLite Loot Tra
 }
 ```
 
+Note: when `boss` is `Penance Queen`, `count` refers to the high level gamble count, rather than kill count.
+
 </details>
 
 ### Combat Achievements:
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index b6c1a40..37565f3 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -249,6 +249,7 @@ public class DinkPlugin extends Plugin {
         speedrunNotifier.onWidgetLoaded(event);
         lootNotifier.onWidgetLoaded(event);
         groupStorageNotifier.onWidgetLoad(event);
+        killCountNotifier.onWidget(event);
     }
 
     @Subscribe
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index 3f413e5..3b6eccd 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -1141,6 +1141,17 @@ public interface DinkPluginConfig extends Config {
         return 50;
     }
 
+    @ConfigItem(
+        keyName = "killCountPenanceQueen",
+        name = "Barbarian Assault",
+        description = "Notify for any Penance Queen kills",
+        position = 95,
+        section = killCountSection
+    )
+    default boolean killCountPenanceQueen() {
+        return true;
+    }
+
     @ConfigItem(
         keyName = "killCountMessage",
         name = "Notification Message",
@@ -1148,7 +1159,7 @@ public interface DinkPluginConfig extends Config {
             "Use %USERNAME% to insert your username<br/>" +
             "Use %BOSS% to insert the NPC name<br/>" +
             "Use %COUNT% to insert the kill count",
-        position = 95,
+        position = 96,
         section = killCountSection
     )
     default String killCountMessage() {
@@ -1163,7 +1174,7 @@ public interface DinkPluginConfig extends Config {
             "Use %BOSS% to insert the NPC name<br/>" +
             "Use %COUNT% to insert the kill count<br/>" +
             "Use %TIME% to insert the completion time",
-        position = 96,
+        position = 97,
         section = killCountSection
     )
     default String killCountBestTimeMessage() {
diff --git a/src/main/java/dinkplugin/notifiers/KillCountNotifier.java b/src/main/java/dinkplugin/notifiers/KillCountNotifier.java
index 2bb599b..bbf00d1 100644
--- a/src/main/java/dinkplugin/notifiers/KillCountNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/KillCountNotifier.java
@@ -11,7 +11,13 @@ import dinkplugin.message.NotificationType;
 import dinkplugin.notifiers.data.BossNotificationData;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.NPC;
+import net.runelite.api.NpcID;
+import net.runelite.api.Varbits;
 import net.runelite.api.annotations.Varbit;
+import net.runelite.api.events.WidgetLoaded;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetID;
+import net.runelite.api.widgets.WidgetInfo;
 import org.apache.commons.lang3.tuple.Pair;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.VisibleForTesting;
@@ -41,6 +47,8 @@ public class KillCountNotifier extends BaseNotifier {
     private static final Pattern SECONDARY_REGEX = Pattern.compile("Your (?:completed|subdued) (?<key>.+) count is: (?<value>\\d+)\\b");
     private static final Pattern TIME_REGEX = Pattern.compile("(?:Duration|time|Subdued in):? (?<time>[\\d:]+(.\\d+)?)\\.?", Pattern.CASE_INSENSITIVE);
 
+    private static final String BA_BOSS_NAME = "Penance Queen";
+
     /**
      * The maximum number of ticks to hold onto a fight duration without a corresponding boss name.
      * <p>
@@ -79,6 +87,21 @@ public class KillCountNotifier extends BaseNotifier {
             this.onGameMessage(message);
     }
 
+    public void onWidget(WidgetLoaded event) {
+        if (!isEnabled())
+            return;
+
+        // Barbarian Assault: Track Penance Queen kills
+        if (config.killCountPenanceQueen() && event.getGroupId() == WidgetID.BA_REWARD_GROUP_ID) {
+            Widget widget = client.getWidget(WidgetInfo.BA_REWARD_TEXT);
+            // https://oldschool.runescape.wiki/w/Barbarian_Assault/Rewards#Earning_Honour_points
+            if (widget != null && widget.getText().contains("80 ") && widget.getText().contains("5 ")) {
+                int gambleCount = client.getVarbitValue(Varbits.BA_GC);
+                this.data.set(new BossNotificationData(BA_BOSS_NAME, gambleCount, "The Queen is dead!", null, null));
+            }
+        }
+    }
+
     public void onTick() {
         BossNotificationData data = this.data.get();
         if (data != null) {
@@ -99,8 +122,9 @@ public class KillCountNotifier extends BaseNotifier {
         if (data.getBoss() == null || data.getCount() == null)
             return;
 
-        // ensure interval met or pb, depending on config
-        if (!checkKillInterval(data.getCount(), data.isPersonalBest()))
+        // ensure interval met or pb or ba, depending on config
+        boolean ba = data.getBoss().equals(BA_BOSS_NAME);
+        if (!checkKillInterval(data.getCount(), data.isPersonalBest()) && !ba)
             return;
 
         // Assemble content
@@ -112,7 +136,7 @@ public class KillCountNotifier extends BaseNotifier {
             .replacementBoundary("%")
             .replacement("%USERNAME%", Replacements.ofText(player))
             .replacement("%BOSS%", Replacements.ofWiki(data.getBoss()))
-            .replacement("%COUNT%", Replacements.ofText(String.valueOf(data.getCount())))
+            .replacement("%COUNT%", Replacements.ofText(data.getCount() + (ba ? " high gambles" : "")))
             .replacement("%TIME%", Replacements.ofText(time))
             .build();
 
@@ -126,16 +150,21 @@ public class KillCountNotifier extends BaseNotifier {
 
         // Add embed if not screenshotting
         boolean screenshot = config.killCountSendImage();
-        if (!screenshot)
-            Arrays.stream(client.getCachedNPCs())
-                .filter(Objects::nonNull)
-                .filter(npc -> data.getBoss().equalsIgnoreCase(npc.getName()))
-                .findAny()
-                .map(NPC::getId)
-                .map(ItemUtils::getNpcImageUrl)
-                .map(Embed::ofImage)
-                .map(Collections::singletonList)
-                .ifPresent(body::embeds);
+        if (!screenshot) {
+            if (ba) {
+                body.embeds(Collections.singletonList(Embed.ofImage(ItemUtils.getNpcImageUrl(NpcID.PENANCE_QUEEN))));
+            } else {
+                Arrays.stream(client.getCachedNPCs())
+                    .filter(Objects::nonNull)
+                    .filter(npc -> data.getBoss().equalsIgnoreCase(npc.getName()))
+                    .findAny()
+                    .map(NPC::getId)
+                    .map(ItemUtils::getNpcImageUrl)
+                    .map(Embed::ofImage)
+                    .map(Collections::singletonList)
+                    .ifPresent(body::embeds);
+            }
+        }
 
         // Call webhook
         createMessage(screenshot, body.build());


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/KillCountNotifierTest.java b/src/test/java/dinkplugin/notifiers/KillCountNotifierTest.java
index 3e818fb..2ee277b 100644
--- a/src/test/java/dinkplugin/notifiers/KillCountNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/KillCountNotifierTest.java
@@ -1,24 +1,34 @@
 package dinkplugin.notifiers;
 
 import com.google.inject.testing.fieldbinder.Bind;
+import dinkplugin.message.Embed;
 import dinkplugin.message.templating.Replacements;
 import dinkplugin.message.templating.Template;
+import dinkplugin.util.ItemUtils;
 import dinkplugin.util.TimeUtils;
 import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
 import dinkplugin.notifiers.data.BossNotificationData;
 import net.runelite.api.NPC;
+import net.runelite.api.NpcID;
+import net.runelite.api.Varbits;
+import net.runelite.api.events.WidgetLoaded;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetID;
+import net.runelite.api.widgets.WidgetInfo;
 import net.runelite.http.api.RuneLiteAPI;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 
 import java.time.Duration;
+import java.util.Collections;
 import java.util.stream.IntStream;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -413,6 +423,62 @@ class KillCountNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testNotifyBarbarianAssault() {
+        // update mocks
+        int count = 420;
+        String boss = "Penance Queen";
+        when(config.killCountPenanceQueen()).thenReturn(true);
+        when(config.killCountSendImage()).thenReturn(false);
+        when(client.getVarbitValue(Varbits.BA_GC)).thenReturn(count);
+        Widget widget = mock(Widget.class);
+        when(widget.getText()).thenReturn("Reward:<br>80 Healer points<br>5 Defender points<br>5 Collector points<br>5 Attacker points");
+        when(client.getWidget(WidgetInfo.BA_REWARD_TEXT)).thenReturn(widget);
+
+        // fire event
+        WidgetLoaded event = new WidgetLoaded();
+        event.setGroupId(WidgetID.BA_REWARD_GROUP_ID);
+        notifier.onWidget(event);
+        notifier.onTick();
+
+        // check notification
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(
+                    Template.builder()
+                        .template(PLAYER_NAME + " has defeated {{boss}} with a completion count of " + count + " high gambles")
+                        .replacement("{{boss}}", Replacements.ofWiki(boss))
+                        .build()
+                )
+                .extra(new BossNotificationData(boss, count, "The Queen is dead!", null, null))
+                .playerName(PLAYER_NAME)
+                .type(NotificationType.KILL_COUNT)
+                .embeds(Collections.singletonList(Embed.ofImage(ItemUtils.getNpcImageUrl(NpcID.PENANCE_QUEEN))))
+                .build()
+        );
+    }
+
+    @Test
+    void testIgnoreBarbarianAssault() {
+        // update mocks
+        when(config.killCountPenanceQueen()).thenReturn(false);
+        when(client.getVarbitValue(Varbits.BA_GC)).thenReturn(420);
+        Widget widget = mock(Widget.class);
+        when(widget.getText()).thenReturn("Reward:<br>80 Healer points<br>5 Defender points<br>5 Collector points<br>5 Attacker points");
+        when(client.getWidget(WidgetInfo.BA_REWARD_TEXT)).thenReturn(widget);
+
+        // fire event
+        WidgetLoaded event = new WidgetLoaded();
+        event.setGroupId(WidgetID.BA_REWARD_GROUP_ID);
+        notifier.onWidget(event);
+        notifier.onTick();
+
+        // ensure no message
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     void testIgnoreNoPb() {
         // more config