From c92f83510297c475617d75ae430c9d7d114174c2 Mon Sep 17 00:00:00 2001 From: Jordan Atwood Date: Sun, 2 Feb 2020 22:24:21 -0800 Subject: [PATCH] timers plugin: Improve imbued heart detection As is the case with other graphics-based timers, the imbued heart timer will not fire if other graphics animations are triggered (such as those created when fighting the Dagannoth Kings). To add this timer more reliably, this commit will add the timer when a Magic stat boost occurs of the appropriate amount after recently clicking on an imbued heart. Because the magic level boost, combined with an imbued heart click check, is a reliable way to detect usage of an imbued heart, the graphics check is removed. --- .../main/java/net/runelite/api/GraphicID.java | 1 - .../client/plugins/timers/GameTimer.java | 2 +- .../client/plugins/timers/TimersPlugin.java | 62 ++++++++- .../plugins/timers/TimersPluginTest.java | 130 +++++++++++++++++- 4 files changed, 186 insertions(+), 9 deletions(-) diff --git a/runelite-api/src/main/java/net/runelite/api/GraphicID.java b/runelite-api/src/main/java/net/runelite/api/GraphicID.java index 669cb1e441..6f2db34f74 100644 --- a/runelite-api/src/main/java/net/runelite/api/GraphicID.java +++ b/runelite-api/src/main/java/net/runelite/api/GraphicID.java @@ -44,7 +44,6 @@ public class GraphicID public static final int BOOK_HOME_TELEPORT_3 = 803; public static final int BOOK_HOME_TELEPORT_4 = 804; public static final int STAFF_OF_THE_DEAD = 1228; - public static final int IMBUED_HEART = 1316; public static final int FLYING_FISH = 1387; public static final int NPC_CONTACT = 728; public static final int POT_SHARE = 733; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java index e4521feb9e..2008152b56 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java @@ -54,7 +54,7 @@ enum GameTimer ICEBURST(SpriteID.SPELL_ICE_BURST, GameTimerImageType.SPRITE, "Ice burst", GraphicID.ICE_BURST, 16, GAME_TICKS, true), ICEBLITZ(SpriteID.SPELL_ICE_BLITZ, GameTimerImageType.SPRITE, "Ice blitz", GraphicID.ICE_BLITZ, 24, GAME_TICKS, true), ICEBARRAGE(SpriteID.SPELL_ICE_BARRAGE, GameTimerImageType.SPRITE, "Ice barrage", GraphicID.ICE_BARRAGE, 32, GAME_TICKS, true), - IMBUEDHEART(ItemID.IMBUED_HEART, GameTimerImageType.ITEM, "Imbued heart", GraphicID.IMBUED_HEART, 420, ChronoUnit.SECONDS, true), + IMBUEDHEART(ItemID.IMBUED_HEART, GameTimerImageType.ITEM, "Imbued heart", 420, ChronoUnit.SECONDS, true), VENGEANCE(SpriteID.SPELL_VENGEANCE, GameTimerImageType.SPRITE, "Vengeance", 30, ChronoUnit.SECONDS), EXSUPERANTIFIRE(ItemID.EXTENDED_SUPER_ANTIFIRE4, GameTimerImageType.ITEM, "Extended Super AntiFire", 6, ChronoUnit.MINUTES), OVERLOAD_RAID(ItemID.OVERLOAD_4_20996, GameTimerImageType.ITEM, "Overload", 5, ChronoUnit.MINUTES, true), 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 341c1c35a0..3fafb14126 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 @@ -39,6 +39,7 @@ import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.Constants; import net.runelite.api.EquipmentInventorySlot; +import net.runelite.api.GameState; import net.runelite.api.InventoryID; import net.runelite.api.Item; import net.runelite.api.ItemContainer; @@ -48,11 +49,13 @@ import static net.runelite.api.ItemID.INFERNAL_CAPE; import net.runelite.api.NPC; import net.runelite.api.NpcID; import net.runelite.api.Player; +import net.runelite.api.Skill; import net.runelite.api.VarPlayer; import net.runelite.api.Varbits; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ActorDeath; import net.runelite.api.events.AnimationChanged; +import net.runelite.api.events.StatChanged; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; @@ -141,6 +144,8 @@ public class TimersPlugin extends Plugin private int lastAnimation; private boolean widgetHiddenChangedOnPvpWorld; private ElapsedTimer tzhaarTimer; + private int imbuedHeartClickTick = -1; + private int lastBoostedMagicLevel = -1; @Inject private ItemManager itemManager; @@ -163,6 +168,15 @@ public class TimersPlugin extends Plugin return configManager.getConfig(TimersConfig.class); } + @Override + public void startUp() + { + if (client.getGameState() == GameState.LOGGED_IN) + { + lastBoostedMagicLevel = client.getBoostedSkillLevel(Skill.MAGIC); + } + } + @Override protected void shutDown() throws Exception { @@ -176,6 +190,8 @@ public class TimersPlugin extends Plugin nextPoisonTick = 0; removeTzhaarTimer(); staminaTimer = null; + imbuedHeartClickTick = -1; + lastBoostedMagicLevel = -1; } @Subscribe @@ -435,6 +451,12 @@ public class TimersPlugin extends Plugin return; } + if (event.getMenuOption().contains("Invigorate") + && event.getId() == ItemID.IMBUED_HEART) + { + imbuedHeartClickTick = client.getTickCount(); + } + TeleportWidget teleportWidget = TeleportWidget.of(event.getWidgetId()); if (teleportWidget != null) { @@ -796,8 +818,10 @@ public class TimersPlugin extends Plugin config.tzhaarLastTime(null); } break; - case HOPPING: case LOGIN_SCREEN: + lastBoostedMagicLevel = -1; + // fall through + case HOPPING: // pause tzhaar timer if logged out without pausing if (config.tzhaarStartTime() != null && config.tzhaarLastTime() == null) { @@ -856,11 +880,6 @@ public class TimersPlugin extends Plugin return; } - if (config.showImbuedHeart() && actor.getGraphic() == IMBUEDHEART.getGraphicId()) - { - createGameTimer(IMBUEDHEART); - } - if (config.showFreezes()) { if (actor.getGraphic() == BIND.getGraphicId()) @@ -978,6 +997,37 @@ public class TimersPlugin extends Plugin } } + @Subscribe + public void onStatChanged(StatChanged statChanged) + { + if (statChanged.getSkill() != Skill.MAGIC) + { + return; + } + + final int boostedMagicLevel = statChanged.getBoostedLevel(); + + if (imbuedHeartClickTick < 0 + || client.getTickCount() > imbuedHeartClickTick + 3 // allow for 2 ticks of lag + || !config.showImbuedHeart()) + { + lastBoostedMagicLevel = boostedMagicLevel; + return; + } + + final int boostAmount = boostedMagicLevel - statChanged.getLevel(); + final int boostChange = boostedMagicLevel - lastBoostedMagicLevel; + final int heartBoost = 1 + (statChanged.getLevel() / 10); + + if ((boostAmount == heartBoost || (lastBoostedMagicLevel != -1 && boostChange == heartBoost)) + && boostChange > 0) + { + createGameTimer(IMBUEDHEART); + } + + lastBoostedMagicLevel = boostedMagicLevel; + } + private void createStaminaTimer() { Duration duration = Duration.ofMinutes(wasWearingEndurance ? 4 : 2); 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 ab7a254c6d..69e2b1eec1 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 @@ -32,10 +32,15 @@ import java.time.Duration; import java.time.Instant; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; +import net.runelite.api.Experience; import net.runelite.api.InventoryID; import net.runelite.api.ItemContainer; +import net.runelite.api.ItemID; +import net.runelite.api.Skill; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.ItemContainerChanged; +import net.runelite.api.events.MenuOptionClicked; +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; @@ -53,9 +58,11 @@ import static org.mockito.ArgumentMatchers.nullable; import org.mockito.Mock; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; @@ -369,4 +376,125 @@ public class TimersPluginTest ElapsedTimer timer = (ElapsedTimer) captor.getValue(); assertEquals("00:06", timer.getText()); } -} \ No newline at end of file + + @Test + public void testImbuedHeartBoost() + { + when(timersConfig.showImbuedHeart()).thenReturn(true); + when(client.getTickCount()).thenReturn(100); + StatChanged event; + + final MenuOptionClicked imbuedHeartClick = new MenuOptionClicked(); + imbuedHeartClick.setMenuOption("Invigorate"); + imbuedHeartClick.setId(ItemID.IMBUED_HEART); + timersPlugin.onMenuOptionClicked(imbuedHeartClick); + + when(client.getTickCount()).thenReturn(101); + + for (int level = 1, i = 0; level <= Experience.MAX_REAL_LEVEL; level++, i++) + { + event = new StatChanged(Skill.MAGIC, 0, level, heartBoostedLevel(level)); + 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()); + } + } + + @Test + public void testImbuedHeartBoostFromDrained() + { + when(timersConfig.showImbuedHeart()).thenReturn(true); + when(client.getTickCount()).thenReturn(100); + + final MenuOptionClicked imbuedHeartClick = new MenuOptionClicked(); + imbuedHeartClick.setMenuOption("Invigorate"); + imbuedHeartClick.setId(ItemID.IMBUED_HEART); + timersPlugin.onMenuOptionClicked(imbuedHeartClick); + + when(client.getTickCount()).thenReturn(101); + + for (int level = 1, i = 0; level <= Experience.MAX_REAL_LEVEL; level++, i++) + { + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, level - 1)); + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, heartBoostedLevel(level) - 1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InfoBox.class); + verify(infoBoxManager, times(i + 1)).addInfoBox(captor.capture()); + TimerTimer infoBox = (TimerTimer) captor.getValue(); + assertEquals(GameTimer.IMBUEDHEART, infoBox.getTimer()); + } + } + + @Test + public void testImbuedHeartBoostFromPartialBoost() + { + when(timersConfig.showImbuedHeart()).thenReturn(true); + when(client.getTickCount()).thenReturn(100); + + final MenuOptionClicked imbuedHeartClick = new MenuOptionClicked(); + imbuedHeartClick.setMenuOption("Invigorate"); + imbuedHeartClick.setId(ItemID.IMBUED_HEART); + timersPlugin.onMenuOptionClicked(imbuedHeartClick); + + when(client.getTickCount()).thenReturn(101); + + for (int level = 10, i = 0; level <= Experience.MAX_REAL_LEVEL; level++, i++) + { + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, level + 1)); + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, heartBoostedLevel(level))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InfoBox.class); + verify(infoBoxManager, times(i + 1)).addInfoBox(captor.capture()); + TimerTimer infoBox = (TimerTimer) captor.getValue(); + assertEquals(GameTimer.IMBUEDHEART, infoBox.getTimer()); + } + } + + @Test + public void testNonImbuedHeartBoost() + { + lenient().when(timersConfig.showImbuedHeart()).thenReturn(true); + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, 1, 1)); + + // Simulate stat changes of imbued heart boost amount without having clicked the imbued heart + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, 29, 34)); // equal to magic essence + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, 39, 43)); // equal to magic potion + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, 49, 54)); // equal to spicy stew + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, 99, 109)); + + verifyNoInteractions(infoBoxManager); + } + + @Test + public void testMagicLevelDrain() + { + lenient().when(timersConfig.showImbuedHeart()).thenReturn(true); + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, 1, 1)); + when(client.getTickCount()).thenReturn(100); + + final MenuOptionClicked imbuedHeartClick = new MenuOptionClicked(); + imbuedHeartClick.setMenuOption("Invigorate"); + imbuedHeartClick.setId(ItemID.IMBUED_HEART); + timersPlugin.onMenuOptionClicked(imbuedHeartClick); + + when(client.getTickCount()).thenReturn(101); + + // Simulate stat changes draining to the imbued heart boost amount + for (int level = 1; level <= Experience.MAX_REAL_LEVEL; level++) + { + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, level)); + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, heartBoostedLevel(level) + 1)); + timersPlugin.onStatChanged(new StatChanged(Skill.MAGIC, 0, level, heartBoostedLevel(level))); + } + + verifyNoInteractions(infoBoxManager); + } + + private static int heartBoostedLevel(final int level) + { + return level + 1 + (level / 10); + } +}