Repository: pajlads/DinkPlugin (java)

Issue:
[Bug] CG deaths are considered dangerous for non-hardcore accounts
To test, die in CG to hunllef immediately after entering with "ignore safe deaths" toggled OFF, and set min lost value to 4. The death will not track. 

Set min lost value to 3. Immediately die to hunllef, and it will track.

(These numbers are based off current GE pestle + mortar price, which is 3 gp as of 10/3/2023)

Pull request description:
fix: treat gauntlet deaths as safe unless hardcore iron
Closes #326


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bfad6c..69cbb70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
 ## Unreleased
 
-- Bugfix: Classify deaths in Tombs of Abascut as safe or dangerous depending on the attempt invocations. (#317)
 - Minor: Indicate in pet notification metadata when a pet was previously owned but lost. (#314)
 - Minor: Support wildcards in loot item name filters. (#312)
+- Bugfix: Classify Gauntlet deaths as safe, unless the player is a hardcore ironman. (#327)
+- Bugfix: Classify deaths in Tombs of Abascut as safe or dangerous depending on the attempt invocations. (#317)
 - Dev: Update gradle wrapper to v8.4 minor version. (#328)
 - Dev: Add pet test for Lil' creator. (#325)
 
diff --git a/src/main/java/dinkplugin/util/WorldUtils.java b/src/main/java/dinkplugin/util/WorldUtils.java
index 974a0bb..d9c9391 100644
--- a/src/main/java/dinkplugin/util/WorldUtils.java
+++ b/src/main/java/dinkplugin/util/WorldUtils.java
@@ -24,6 +24,7 @@ public class WorldUtils {
     private final Set<Integer> CASTLE_WARS_REGIONS = ImmutableSet.of(9520, 9620);
     private final Set<Integer> CLAN_WARS_REGIONS = ImmutableSet.of(12621, 12622, 12623, 13130, 13131, 13133, 13134, 13135, 13386, 13387, 13390, 13641, 13642, 13643, 13644, 13645, 13646, 13647, 13899, 13900, 14155, 14156);
     private final Set<Integer> COX_REGIONS = ImmutableSet.of(12889, 13136, 13137, 13138, 13139, 13140, 13141, 13145, 13393, 13394, 13395, 13396, 13397, 13401);
+    private final Set<Integer> GAUNTLET_REGIONS = ImmutableSet.of(7512, 7768, 12127); // includes CG
     private final Set<Integer> LMS_REGIONS = ImmutableSet.of(13658, 13659, 13660, 13914, 13915, 13916, 13918, 13919, 13920, 14174, 14175, 14176, 14430, 14431, 14432);
     private final Set<Integer> POH_REGIONS = ImmutableSet.of(7257, 7513, 7514, 7769, 7770, 8025, 8026);
     private final Set<Integer> SOUL_REGIONS = ImmutableSet.of(8493, 8748, 8749, 9005);
@@ -78,6 +79,10 @@ public class WorldUtils {
         return CLAN_WARS_REGIONS.contains(regionId);
     }
 
+    public boolean isGauntlet(int regionId) {
+        return GAUNTLET_REGIONS.contains(regionId);
+    }
+
     public boolean isInferno(int regionId) {
         return regionId == INFERNO_REGION;
     }
@@ -113,6 +118,12 @@ public class WorldUtils {
             return !Utils.getAccountType(client).isHardcore();
         }
 
+        if (isGauntlet(regionId)) {
+            // Players can't take items in or out of (Corrupted) Gauntlet, so these deaths are effectively safe
+            // However: any Gauntlet death is still dangerous for hardcore (group) ironmen
+            return !Utils.getAccountType(client).isHardcore();
+        }
+
         if (isBarbarianAssault(regionId) || isChambersOfXeric(regionId) || isInferno(regionId) ||
             isNightmareZone(regionId) || isTzHaarFightCave(regionId) || isPestControl(client)) {
             // All PvM activities are dangerous for Hardcore group iron players


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java b/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
index d42288e..fc58458 100644
--- a/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
@@ -504,6 +504,95 @@ class DeathNotifierTest extends MockedNotifierTest {
         verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
     }
 
+    @Test
+    void testNotifyGauntletSafe() {
+        // update mocks
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(1950, 5650, 0));
+        when(config.deathIgnoreSafe()).thenReturn(false);
+        when(config.deathEmbedKeptItems()).thenReturn(false);
+        when(client.isPrayerActive(Prayer.PROTECT_ITEM)).thenReturn(true);
+        Item[] items = {
+            new Item(ItemID.RUBY, 1),
+            new Item(ItemID.TUNA, 1),
+            new Item(ItemID.COAL, 1),
+            new Item(ItemID.SHARK, 1),
+            new Item(ItemID.OPAL, 1),
+        };
+        ItemContainer inv = mock(ItemContainer.class);
+        when(client.getItemContainer(InventoryID.INVENTORY)).thenReturn(inv);
+        when(inv.getItems()).thenReturn(items);
+
+        // fire event
+        plugin.onActorDeath(new ActorDeath(localPlayer));
+
+        // verify notification
+        List<SerializedItemStack> kept = Arrays.stream(items)
+            .map(i -> ItemUtils.stackFromItem(itemManager, i))
+            .sorted(Comparator.comparingLong(SerializedItemStack::getTotalPrice).reversed())
+            .collect(Collectors.toList());
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(buildTemplate(String.format("%s has died, losing %d gp", PLAYER_NAME, 0)))
+                .extra(new DeathNotificationData(0L, false, null, null, null, kept, Collections.emptyList()))
+                .type(NotificationType.DEATH)
+                .build()
+        );
+    }
+
+    @Test
+    void testNotifyGauntletHardcore() {
+        // update mocks
+        when(client.getVarbitValue(Varbits.ACCOUNT_TYPE)).thenReturn(3);
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(1950, 5650, 0));
+        when(config.deathIgnoreSafe()).thenReturn(true);
+        when(config.deathEmbedKeptItems()).thenReturn(false);
+        when(client.isPrayerActive(Prayer.PROTECT_ITEM)).thenReturn(true);
+        Item[] items = {
+            new Item(ItemID.TUNA, 1),
+            new Item(ItemID.RUBY, 1),
+            new Item(ItemID.COAL, 1),
+            new Item(ItemID.SHARK, 1),
+            new Item(ItemID.OPAL, 1),
+        };
+        ItemContainer inv = mock(ItemContainer.class);
+        when(client.getItemContainer(InventoryID.INVENTORY)).thenReturn(inv);
+        when(inv.getItems()).thenReturn(items);
+
+        // fire event
+        plugin.onActorDeath(new ActorDeath(localPlayer));
+
+        // verify notification
+        List<SerializedItemStack> lost = Collections.singletonList(ItemUtils.stackFromItem(itemManager, items[0]));
+        List<SerializedItemStack> kept = Arrays.stream(items)
+            .skip(1) // tuna is lost (least valuable)
+            .map(i -> ItemUtils.stackFromItem(itemManager, i))
+            .sorted(Comparator.comparingLong(SerializedItemStack::getTotalPrice).reversed())
+            .collect(Collectors.toList());
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(buildTemplate(String.format("%s has died, losing %d gp", PLAYER_NAME, TUNA_PRICE)))
+                .extra(new DeathNotificationData(TUNA_PRICE, false, null, null, null, kept, lost))
+                .type(NotificationType.DEATH)
+                .build()
+        );
+    }
+
+    @Test
+    void testIgnoreGauntlet() {
+        // mock corrupted gauntlet
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(1950, 5650, 0));
+
+        // fire event
+        plugin.onActorDeath(new ActorDeath(localPlayer));
+
+        // ensure no notification occurred
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     void testIgnore() {
         // prepare mock