diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java
index 3b6f84823e..0b2151ab16 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java
@@ -25,6 +25,7 @@
*/
package net.runelite.client.plugins.timers;
+import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Provides;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -43,10 +44,12 @@ import net.runelite.api.ItemID;
import net.runelite.api.NPC;
import net.runelite.api.NpcID;
import net.runelite.api.Player;
+import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.WorldType;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.AnimationChanged;
+import net.runelite.api.events.StatChanged;
import net.runelite.api.events.ChatMessage;
import net.runelite.client.events.ConfigChanged;
import net.runelite.api.events.GameStateChanged;
@@ -112,6 +115,9 @@ public class TimersPlugin extends Plugin
private static final Pattern HALF_TELEBLOCK_PATTERN = Pattern.compile("
A Tele Block spell has been cast on you by (.+)\\. It will expire in 2 minutes, 30 seconds\\.");
private static final Pattern DIVINE_POTION_PATTERN = Pattern.compile("You drink some of your divine (.+) potion\\.");
+ @VisibleForTesting
+ static final int IMBUED_HEART_MIN_CERTAIN_BOOST_LEVEL = 40; // Before this level, other effects can grant boosts of equal amounts
+
private TimerTimer freezeTimer;
private int freezeTime = -1; // time frozen, in game ticks
@@ -892,6 +898,24 @@ public class TimersPlugin extends Plugin
}
}
+ @Subscribe
+ public void onStatChanged(StatChanged statChanged)
+ {
+ if (statChanged.getSkill() != Skill.MAGIC || !config.showImbuedHeart())
+ {
+ return;
+ }
+
+ final int magicLevel = statChanged.getLevel();
+ final int boostAmount = statChanged.getBoostedLevel() - magicLevel;
+ final int heartBoost = 1 + (magicLevel / 10);
+
+ if (magicLevel >= IMBUED_HEART_MIN_CERTAIN_BOOST_LEVEL && boostAmount == heartBoost)
+ {
+ createGameTimer(IMBUEDHEART);
+ }
+ }
+
private TimerTimer createGameTimer(final GameTimer timer)
{
removeGameTimer(timer);
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java
index 976b0e2970..cf359ed14d 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java
@@ -31,8 +31,11 @@ import com.google.inject.testing.fieldbinder.BoundFieldModule;
import java.util.EnumSet;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
+import net.runelite.api.Experience;
+import net.runelite.api.Skill;
import net.runelite.api.WorldType;
import net.runelite.api.events.ChatMessage;
+import net.runelite.api.events.StatChanged;
import net.runelite.client.game.ItemManager;
import net.runelite.client.game.SpriteManager;
import net.runelite.client.ui.overlay.infobox.InfoBox;
@@ -42,7 +45,10 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import static org.mockito.ArgumentMatchers.any;
import org.mockito.Mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.mockito.junit.MockitoJUnitRunner;
@@ -136,4 +142,44 @@ public class TimersPluginTest
TimerTimer infoBox = (TimerTimer) captor.getValue();
assertEquals(GameTimer.DMM_FULLTB, infoBox.getTimer());
}
-}
\ No newline at end of file
+
+ @Test
+ public void testImbuedHeartBoost()
+ {
+ when(timersConfig.showImbuedHeart()).thenReturn(true);
+ StatChanged event;
+
+ // The following simulates imbued heart boosts at low magic levels, but should not create an imbued heart timer
+ // because it is ambiguous what caused the boost. (Magic essences and potions can create similar boost amounts)
+ for (int level = 1; level < TimersPlugin.IMBUED_HEART_MIN_CERTAIN_BOOST_LEVEL; level++)
+ {
+ event = new StatChanged(Skill.MAGIC, 0, level, level + 1 + (level / 10));
+ timersPlugin.onStatChanged(event);
+ verify(infoBoxManager, never()).addInfoBox(any());
+ }
+
+ // The following simulates magic essence and magic potion boosts and should not create an imbued heart timer
+ for (int level = TimersPlugin.IMBUED_HEART_MIN_CERTAIN_BOOST_LEVEL; level <= Experience.MAX_REAL_LEVEL; level++)
+ {
+ event = new StatChanged(Skill.MAGIC, 0, level, level + 3); // Magic essence
+ timersPlugin.onStatChanged(event);
+ verify(infoBoxManager, never()).addInfoBox(any());
+
+ event = new StatChanged(Skill.MAGIC, 0, level, level + 4);
+ timersPlugin.onStatChanged(event);
+ verify(infoBoxManager, never()).addInfoBox(any());
+ }
+
+ // The following simulates a real imbued heart magic boost and should create imbued heart timers
+ for (int level = TimersPlugin.IMBUED_HEART_MIN_CERTAIN_BOOST_LEVEL, i = 0; level <= Experience.MAX_REAL_LEVEL; level++, i++)
+ {
+ event = new StatChanged(Skill.MAGIC, 0, level, level + 1 + (level / 10));
+ timersPlugin.onStatChanged(event);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(InfoBox.class);
+ verify(infoBoxManager, times(i + 1)).addInfoBox(captor.capture());
+ TimerTimer infoBox = (TimerTimer) captor.getValue();
+ assertEquals(GameTimer.IMBUEDHEART, infoBox.getTimer());
+ }
+ }
+}