Repository: pajlads/DinkPlugin (java)

Issue:
Loot -> PVP loot
### Checklist

- [X] I'm reporting a problem with Dink
- [X] I've searched the [issues and pull requests](https://github.com/pajlads/Dinkplugin/issues?q=) for similar looking reports

### Describe your issue

Hey, we have been testing new "Dink" plugins "Loot" feature "send pk loot to PK URL" and it works as it supposed to be, does the job, but I think we met problem, where it tracks value of single item in the "loot chest" rather than whole chest value, where if you put values like this: for 1m and if the chest has key for 10m total value, but no single item worth 1m+, It doesnt pop out in discord.
Would be awesome if loot keys would be checked as total value, not per single item when it gets opened.

### Screenshots

![image](https://github.com/pajlads/DinkPlugin/assets/150721128/ea8a8802-ca12-490b-aa5c-c768f1a732e1)


### Runelite Logs

.

### Runelite Version

1.10.18.2

Pull request description:
feat: fire loot notif for pk chests if total value exceeds min
Closes #403


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9beb686..beec98c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- Minor: Fire loot notification for PK loot chests with sufficiently high total value. (#417)
 - Minor: Include region information in death notification metadata. (#420)
 - Minor: Allow customization of region IDs where deaths should be ignored. (#415)
 - Bugfix: Treat deaths in the graveyard room of the mage training arena as safe. (#418)
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index a297890..fa1acc4 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -793,7 +793,8 @@ public interface DinkPluginConfig extends Config {
     @ConfigItem(
         keyName = "minLootValue",
         name = "Min Loot value",
-        description = "The minimum value of an item for a notification to be sent",
+        description = "The minimum value of an item for a notification to be sent.<br/>" +
+            "For PK chests, the <i>total</i> value of the items is compared with this threshold",
         position = 33,
         section = lootSection
     )
diff --git a/src/main/java/dinkplugin/notifiers/LootNotifier.java b/src/main/java/dinkplugin/notifiers/LootNotifier.java
index f6e356c..77cde75 100644
--- a/src/main/java/dinkplugin/notifiers/LootNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/LootNotifier.java
@@ -7,6 +7,7 @@ import com.google.gson.JsonSyntaxException;
 import dinkplugin.message.Embed;
 import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
+import dinkplugin.message.templating.Evaluable;
 import dinkplugin.message.templating.Replacements;
 import dinkplugin.message.templating.Template;
 import dinkplugin.message.templating.impl.JoiningReplacement;
@@ -198,10 +199,12 @@ public class LootNotifier extends BaseNotifier {
             SerializedItemStack stack = ItemUtils.stackFromItem(itemManager, item.getId(), item.getQuantity());
             long totalPrice = stack.getTotalPrice();
             boolean worthy = totalPrice >= minValue || matches(itemNameAllowlist, stack.getName());
-            if (worthy && !matches(itemNameDenylist, stack.getName())) {
-                sendMessage = true;
-                lootMessage.component(ItemUtils.templateStack(stack, true));
-                if (icons) embeds.add(Embed.ofImage(ItemUtils.getItemImageUrl(item.getId())));
+            if (!matches(itemNameDenylist, stack.getName())) {
+                if (worthy) {
+                    sendMessage = true;
+                    lootMessage.component(ItemUtils.templateStack(stack, true));
+                    if (icons) embeds.add(Embed.ofImage(ItemUtils.getItemImageUrl(item.getId())));
+                }
                 if (max == null || totalPrice > max.getTotalPrice()) {
                     max = stack;
                 }
@@ -210,6 +213,26 @@ public class LootNotifier extends BaseNotifier {
             totalStackValue += totalPrice;
         }
 
+        Evaluable lootMsg;
+        if (!sendMessage && totalStackValue >= minValue && max != null && "Loot Chest".equalsIgnoreCase(dropper)) {
+            // Special case: PK loot keys should trigger notification if total value exceeds configured minimum even
+            // if no single item itself would exceed the min value config - github.com/pajlads/DinkPlugin/issues/403
+            sendMessage = true;
+
+            JoiningReplacement msg = lootMessage.build();
+            if (msg.getComponents().isEmpty()) {
+                // ensure %LOOT% isn't empty
+                lootMsg = Replacements.ofMultiple(" ",
+                    Replacements.ofText("Various items including:"),
+                    ItemUtils.templateStack(max, true)
+                );
+            } else {
+                lootMsg = msg;
+            }
+        } else {
+            lootMsg = lootMessage.build();
+        }
+
         if (sendMessage) {
             String overrideUrl = getWebhookUrl();
             if (config.lootRedirectPlayerKill() && !config.pkWebhook().isBlank()) {
@@ -222,7 +245,7 @@ public class LootNotifier extends BaseNotifier {
                 .template(config.lootNotifyMessage())
                 .replacementBoundary("%")
                 .replacement("%USERNAME%", Replacements.ofText(Utils.getPlayerName(client)))
-                .replacement("%LOOT%", lootMessage.build())
+                .replacement("%LOOT%", lootMsg)
                 .replacement("%TOTAL_VALUE%", Replacements.ofText(QuantityFormatter.quantityToStackSize(totalStackValue)))
                 .replacement("%SOURCE%", Replacements.ofText(dropper))
                 .build();


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
index 23278e0..7d3bc34 100644
--- a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
@@ -7,6 +7,7 @@ import dinkplugin.message.templating.Replacements;
 import dinkplugin.message.templating.Template;
 import dinkplugin.notifiers.data.LootNotificationData;
 import dinkplugin.notifiers.data.SerializedItemStack;
+import dinkplugin.util.ItemUtils;
 import net.runelite.api.ItemID;
 import net.runelite.api.NPC;
 import net.runelite.api.NpcID;
@@ -26,6 +27,7 @@ import org.mockito.InjectMocks;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -395,6 +397,55 @@ class LootNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testNotifyPkChest() {
+        // update mocks
+        final int minValue = 750;
+        when(config.minLootValue()).thenReturn(minValue);
+        assert OPAL_PRICE < minValue && TUNA_PRICE < minValue;
+
+        // fire event
+        String source = "Loot Chest";
+        List<ItemStack> items = List.of(new ItemStack(ItemID.OPAL, 1, null), new ItemStack(ItemID.TUNA, 2, null));
+        int totalValue = OPAL_PRICE + 2 * TUNA_PRICE;
+        LootReceived event = new LootReceived(source, -1, LootRecordType.EVENT, items, 1);
+        plugin.onLootReceived(event);
+
+        // verify notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(
+                    Template.builder()
+                        .template(String.format("%s has looted: Various items including: 1 x {{opal}} (%d) from %s for %s gp", PLAYER_NAME, OPAL_PRICE, source, QuantityFormatter.quantityToStackSize(totalValue)))
+                        .replacement("{{opal}}", Replacements.ofWiki("Opal"))
+                        .build()
+                )
+                .extra(new LootNotificationData(List.of(new SerializedItemStack(ItemID.OPAL, 1, OPAL_PRICE, "Opal"), new SerializedItemStack(ItemID.TUNA, 2, TUNA_PRICE, "Tuna")), source, LootRecordType.EVENT, null))
+                .type(NotificationType.LOOT)
+                .thumbnailUrl(ItemUtils.getItemImageUrl(ItemID.TUNA))
+                .build()
+        );
+    }
+
+    @Test
+    void testIgnorePkChest() {
+        // update mocks
+        final int minValue = 750;
+        when(config.minLootValue()).thenReturn(minValue);
+        assert OPAL_PRICE < minValue && TUNA_PRICE < minValue;
+
+        // fire event
+        String source = "Loot Chest";
+        List<ItemStack> items = List.of(new ItemStack(ItemID.OPAL, 1, null), new ItemStack(ItemID.TUNA, 1, null));
+        LootReceived event = new LootReceived(source, -1, LootRecordType.EVENT, items, 1);
+        plugin.onLootReceived(event);
+
+        // ensure no notification
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     void testIgnorePlayer() {
         // prepare mocks