diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java index 382a17b3e1..012c168534 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostIndicator.java @@ -29,21 +29,22 @@ import java.awt.image.BufferedImage; import lombok.Getter; import net.runelite.api.Client; import net.runelite.api.Skill; -import net.runelite.client.plugins.Plugin; import net.runelite.client.ui.overlay.infobox.InfoBox; import net.runelite.client.ui.overlay.infobox.InfoBoxPriority; public class BoostIndicator extends InfoBox { + private final BoostsPlugin plugin; private final BoostsConfig config; private final Client client; @Getter private final Skill skill; - public BoostIndicator(Skill skill, BufferedImage image, Plugin plugin, Client client, BoostsConfig config) + BoostIndicator(Skill skill, BufferedImage image, BoostsPlugin plugin, Client client, BoostsConfig config) { super(image, plugin); + this.plugin = plugin; this.config = config; this.client = client; this.skill = skill; @@ -82,4 +83,15 @@ public class BoostIndicator extends InfoBox return new Color(238, 51, 51); } + + @Override + public boolean render() + { + if (config.displayIndicators() && plugin.canShowBoosts() && plugin.getShownSkills().contains(getSkill())) + { + return client.getBoostedSkillLevel(skill) != client.getRealSkillLevel(skill); + } + + return false; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java index 3e2a04edc9..14cc80b003 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsConfig.java @@ -31,6 +31,13 @@ import net.runelite.client.config.ConfigItem; @ConfigGroup("boosts") public interface BoostsConfig extends Config { + enum DisplayChangeMode + { + ALWAYS, + BOOSTED, + NEVER + } + @ConfigItem( keyName = "enableSkill", name = "Enable Skill Boosts", @@ -65,21 +72,32 @@ public interface BoostsConfig extends Config } @ConfigItem( - keyName = "displayNextChange", - name = "Display next change", - description = "Configures whether or not to display when the next stat change will be", + keyName = "displayNextBuffChange", + name = "Display next buff change", + description = "Configures whether or not to display when the next buffed stat change will be", position = 4 ) - default boolean displayNextChange() + default DisplayChangeMode displayNextBuffChange() { - return true; + return DisplayChangeMode.BOOSTED; + } + + @ConfigItem( + keyName = "displayNextDebuffChange", + name = "Display next debuff change", + description = "Configures whether or not to display when the next debuffed stat change will be", + position = 5 + ) + default DisplayChangeMode displayNextDebuffChange() + { + return DisplayChangeMode.NEVER; } @ConfigItem( keyName = "boostThreshold", name = "Boost Amount Threshold", description = "The amount of levels boosted to send a notification at. A value of 0 will disable notification.", - position = 5 + position = 6 ) default int boostThreshold() { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java index 364d2e97b3..44f6d2cd31 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsOverlay.java @@ -27,117 +27,79 @@ package net.runelite.client.plugins.boosts; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; -import java.time.Instant; import javax.inject.Inject; -import lombok.Getter; import net.runelite.api.Client; import net.runelite.api.Skill; -import net.runelite.client.game.SkillIconManager; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayPriority; import net.runelite.client.ui.overlay.components.LineComponent; import net.runelite.client.ui.overlay.components.PanelComponent; -import net.runelite.client.ui.overlay.infobox.InfoBoxManager; class BoostsOverlay extends Overlay { - @Getter - private final BoostIndicator[] indicators = new BoostIndicator[Skill.values().length - 1]; - private final Client client; private final BoostsConfig config; - private final InfoBoxManager infoBoxManager; private final PanelComponent panelComponent = new PanelComponent(); + private final BoostsPlugin plugin; @Inject - private BoostsPlugin plugin; - - @Inject - private SkillIconManager iconManager; - - private boolean overlayActive; - - @Inject - BoostsOverlay(Client client, BoostsConfig config, InfoBoxManager infoBoxManager) + private BoostsOverlay(Client client, BoostsConfig config, BoostsPlugin plugin) { - setPosition(OverlayPosition.TOP_LEFT); - setPriority(OverlayPriority.MED); + this.plugin = plugin; this.client = client; this.config = config; - this.infoBoxManager = infoBoxManager; + setPosition(OverlayPosition.TOP_LEFT); + setPriority(OverlayPriority.MED); } @Override public Dimension render(Graphics2D graphics) { - Instant lastChange = plugin.getLastChange(); - panelComponent.getChildren().clear(); - - if (!config.displayIndicators() - && config.displayNextChange() - && lastChange != null - && overlayActive) + if (config.displayIndicators()) { - int nextChange = plugin.getChangeTime(); - if (nextChange > 0) - { - panelComponent.getChildren().add(LineComponent.builder() - .left("Next change in") - .right(String.valueOf(nextChange)) - .build()); - } + return null; } - overlayActive = false; + panelComponent.getChildren().clear(); - for (Skill skill : plugin.getShownSkills()) + int nextChange = plugin.getChangeDownTicks(); + + if (nextChange != -1) { - int boosted = client.getBoostedSkillLevel(skill), - base = client.getRealSkillLevel(skill); + panelComponent.getChildren().add(LineComponent.builder() + .left("Next + restore in") + .right(String.valueOf(plugin.getChangeTime(nextChange))) + .build()); + } - BoostIndicator indicator = indicators[skill.ordinal()]; + nextChange = plugin.getChangeUpTicks(); - if (boosted == base) + if (nextChange != -1) + { + panelComponent.getChildren().add(LineComponent.builder() + .left("Next - restore in") + .right(String.valueOf(plugin.getChangeTime(nextChange))) + .build()); + } + + if (plugin.canShowBoosts()) + { + for (Skill skill : plugin.getShownSkills()) { - if (indicator != null && infoBoxManager.getInfoBoxes().contains(indicator)) + final int boosted = client.getBoostedSkillLevel(skill); + final int base = client.getRealSkillLevel(skill); + + if (boosted == base) { - infoBoxManager.removeInfoBox(indicator); - } - - continue; - } - - overlayActive = true; - - if (config.displayIndicators()) - { - if (indicator == null) - { - indicator = new BoostIndicator(skill, iconManager.getSkillImage(skill), plugin, client, config); - indicators[skill.ordinal()] = indicator; - } - - if (!infoBoxManager.getInfoBoxes().contains(indicator)) - { - infoBoxManager.addInfoBox(indicator); - } - } - else - { - if (indicator != null && infoBoxManager.getInfoBoxes().contains(indicator)) - { - infoBoxManager.removeInfoBox(indicator); + continue; } + final int boost = boosted - base; + final Color strColor = getTextColor(boost); String str; - int boost = boosted - base; - Color strColor = getTextColor(boost); - if (!config.useRelativeBoost()) - { - str = "" + boosted + "/" + base; - } - else + + if (config.useRelativeBoost()) { str = String.valueOf(boost); if (boost > 0) @@ -145,6 +107,10 @@ class BoostsOverlay extends Overlay str = "+" + str; } } + else + { + str = "" + boosted + "/" + base; + } panelComponent.getChildren().add(LineComponent.builder() .left(skill.getName()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java index 0efb989ac0..623d40a3da 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/BoostsPlugin.java @@ -24,15 +24,15 @@ */ package net.runelite.client.plugins.boosts; -import com.google.common.collect.ObjectArrays; +import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; -import java.awt.image.BufferedImage; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.imageio.ImageIO; import javax.inject.Inject; +import javax.inject.Singleton; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; @@ -40,6 +40,8 @@ import net.runelite.api.Prayer; import net.runelite.api.Skill; import net.runelite.api.events.BoostedLevelChanged; import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; import net.runelite.client.Notifier; import net.runelite.client.config.ConfigManager; import net.runelite.client.game.SkillIconManager; @@ -54,23 +56,20 @@ import net.runelite.client.ui.overlay.infobox.InfoBoxManager; tags = {"combat", "notifications", "skilling", "overlay"} ) @Slf4j +@Singleton public class BoostsPlugin extends Plugin { - private static final Skill[] COMBAT = new Skill[] - { - Skill.ATTACK, Skill.STRENGTH, Skill.DEFENCE, Skill.RANGED, Skill.MAGIC - }; - private static final Skill[] SKILLING = new Skill[] - { + private static final Set BOOSTABLE_COMBAT_SKILLS = ImmutableSet.of( + Skill.ATTACK, + Skill.STRENGTH, + Skill.DEFENCE, + Skill.RANGED, + Skill.MAGIC); + + private static final Set BOOSTABLE_NON_COMBAT_SKILLS = ImmutableSet.of( Skill.MINING, Skill.AGILITY, Skill.SMITHING, Skill.HERBLORE, Skill.FISHING, Skill.THIEVING, Skill.COOKING, Skill.CRAFTING, Skill.FIREMAKING, Skill.FLETCHING, Skill.WOODCUTTING, Skill.RUNECRAFT, - Skill.SLAYER, Skill.FARMING, Skill.CONSTRUCTION, Skill.HUNTER - }; - - private final int[] lastSkillLevels = new int[Skill.values().length - 1]; - - @Getter - private Instant lastChange; + Skill.SLAYER, Skill.FARMING, Skill.CONSTRUCTION, Skill.HUNTER); @Inject private Notifier notifier; @@ -94,13 +93,15 @@ public class BoostsPlugin extends Plugin private SkillIconManager skillIconManager; @Getter - private Skill[] shownSkills; - - private StatChangeIndicator statChangeIndicator; - - private BufferedImage overallIcon; + private final Set shownSkills = new HashSet<>(); + private boolean isChangedDown = false; + private boolean isChangedUp = false; + private final int[] lastSkillLevels = new int[Skill.values().length - 1]; + private int lastChangeDown = -1; + private int lastChangeUp = -1; private boolean preserveBeenActive = false; + private long lastTickMillis; @Provides BoostsConfig provideConfig(ConfigManager configManager) @@ -109,12 +110,27 @@ public class BoostsPlugin extends Plugin } @Override - protected void startUp() + protected void startUp() throws Exception { overlayManager.add(boostsOverlay); - updateShownSkills(config.enableSkill()); + updateShownSkills(); + updateBoostedStats(); Arrays.fill(lastSkillLevels, -1); - overallIcon = skillIconManager.getSkillImage(Skill.OVERALL); + + // Add infoboxes for everything at startup and then determine inside if it will be rendered + synchronized (ImageIO.class) + { + infoBoxManager.addInfoBox(new StatChangeIndicator(true, ImageIO.read(getClass().getResourceAsStream("debuffed.png")), this, config)); + infoBoxManager.addInfoBox(new StatChangeIndicator(false, ImageIO.read(getClass().getResourceAsStream("buffed.png")), this, config)); + } + + for (final Skill skill : Skill.values()) + { + if (skill != Skill.OVERALL) + { + infoBoxManager.addInfoBox(new BoostIndicator(skill, skillIconManager.getSkillImage(skill), this, client, config)); + } + } } @Override @@ -122,6 +138,24 @@ public class BoostsPlugin extends Plugin { overlayManager.remove(boostsOverlay); infoBoxManager.removeIf(t -> t instanceof BoostIndicator || t instanceof StatChangeIndicator); + preserveBeenActive = false; + lastChangeDown = -1; + lastChangeUp = -1; + isChangedUp = false; + isChangedDown = false; + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOGIN_SCREEN: + case HOPPING: + // After world hop and log out timers are in undefined state so just reset + lastChangeDown = -1; + lastChangeUp = -1; + } } @Subscribe @@ -132,29 +166,25 @@ public class BoostsPlugin extends Plugin return; } - if (event.getKey().equals("displayIndicators") || event.getKey().equals("displayNextChange")) + updateShownSkills(); + + if (config.displayNextBuffChange() == BoostsConfig.DisplayChangeMode.NEVER) { - addStatChangeIndicator(); - return; + lastChangeDown = -1; } - Skill[] old = shownSkills; - updateShownSkills(config.enableSkill()); - - if (!Arrays.equals(old, shownSkills)) + if (config.displayNextDebuffChange() == BoostsConfig.DisplayChangeMode.NEVER) { - infoBoxManager.removeIf(t -> t instanceof BoostIndicator - && !Arrays.asList(shownSkills).contains(((BoostIndicator) t).getSkill())); + lastChangeUp = -1; } } @Subscribe - void onBoostedLevelChange(BoostedLevelChanged boostedLevelChanged) + public void onBoostedLevelChange(BoostedLevelChanged boostedLevelChanged) { Skill skill = boostedLevelChanged.getSkill(); - // Ignore changes to hitpoints or prayer - if (skill == Skill.HITPOINTS || skill == Skill.PRAYER) + if (!BOOSTABLE_COMBAT_SKILLS.contains(skill) && !BOOSTABLE_NON_COMBAT_SKILLS.contains(skill)) { return; } @@ -163,16 +193,23 @@ public class BoostsPlugin extends Plugin int last = lastSkillLevels[skillIdx]; int cur = client.getBoostedSkillLevel(skill); - // Check if stat goes +1 or -1 - if (cur == last + 1 || cur == last - 1) + if (cur == last - 1) { - log.debug("Skill {} healed", skill); - lastChange = Instant.now(); - addStatChangeIndicator(); + // Stat was restored down (from buff) + lastChangeDown = client.getTickCount(); } + + if (cur == last + 1) + { + // Stat was restored up (from debuff) + lastChangeUp = client.getTickCount(); + } + lastSkillLevels[skillIdx] = cur; + updateBoostedStats(); int boostThreshold = config.boostThreshold(); + if (boostThreshold != 0) { int real = client.getRealSkillLevel(skill); @@ -185,30 +222,93 @@ public class BoostsPlugin extends Plugin } } - private void updateShownSkills(boolean showSkillingSkills) + @Subscribe + public void onGameTick(GameTick event) { - if (showSkillingSkills) + lastTickMillis = System.currentTimeMillis(); + + if (getChangeUpTicks() <= 0) { - shownSkills = ObjectArrays.concat(COMBAT, SKILLING, Skill.class); + switch (config.displayNextDebuffChange()) + { + case ALWAYS: + if (lastChangeUp != -1) + { + lastChangeUp = client.getTickCount(); + } + + break; + case BOOSTED: + case NEVER: + lastChangeUp = -1; + break; + } } - else + + if (getChangeDownTicks() <= 0) { - shownSkills = COMBAT; + switch (config.displayNextBuffChange()) + { + case ALWAYS: + if (lastChangeDown != -1) + { + lastChangeDown = client.getTickCount(); + } + + break; + case BOOSTED: + case NEVER: + lastChangeDown = -1; + break; + } } } - public void addStatChangeIndicator() + private void updateShownSkills() { - infoBoxManager.removeInfoBox(statChangeIndicator); - statChangeIndicator = null; - - if (lastChange != null - && config.displayIndicators() - && config.displayNextChange()) + if (config.enableSkill()) { - statChangeIndicator = new StatChangeIndicator(getChangeTime(), ChronoUnit.SECONDS, overallIcon, this); - infoBoxManager.addInfoBox(statChangeIndicator); + shownSkills.addAll(BOOSTABLE_NON_COMBAT_SKILLS); } + else + { + shownSkills.removeAll(BOOSTABLE_NON_COMBAT_SKILLS); + } + + shownSkills.addAll(BOOSTABLE_COMBAT_SKILLS); + } + + private void updateBoostedStats() + { + // Reset is boosted + isChangedDown = false; + isChangedUp = false; + + // Check if we are still boosted + for (final Skill skill : Skill.values()) + { + if (!BOOSTABLE_COMBAT_SKILLS.contains(skill) && !BOOSTABLE_NON_COMBAT_SKILLS.contains(skill)) + { + continue; + } + + final int boosted = client.getBoostedSkillLevel(skill); + final int base = client.getRealSkillLevel(skill); + + if (boosted > base) + { + isChangedUp = true; + } + else if (boosted < base) + { + isChangedDown = true; + } + } + } + + boolean canShowBoosts() + { + return isChangedDown || isChangedUp; } /** @@ -224,26 +324,54 @@ public class BoostsPlugin extends Plugin * Preserve is only required to be on for the 4th and 5th sections of the boost timer * to gain full effect (seconds 45-75). * - * @return integer value in seconds until next boost change + * @return integer value in ticks until next boost change */ - public int getChangeTime() + int getChangeDownTicks() { - int timeSinceChange = timeSinceLastChange(); + if (lastChangeDown == -1 || (config.displayNextBuffChange() == BoostsConfig.DisplayChangeMode.BOOSTED && !isChangedUp)) + { + return -1; + } + + int ticksSinceChange = client.getTickCount() - lastChangeDown; boolean isPreserveActive = client.isPrayerActive(Prayer.PRESERVE); - if ((isPreserveActive && (timeSinceChange < 45 || preserveBeenActive)) || timeSinceChange > 75) + if ((isPreserveActive && (ticksSinceChange < 75 || preserveBeenActive)) || ticksSinceChange > 125) { preserveBeenActive = true; - return 90 - timeSinceChange; + return 150 - ticksSinceChange; } preserveBeenActive = false; - return (timeSinceChange > 60) ? 75 - timeSinceChange : 60 - timeSinceChange; + return (ticksSinceChange > 100) ? 125 - ticksSinceChange : 100 - ticksSinceChange; } - private int timeSinceLastChange() + /** + * Restoration from debuff is separate timer as restoration from buff because of preserve messing up the buff timer. + * Restoration timer is always in 100 tick cycles. + * + * @return integer value in ticks until next stat restoration up + */ + int getChangeUpTicks() { - return (int) Duration.between(lastChange, Instant.now()).getSeconds(); + if (lastChangeUp == -1 || (config.displayNextDebuffChange() == BoostsConfig.DisplayChangeMode.BOOSTED && !isChangedDown)) + { + return -1; + } + + int ticksSinceChange = client.getTickCount() - lastChangeUp; + return 100 - ticksSinceChange; } -} + + /** + * Converts tick-based time to accurate second time + * @param time tick-based time + * @return second-based time + */ + int getChangeTime(final int time) + { + final long diff = System.currentTimeMillis() - lastTickMillis; + return time != -1 ? (int)(time * 0.6 - (diff / 1000d)) : time; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java index 78275e4027..4d6d1952ab 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/boosts/StatChangeIndicator.java @@ -24,18 +24,43 @@ */ package net.runelite.client.plugins.boosts; +import java.awt.Color; import java.awt.image.BufferedImage; -import java.time.temporal.ChronoUnit; -import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.infobox.InfoBox; import net.runelite.client.ui.overlay.infobox.InfoBoxPriority; -import net.runelite.client.ui.overlay.infobox.Timer; -public class StatChangeIndicator extends Timer +public class StatChangeIndicator extends InfoBox { - public StatChangeIndicator(long period, ChronoUnit unit, BufferedImage image, Plugin plugin) + private final boolean up; + private final BoostsPlugin plugin; + private final BoostsConfig config; + + StatChangeIndicator(boolean up, BufferedImage image, BoostsPlugin plugin, BoostsConfig config) { - super(period, unit, image, plugin); + super(image, plugin); + this.up = up; + this.plugin = plugin; + this.config = config; setPriority(InfoBoxPriority.MED); - setTooltip("Next stat change"); + setTooltip(up ? "Next debuff change" : "Next buff change"); + } + + @Override + public String getText() + { + return String.format("%02d", plugin.getChangeTime(up ? plugin.getChangeUpTicks() : plugin.getChangeDownTicks())); + } + + @Override + public Color getTextColor() + { + return (up ? plugin.getChangeUpTicks() : plugin.getChangeDownTicks()) < 10 ? Color.RED.brighter() : Color.WHITE; + } + + @Override + public boolean render() + { + final int time = up ? plugin.getChangeUpTicks() : plugin.getChangeDownTicks(); + return config.displayIndicators() && time != -1; } } diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/InfoBoxOverlay.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/InfoBoxOverlay.java index 25e42b03fa..d3aa9923e7 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/InfoBoxOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/infobox/InfoBoxOverlay.java @@ -89,15 +89,20 @@ public class InfoBoxOverlay extends Overlay : PanelComponent.Orientation.HORIZONTAL); panelComponent.setPreferredSize(new Dimension(config.infoBoxSize(), config.infoBoxSize())); - infoBoxes.forEach(box -> + for (InfoBox box : infoBoxes) { + if (!box.render()) + { + continue; + } + final InfoBoxComponent infoBoxComponent = new InfoBoxComponent(); infoBoxComponent.setColor(box.getTextColor()); infoBoxComponent.setImage(box.getScaledImage()); infoBoxComponent.setText(box.getText()); infoBoxComponent.setTooltip(box.getTooltip()); panelComponent.getChildren().add(infoBoxComponent); - }); + } final Dimension dimension = panelComponent.render(graphics); final Client client = clientProvider.get(); diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/boosts/buffed.png b/runelite-client/src/main/resources/net/runelite/client/plugins/boosts/buffed.png new file mode 100644 index 0000000000..c8ab82e845 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/boosts/buffed.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/boosts/debuffed.png b/runelite-client/src/main/resources/net/runelite/client/plugins/boosts/debuffed.png new file mode 100644 index 0000000000..fb334fd743 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/boosts/debuffed.png differ