Repository: pajlads/DinkPlugin (java)

Issue:
Death notifs: TOA safe deaths
Currently, all TOA deaths trigger the discord notification, even the safe deaths with the "Ignore Safe Deaths" setting toggled on.
My suggestion is to have this only cause the event when you've run out of lives. If the issue concerns how to know which one is safe, have the screenshot taken on death but wait for the "You failed to survive the Tombs of Amascut." message before actually sending the notification. 

Pull request description:
fix: classify toa attempts as safe deaths
Closes #316


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4edff9d..edb8fd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## 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)
 
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index e4caeef..04a0c09 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -178,6 +178,7 @@ public class DinkPlugin extends Plugin {
                 clueNotifier.onChatMessage(chatMessage);
                 killCountNotifier.onGameMessage(chatMessage);
                 combatTaskNotifier.onGameMessage(chatMessage);
+                deathNotifier.onGameMessage(chatMessage);
                 break;
 
             case FRIENDSCHATNOTIFICATION:
diff --git a/src/main/java/dinkplugin/domain/AccountType.java b/src/main/java/dinkplugin/domain/AccountType.java
index b85773a..ada0d59 100644
--- a/src/main/java/dinkplugin/domain/AccountType.java
+++ b/src/main/java/dinkplugin/domain/AccountType.java
@@ -11,6 +11,10 @@ public enum AccountType {
 
     private static final AccountType[] TYPES = values();
 
+    public boolean isHardcore() {
+        return this == HARDCORE_IRONMAN || this == HARDCORE_GROUP_IRONMAN;
+    }
+
     /**
      * @param varbitValue the value associated with {@link net.runelite.api.Varbits#ACCOUNT_TYPE}
      * @return the equivalent enum value
diff --git a/src/main/java/dinkplugin/notifiers/DeathNotifier.java b/src/main/java/dinkplugin/notifiers/DeathNotifier.java
index 33c2b86..37ef9e9 100644
--- a/src/main/java/dinkplugin/notifiers/DeathNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/DeathNotifier.java
@@ -55,6 +55,8 @@ public class DeathNotifier extends BaseNotifier {
 
     private static final String ATTACK_OPTION = "Attack";
 
+    private static final String TOA_DEATH_MSG = "You failed to survive the Tombs of Amascut";
+
     /**
      * Checks whether the actor is alive and interacting with the specified player.
      */
@@ -107,20 +109,28 @@ public class DeathNotifier extends BaseNotifier {
         boolean self = client.getLocalPlayer() == actor.getActor();
 
         if (self && isEnabled())
-            handleNotify();
+            handleNotify(null);
 
         if (self || actor.getActor() == lastTarget.get())
             lastTarget = new WeakReference<>(null);
     }
 
+    public void onGameMessage(String message) {
+        if (config.deathIgnoreSafe() && !Utils.getAccountType(client).isHardcore() && message.contains(TOA_DEATH_MSG) && isEnabled()) {
+            // https://github.com/pajlads/DinkPlugin/issues/316
+            // though, hardcore (group) ironmen just use the normal ActorDeath trigger for TOA
+            handleNotify(Danger.DANGEROUS);
+        }
+    }
+
     public void onInteraction(InteractingChanged event) {
         if (event.getSource() == client.getLocalPlayer() && event.getTarget() != null && event.getTarget().getCombatLevel() > 0) {
             lastTarget = new WeakReference<>(event.getTarget());
         }
     }
 
-    private void handleNotify() {
-        Danger danger = getDangerLevel(client);
+    private void handleNotify(Danger dangerOverride) {
+        Danger danger = dangerOverride != null ? dangerOverride : getDangerLevel(client);
         if (danger == Danger.SAFE && config.deathIgnoreSafe())
             return;
 
diff --git a/src/main/java/dinkplugin/util/WorldUtils.java b/src/main/java/dinkplugin/util/WorldUtils.java
index 9b8810a..974a0bb 100644
--- a/src/main/java/dinkplugin/util/WorldUtils.java
+++ b/src/main/java/dinkplugin/util/WorldUtils.java
@@ -27,6 +27,7 @@ public class WorldUtils {
     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);
+    private final Set<Integer> TOA_REGIONS = ImmutableSet.of(14160, 14162, 14164, 14674, 14676, 15184, 15186, 15188, 15696, 15698, 15700);
     private final int BURTHORPE_REGION = 8781;
     private final int INFERNO_REGION = 9043;
     private final int NMZ_REGION = 9033;
@@ -105,6 +106,13 @@ public class WorldUtils {
     public boolean isSafeArea(Client client) {
         int regionId = getLocation(client).getRegionID();
 
+        if (isAmascutTombs(regionId)) {
+            // ToA is technically a dangerous activity, but multiple attempts can be permitted
+            // the real TOA death is detected via game message in death notifier
+            // However: any TOA 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
@@ -119,6 +127,10 @@ public class WorldUtils {
         return SOUL_REGIONS.contains(regionId);
     }
 
+    public boolean isAmascutTombs(int regionId) {
+        return TOA_REGIONS.contains(regionId);
+    }
+
     public boolean isTzHaarFightCave(int regionId) {
         return regionId == TZHAAR_CAVE;
     }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java b/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
index 32cf60e..d42288e 100644
--- a/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
@@ -1,6 +1,7 @@
 package dinkplugin.notifiers;
 
 import com.google.inject.testing.fieldbinder.Bind;
+import dinkplugin.domain.AccountType;
 import dinkplugin.message.Embed;
 import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
@@ -27,6 +28,7 @@ import net.runelite.api.events.InteractingChanged;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
+import org.mockito.Mockito;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -355,6 +357,153 @@ class DeathNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testNotifyAmascut() {
+        // update mocks
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(3520, 5120, 0));
+        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
+        notifier.onGameMessage("You failed to survive the Tombs of Amascut");
+
+        // verify notification
+        List<SerializedItemStack> kept = Arrays.asList(
+            ItemUtils.stackFromItem(itemManager, items[0]),
+            ItemUtils.stackFromItem(itemManager, items[3]),
+            ItemUtils.stackFromItem(itemManager, items[4]),
+            ItemUtils.stackFromItem(itemManager, items[2])
+        );
+        List<SerializedItemStack> lost = Collections.singletonList(
+            ItemUtils.stackFromItem(itemManager, items[1])
+        );
+        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 testNotifyAmascutSafe() {
+        // update mocks
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(3520, 5120, 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 testNotifyAmascutHardcoreIron() {
+        // update mocks
+        when(client.getVarbitValue(Varbits.ACCOUNT_TYPE)).thenReturn(AccountType.HARDCORE_IRONMAN.ordinal());
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(3520, 5120, 0));
+        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.asList(
+            ItemUtils.stackFromItem(itemManager, items[0]),
+            ItemUtils.stackFromItem(itemManager, items[3]),
+            ItemUtils.stackFromItem(itemManager, items[4]),
+            ItemUtils.stackFromItem(itemManager, items[2])
+        );
+        List<SerializedItemStack> lost = Collections.singletonList(
+            ItemUtils.stackFromItem(itemManager, items[1])
+        );
+        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()
+        );
+
+        // ensure TOA death chat message is ignored for HCIM
+        notifier.onGameMessage("You failed to survive the Tombs of Amascut");
+        Mockito.verifyNoMoreInteractions(messageHandler);
+    }
+
+    @Test
+    void testIgnoreAmascut() {
+        // update mocks
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(3520, 5120, 0));
+        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));
+
+        // ensure no notification occurred
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     void testIgnore() {
         // prepare mock