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 98701e880c..bc751e4178 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 @@ -40,7 +40,7 @@ import static net.runelite.client.util.RSTimeUnit.GAME_TICKS; @Getter(AccessLevel.PACKAGE) enum GameTimer { - STAMINA(ItemID.STAMINA_POTION4, GameTimerImageType.ITEM, "Stamina", 2, ChronoUnit.MINUTES, true), + STAMINA(ItemID.STAMINA_POTION4, GameTimerImageType.ITEM, "Stamina", true), ANTIFIRE(ItemID.ANTIFIRE_POTION4, GameTimerImageType.ITEM, "Antifire", 6, ChronoUnit.MINUTES), EXANTIFIRE(ItemID.EXTENDED_ANTIFIRE4, GameTimerImageType.ITEM, "Extended antifire", 12, ChronoUnit.MINUTES), OVERLOAD(ItemID.OVERLOAD_4, GameTimerImageType.ITEM, "Overload", 5, ChronoUnit.MINUTES, true), @@ -78,8 +78,8 @@ enum GameTimer DIVINE_MAGIC(ItemID.DIVINE_MAGIC_POTION4, GameTimerImageType.ITEM, "Divine Magic", 5, ChronoUnit.MINUTES), DIVINE_BASTION(ItemID.DIVINE_BASTION_POTION4, GameTimerImageType.ITEM, "Divine Bastion", 5, ChronoUnit.MINUTES), DIVINE_BATTLEMAGE(ItemID.DIVINE_BATTLEMAGE_POTION4, GameTimerImageType.ITEM, "Divine Battlemage", 5, ChronoUnit.MINUTES), - ANTIPOISON(ItemID.ANTIPOISON4, GameTimerImageType.ITEM, "Antipoison"), - ANTIVENOM(ItemID.ANTIVENOM4, GameTimerImageType.ITEM, "Anti-venom"); + ANTIPOISON(ItemID.ANTIPOISON4, GameTimerImageType.ITEM, "Antipoison", false), + ANTIVENOM(ItemID.ANTIVENOM4, GameTimerImageType.ITEM, "Anti-venom", false); @Nullable private final Duration duration; @@ -110,12 +110,12 @@ enum GameTimer this(imageId, idType, description, null, time, unit, false); } - GameTimer(int imageId, GameTimerImageType idType, String description) + GameTimer(int imageId, GameTimerImageType idType, String description, boolean removedOnDeath) { this.duration = null; this.graphicId = null; this.description = description; - this.removedOnDeath = false; + this.removedOnDeath = removedOnDeath; this.imageId = imageId; this.imageType = idType; } 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 6376b329ed..24f9d5297c 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 @@ -27,6 +27,7 @@ package net.runelite.client.plugins.timers; import com.google.inject.Provides; import java.time.Duration; +import java.time.Instant; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; @@ -107,6 +108,7 @@ public class TimersPlugin extends Plugin private static final String SUPER_ANTIFIRE_EXPIRED_MESSAGE = "Your super antifire potion has expired."; private static final String KILLED_TELEBLOCK_OPPONENT_TEXT = "Your Tele Block has been removed because you killed "; private static final String PRAYER_ENHANCE_EXPIRED = "Your prayer enhance effect has worn off."; + private static final String ENDURANCE_EFFECT_MESSAGE = "Your Ring of endurance doubles the duration of your stamina potion's effect."; private static final Pattern DEADMAN_HALF_TELEBLOCK_PATTERN = Pattern.compile("A Tele Block spell has been cast on you by (.+)\\. It will expire in 1 minute, 15 seconds\\."); private static final Pattern FULL_TELEBLOCK_PATTERN = Pattern.compile("A Tele Block spell has been cast on you by (.+)\\. It will expire in 5 minutes\\."); @@ -118,6 +120,9 @@ public class TimersPlugin extends Plugin private TimerTimer freezeTimer; private int freezeTime = -1; // time frozen, in game ticks + private TimerTimer staminaTimer; + private boolean wasWearingEndurance; + private int lastRaidVarb; private int lastWildernessVarb; private int lastVengCooldownVarb; @@ -163,6 +168,7 @@ public class TimersPlugin extends Plugin widgetHiddenChangedOnPvpWorld = false; lastPoisonVarp = 0; nextPoisonTick = 0; + staminaTimer = null; } @Subscribe @@ -379,7 +385,7 @@ public class TimersPlugin extends Plugin || event.getId() == ItemID.EGNIOL_POTION_4)) { // Needs menu option hook because mixes use a common drink message, distinct from their standard potion messages - createGameTimer(STAMINA); + createStaminaTimer(); return; } @@ -438,14 +444,20 @@ public class TimersPlugin extends Plugin return; } - if (config.showStamina() && (event.getMessage().equals(STAMINA_DRINK_MESSAGE) || event.getMessage().equals(STAMINA_SHARED_DRINK_MESSAGE))) + if (event.getMessage().equals(ENDURANCE_EFFECT_MESSAGE)) { - createGameTimer(STAMINA); + wasWearingEndurance = true; } - if (config.showStamina() && (event.getMessage().equals(STAMINA_EXPIRED_MESSAGE) || event.getMessage().equals(GAUNTLET_ENTER_MESSAGE))) + if (config.showStamina() && (event.getMessage().equals(STAMINA_DRINK_MESSAGE) || event.getMessage().equals(STAMINA_SHARED_DRINK_MESSAGE))) + { + createStaminaTimer(); + } + + if (event.getMessage().equals(STAMINA_EXPIRED_MESSAGE) || event.getMessage().equals(GAUNTLET_ENTER_MESSAGE)) { removeGameTimer(STAMINA); + staminaTimer = null; } if (config.showAntiFire() && event.getMessage().equals(ANTIFIRE_DRINK_MESSAGE)) @@ -797,34 +809,50 @@ public class TimersPlugin extends Plugin } /** - * remove SOTD timer when weapon is changed - * - * @param itemContainerChanged + * Remove SOTD timer and update stamina timer when equipment is changed. */ @Subscribe public void onItemContainerChanged(ItemContainerChanged itemContainerChanged) { - ItemContainer container = itemContainerChanged.getItemContainer(); - if (container == client.getItemContainer(InventoryID.EQUIPMENT)) + if (itemContainerChanged.getContainerId() != InventoryID.EQUIPMENT.getId()) { - Item weapon = container.getItem(EquipmentInventorySlot.WEAPON.getSlotIdx()); + return; + } - if (weapon == null) - { - removeGameTimer(STAFF_OF_THE_DEAD); - return; - } + ItemContainer container = itemContainerChanged.getItemContainer(); - switch (weapon.getId()) + Item weapon = container.getItem(EquipmentInventorySlot.WEAPON.getSlotIdx()); + if (weapon == null || + (weapon.getId() != ItemID.STAFF_OF_THE_DEAD && + weapon.getId() != ItemID.TOXIC_STAFF_OF_THE_DEAD && + weapon.getId() != ItemID.STAFF_OF_LIGHT && + weapon.getId() != ItemID.TOXIC_STAFF_UNCHARGED)) + { + // remove sotd timer if the staff has been unwielded + removeGameTimer(STAFF_OF_THE_DEAD); + } + + if (wasWearingEndurance) + { + Item ring = container.getItem(EquipmentInventorySlot.RING.getSlotIdx()); + + // when using the last ring charge the ring changes to the uncharged version, ignore that and don't + // halve the timer + if (ring == null || (ring.getId() != ItemID.RING_OF_ENDURANCE && ring.getId() != ItemID.RING_OF_ENDURANCE_UNCHARGED_24844)) { - case ItemID.STAFF_OF_THE_DEAD: - case ItemID.TOXIC_STAFF_OF_THE_DEAD: - case ItemID.STAFF_OF_LIGHT: - case ItemID.TOXIC_STAFF_UNCHARGED: - // don't reset timer if still wielding staff - return; - default: - removeGameTimer(STAFF_OF_THE_DEAD); + wasWearingEndurance = false; + if (staminaTimer != null) + { + // Remaining duration gets divided by 2 + Duration remainingDuration = Duration.between(Instant.now(), staminaTimer.getEndTime()).dividedBy(2); + // This relies on the chat message to be removed, which could be after the timer has been culled; + // so check there is still remaining time + if (!remainingDuration.isNegative() && !remainingDuration.isZero()) + { + log.debug("Halving stamina timer"); + staminaTimer.setDuration(remainingDuration); + } + } } } } @@ -856,6 +884,12 @@ public class TimersPlugin extends Plugin } } + private void createStaminaTimer() + { + Duration duration = Duration.ofMinutes(wasWearingEndurance ? 4 : 2); + staminaTimer = createGameTimer(STAMINA, duration); + } + private TimerTimer createGameTimer(final GameTimer timer) { if (timer.getDuration() == null) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/Timer.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/Timer.java index 4d6c2b7be4..c9a3664222 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/Timer.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/Timer.java @@ -39,8 +39,8 @@ import net.runelite.client.plugins.Plugin; public class Timer extends InfoBox { private final Instant startTime; - private final Instant endTime; - private final Duration duration; + private Instant endTime; + private Duration duration; public Timer(long period, ChronoUnit unit, BufferedImage image, Plugin plugin) { @@ -94,4 +94,9 @@ public class Timer extends InfoBox return timeLeft.isZero() || timeLeft.isNegative(); } + public void setDuration(Duration duration) + { + this.duration = duration; + endTime = startTime.plus(duration); + } } 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 ea51b20747..9ef79a29a3 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 @@ -28,16 +28,21 @@ import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.time.Duration; import java.util.EnumSet; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; +import net.runelite.api.InventoryID; +import net.runelite.api.ItemContainer; import net.runelite.api.WorldType; import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.ItemContainerChanged; import net.runelite.client.game.ItemManager; import net.runelite.client.game.SpriteManager; import net.runelite.client.ui.overlay.infobox.InfoBox; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,6 +50,7 @@ import org.mockito.ArgumentCaptor; import static org.mockito.ArgumentMatchers.any; import org.mockito.Mock; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.mockito.junit.MockitoJUnitRunner; @@ -189,4 +195,42 @@ public class TimersPluginTest verify(infoBoxManager, atLeastOnce()).removeIf(any()); } + + @Test + public void testStamina() + { + when(timersConfig.showStamina()).thenReturn(true); + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You drink some of your stamina potion.", "", 0); + timersPlugin.onChatMessage(chatMessage); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InfoBox.class); + verify(infoBoxManager).addInfoBox(captor.capture()); + TimerTimer infoBox = (TimerTimer) captor.getValue(); + assertEquals(GameTimer.STAMINA, infoBox.getTimer()); + assertEquals(Duration.ofMinutes(2), infoBox.getDuration()); + } + + @Test + public void testEndurance() + { + when(timersConfig.showStamina()).thenReturn(true); + + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "Your Ring of endurance doubles the duration of your stamina potion's effect.", "", 0); + timersPlugin.onChatMessage(chatMessage); + + chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You drink some of your stamina potion.", "", 0); + timersPlugin.onChatMessage(chatMessage); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InfoBox.class); + verify(infoBoxManager).addInfoBox(captor.capture()); + TimerTimer infoBox = (TimerTimer) captor.getValue(); + assertEquals(GameTimer.STAMINA, infoBox.getTimer()); + assertEquals(Duration.ofMinutes(4), infoBox.getDuration()); + + // unwield ring + timersPlugin.onItemContainerChanged(new ItemContainerChanged(InventoryID.EQUIPMENT.getId(), mock(ItemContainer.class))); + // some time has elapsed in the test; this should be just under 2 mins + int mins = (int) infoBox.getDuration().toMinutes(); + assertTrue(mins == 1 || mins == 2); + } } \ No newline at end of file