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