From fcc9bb7268020389f832d36c1067a830b933c3aa Mon Sep 17 00:00:00 2001 From: SoapTurtle Date: Sat, 28 Sep 2024 15:51:09 +0200 Subject: [PATCH] Add goal tracking infobox This adds a config section where you can choose an item to track your progress towards and shows an infobox with overall progress and component progress --- .../masteringmixology/GoalInfoBoxOverlay.java | 200 ++++++++++++++++++ .../MasteringMixologyConfig.java | 29 +++ .../MasteringMixologyPlugin.java | 36 +++- .../masteringmixology/PotionComponent.java | 14 +- .../fking/masteringmixology/RewardItem.java | 52 +++++ 5 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 src/main/java/work/fking/masteringmixology/GoalInfoBoxOverlay.java create mode 100644 src/main/java/work/fking/masteringmixology/RewardItem.java diff --git a/src/main/java/work/fking/masteringmixology/GoalInfoBoxOverlay.java b/src/main/java/work/fking/masteringmixology/GoalInfoBoxOverlay.java new file mode 100644 index 0000000..b473bc6 --- /dev/null +++ b/src/main/java/work/fking/masteringmixology/GoalInfoBoxOverlay.java @@ -0,0 +1,200 @@ +package work.fking.masteringmixology; + +import net.runelite.api.Client; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.components.ComponentOrientation; +import net.runelite.client.ui.overlay.components.ImageComponent; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.components.ProgressBarComponent; +import net.runelite.client.ui.overlay.components.SplitComponent; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.QuantityFormatter; + +import javax.inject.Inject; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +class GoalInfoBoxOverlay extends OverlayPanel { + private static final int BORDER_SIZE = 3; + private static final int VERTICAL_GAP = 2; + private static final int ICON_AND_GOAL_GAP = 5; + private static final Rectangle TOP_PANEL_BORDER = new Rectangle(2, 0, 4, 4); + private static final int COMPONENT_SPRITE_SIZE = 16; + + private final Client client; + private final MasteringMixologyPlugin plugin; + private final MasteringMixologyConfig config; + private final ItemManager itemManager; + private final SpriteManager spriteManager; + + private final PanelComponent topPanel = new PanelComponent(); + + private final EnumMap componentDataMap = new EnumMap<>(PotionComponent.class); + private double overallProgress = 0.0; + private RewardItem rewardItem; + + private final Map rewardIconCache = new HashMap<>(); + private final EnumMap componentSpriteCache = new EnumMap<>(PotionComponent.class); + + private boolean dataDirty = true; + + @Inject + GoalInfoBoxOverlay(Client client, MasteringMixologyPlugin plugin, MasteringMixologyConfig config, + ItemManager itemManager, SpriteManager spriteManager) { + super(plugin); + this.client = client; + this.plugin = plugin; + this.config = config; + this.itemManager = itemManager; + this.spriteManager = spriteManager; + + panelComponent.setBorder(new Rectangle(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE)); + panelComponent.setGap(new Point(0, VERTICAL_GAP)); + topPanel.setBorder(TOP_PANEL_BORDER); + topPanel.setBackgroundColor(null); + } + + @Override + public Dimension render(Graphics2D graphics) { + // Recalculate the data before checking if the overlay should be displayed + if (dataDirty) { + recalculateData(); + } + + if (rewardItem == RewardItem.NONE || !plugin.isInLabRegion()) { + return null; + } + + // Clear the panel components + topPanel.getChildren().clear(); + + // Set the font for the panel + graphics.setFont(FontManager.getRunescapeSmallFont()); + + // Build the top display with the reward item and overall progress + final LineComponent topLine = LineComponent.builder() + .left(rewardItem.itemName()) + .leftFont(FontManager.getRunescapeFont()) + .build(); + + final LineComponent bottomLine = LineComponent.builder() + .left("Progress:") + .leftFont(FontManager.getRunescapeFont()) + .right((int) (overallProgress * 100) + "%") + .rightFont(FontManager.getRunescapeFont()) + .rightColor(overallProgress >= 1 ? Color.GREEN : Color.WHITE) + .build(); + + final SplitComponent textSplit = SplitComponent.builder() + .first(topLine) + .second(bottomLine) + .orientation(ComponentOrientation.VERTICAL) + .build(); + + final ImageComponent rewardImageComponent = new ImageComponent(getRewardImage()); + final SplitComponent topInfoSplit = SplitComponent.builder() + .first(rewardImageComponent) + .second(textSplit) + .orientation(ComponentOrientation.HORIZONTAL) + .gap(new Point(ICON_AND_GOAL_GAP, 0)) + .build(); + + topPanel.getChildren().add(topInfoSplit); + panelComponent.getChildren().add(topPanel); + + if (config.showResinProgressBars()) { + // Add the progress bars in order: MOX, AGA, LYE, PotionComponent order is not the same + createProgressBar(PotionComponent.MOX); + createProgressBar(PotionComponent.AGA); + createProgressBar(PotionComponent.LYE); + } + + return super.render(graphics); + } + + public void markDataAsDirty() { + this.dataDirty = true; + } + + private void recalculateData() { + rewardItem = config.selectedReward(); + + double totalPercentage = 0.0; + totalPercentage += getAndCalculateComponentData(PotionComponent.MOX, MasteringMixologyPlugin.VARP_MOX_RESIN); + totalPercentage += getAndCalculateComponentData(PotionComponent.AGA, MasteringMixologyPlugin.VARP_AGA_RESIN); + totalPercentage += getAndCalculateComponentData(PotionComponent.LYE, MasteringMixologyPlugin.VARP_LYE_RESIN); + overallProgress = totalPercentage / PotionComponent.values().length; + + dataDirty = false; + } + + private double getAndCalculateComponentData(PotionComponent component, int varp) { + int currentAmount = client.getVarpValue(varp); + int goalAmount = rewardItem.componentCost(component); + double percentage = goalAmount == 0 ? 1.0 : Math.min((double) currentAmount / goalAmount, 1.0); + + ComponentData data = new ComponentData(currentAmount, goalAmount, percentage); + componentDataMap.put(component, data); + + return percentage; + } + + private void createProgressBar(PotionComponent component) { + ComponentData data = componentDataMap.get(component); + + final ImageComponent imageComponent = new ImageComponent(getComponentSprite(component)); + final ProgressBarComponent progressBarComponent = new ProgressBarComponent(); + progressBarComponent.setForegroundColor(Color.decode("#" + component.color())); + progressBarComponent.setBackgroundColor(new Color(61, 56, 49)); + progressBarComponent.setValue(data.percentage * 100d); + progressBarComponent.setLeftLabel(QuantityFormatter.quantityToStackSize(data.currentAmount)); + progressBarComponent.setRightLabel(QuantityFormatter.quantityToStackSize(data.goalAmount)); + + final SplitComponent progressBarSplit = SplitComponent.builder() + .first(imageComponent) + .second(progressBarComponent) + .orientation(ComponentOrientation.HORIZONTAL) + .gap(new Point(ICON_AND_GOAL_GAP, 0)) + .build(); + + panelComponent.getChildren().add(progressBarSplit); + } + + private BufferedImage getRewardImage() { + return rewardIconCache.computeIfAbsent(rewardItem.itemId(), itemManager::getImage); + } + + private BufferedImage getComponentSprite(PotionComponent component) { + return componentSpriteCache.computeIfAbsent(component, comp -> { + BufferedImage sprite = spriteManager.getSprite(comp.spriteId(), 0); + if (sprite != null) { + BufferedImage resizedImage = ImageUtil.resizeImage(sprite, COMPONENT_SPRITE_SIZE, COMPONENT_SPRITE_SIZE, true); + return ImageUtil.resizeCanvas(resizedImage, COMPONENT_SPRITE_SIZE, COMPONENT_SPRITE_SIZE); + } + return null; + }); + } + + private static class ComponentData { + final int currentAmount; + final int goalAmount; + final double percentage; + + ComponentData(int currentAmount, int goalAmount, double percentage) { + this.currentAmount = currentAmount; + this.goalAmount = goalAmount; + this.percentage = percentage; + } + } +} diff --git a/src/main/java/work/fking/masteringmixology/MasteringMixologyConfig.java b/src/main/java/work/fking/masteringmixology/MasteringMixologyConfig.java index ab5e3f7..206537e 100644 --- a/src/main/java/work/fking/masteringmixology/MasteringMixologyConfig.java +++ b/src/main/java/work/fking/masteringmixology/MasteringMixologyConfig.java @@ -151,4 +151,33 @@ default int highlightBorderWidth() { default int highlightFeather() { return 1; } + + @ConfigSection( + name = "Reward Tracking", + description = "Track your progress towards rewards", + position = 13 + ) + String REWARD_TRACKING = "RewardTracking"; + + @ConfigItem( + section = REWARD_TRACKING, + keyName = "selectedReward", + name = "Selected Reward", + description = "Select a reward to track resin for", + position = 1 + ) + default RewardItem selectedReward() { + return RewardItem.NONE; + } + + @ConfigItem( + section = REWARD_TRACKING, + keyName = "showResinProgressBars", + name = "Show Resin Progress Bars", + description = "Toggle to display or hide the resin progress bars in the goal overlay", + position = 2 + ) + default boolean showResinProgressBars() { + return true; + } } diff --git a/src/main/java/work/fking/masteringmixology/MasteringMixologyPlugin.java b/src/main/java/work/fking/masteringmixology/MasteringMixologyPlugin.java index 44ba9dc..8a9e487 100644 --- a/src/main/java/work/fking/masteringmixology/MasteringMixologyPlugin.java +++ b/src/main/java/work/fking/masteringmixology/MasteringMixologyPlugin.java @@ -5,6 +5,7 @@ import net.runelite.api.FontID; import net.runelite.api.GameState; import net.runelite.api.InventoryID; +import net.runelite.api.Player; import net.runelite.api.TileObject; import net.runelite.api.coords.LocalPoint; import net.runelite.api.events.GameStateChanged; @@ -60,9 +61,9 @@ public class MasteringMixologyPlugin extends Plugin { private static final int VARBIT_POTION_ORDER_3 = 11319; private static final int VARBIT_POTION_MODIFIER_3 = 11320; - private static final int VARP_LYE_RESIN = 4414; - private static final int VARP_AGA_RESIN = 4415; - private static final int VARP_MOX_RESIN = 4416; + static final int VARP_LYE_RESIN = 4414; + static final int VARP_AGA_RESIN = 4415; + static final int VARP_MOX_RESIN = 4416; private static final int VARBIT_ALEMBIC_PROGRESS = 11328; private static final int VARBIT_AGITATOR_PROGRESS = 11329; @@ -86,6 +87,9 @@ public class MasteringMixologyPlugin extends Plugin { private static final int COMPONENT_POTION_ORDERS_GROUP_ID = 882; private static final int COMPONENT_POTION_ORDERS = COMPONENT_POTION_ORDERS_GROUP_ID << 16 | 2; + private static final int LABS_REGION_ID = 5521; + private static final int LABS_REGION_PLANE = 0; + @Inject private Client client; @@ -107,6 +111,9 @@ public class MasteringMixologyPlugin extends Plugin { @Inject private InventoryPotionOverlay potionOverlay; + @Inject + private GoalInfoBoxOverlay goalInfoBoxOverlay; + private final Map highlightedObjects = new LinkedHashMap<>(); private List potionOrders = Collections.emptyList(); private boolean inLab = false; @@ -129,6 +136,16 @@ public boolean isInLab() { return inLab; } + /** + * @return true if the player is in the labs region (the area where the minigame takes place) + * the isInlab method only checks if they are inside the actual lab room where the UI is active + */ + public boolean isInLabRegion() { + Player player = client.getLocalPlayer(); + return player != null && player.getWorldLocation().getRegionID() == LABS_REGION_ID + && player.getWorldLocation().getPlane() == LABS_REGION_PLANE; + } + @Provides MasteringMixologyConfig provideConfig(ConfigManager configManager) { return configManager.getConfig(MasteringMixologyConfig.class); @@ -138,16 +155,14 @@ MasteringMixologyConfig provideConfig(ConfigManager configManager) { protected void startUp() { overlayManager.add(overlay); overlayManager.add(potionOverlay); - - if (client.getGameState() == GameState.LOGGED_IN) { - clientThread.invokeLater(this::initialize); - } + overlayManager.add(goalInfoBoxOverlay); } @Override protected void shutDown() { overlayManager.remove(overlay); overlayManager.remove(potionOverlay); + overlayManager.remove(goalInfoBoxOverlay); inLab = false; } @@ -197,6 +212,10 @@ public void onConfigChanged(ConfigChanged event) { unHighlightObject(AlchemyObject.DIGWEED_NORTH_WEST); } + if (event.getKey().equals("selectedReward") || event.getKey().equals("fullProgressBarBehavior")) { + goalInfoBoxOverlay.markDataAsDirty(); + } + if (config.highlightLevers()) { highlightLevers(); } else { @@ -235,6 +254,7 @@ public void onItemContainerChanged(ItemContainerChanged event) { @Subscribe public void onVarbitChanged(VarbitChanged event) { var varbitId = event.getVarbitId(); + var varpId = event.getVarpId(); var value = event.getValue(); // Whenever a potion is delivered, all the potion order related varbits are reset to 0 first then @@ -348,6 +368,8 @@ public void onVarbitChanged(VarbitChanged event) { } else if (varbitId == VARBIT_ALEMBIC_QUICKACTION) { // alembic quick action was just successfully popped resetDefaultHighlight(AlchemyObject.ALEMBIC); + } else if (varpId == VARP_MOX_RESIN || varpId == VARP_AGA_RESIN || varpId == VARP_LYE_RESIN) { + goalInfoBoxOverlay.markDataAsDirty(); } } diff --git a/src/main/java/work/fking/masteringmixology/PotionComponent.java b/src/main/java/work/fking/masteringmixology/PotionComponent.java index b2890b9..7030089 100644 --- a/src/main/java/work/fking/masteringmixology/PotionComponent.java +++ b/src/main/java/work/fking/masteringmixology/PotionComponent.java @@ -1,16 +1,18 @@ package work.fking.masteringmixology; public enum PotionComponent { - AGA('A', "00e676"), - LYE('L', "e91e63"), - MOX('M', "03a9f4"); + AGA('A', "00e676", 5667), + LYE('L', "e91e63", 5668), + MOX('M', "03a9f4", 5666); private final char character; private final String color; + private final int spriteId; - PotionComponent(char character, String color) { + PotionComponent(char character, String color, int spriteId) { this.character = character; this.color = color; + this.spriteId = spriteId; } public char character() { @@ -20,4 +22,8 @@ public char character() { public String color() { return color; } + + public int spriteId() { + return spriteId; + } } diff --git a/src/main/java/work/fking/masteringmixology/RewardItem.java b/src/main/java/work/fking/masteringmixology/RewardItem.java new file mode 100644 index 0000000..3d5d2b4 --- /dev/null +++ b/src/main/java/work/fking/masteringmixology/RewardItem.java @@ -0,0 +1,52 @@ +package work.fking.masteringmixology; + +import net.runelite.api.ItemID; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") // The items are used in the config +public enum RewardItem { + NONE("None", 0, 0, 0, 0), + + APPRENTICE_POTION_PACK("Apprentice Potion Pack", ItemID.APPRENTICE_POTION_PACK, 420, 70, 30), + ADEPT_POTION_PACK("Adept Potion Pack", ItemID.ADEPT_POTION_PACK, 180, 440, 70), + EXPERT_POTION_PACK("Expert Potion Pack", ItemID.EXPERT_POTION_PACK, 410, 320, 480), + PRESCRIPTION_GOGGLES("Prescription Goggles", ItemID.PRESCRIPTION_GOGGLES, 8600, 7000, 9350), + ALCHEMIST_LABCOAT("Alchemist Labcoat", ItemID.ALCHEMIST_LABCOAT, 2250, 2800, 3700), + ALCHEMIST_PANTS("Alchemist Pants", ItemID.ALCHEMIST_PANTS, 2250, 2800, 3700), + ALCHEMIST_GLOVES("Alchemist Gloves", ItemID.ALCHEMIST_GLOVES, 2250, 2800, 3700), + REAGENT_POUCH("Reagent Pouch", ItemID.REAGENT_POUCH, 13800, 11200, 15100), + POTION_STORAGE("Potion Storage", ItemID.POTION_STORAGE, 7750, 6300, 8950), + CHUGGING_BARREL("Chugging Barrel", ItemID.CHUGGING_BARREL, 17250, 14000, 18600), + ALCHEMISTS_AMULET("Alchemist's Amulet", ItemID.ALCHEMISTS_AMULET, 6900, 5650, 7400), + ALDARIUM("Aldarium", ItemID.ALDARIUM, 80, 60, 90); + + private final String itemName; + private final int itemId; + private final Map componentCost = new HashMap<>(); + + RewardItem(String itemName, int itemId, int moxResinCost, int agaResinCost, int lyeResinCost) { + this.itemName = itemName; + this.itemId = itemId; + this.componentCost.put(PotionComponent.MOX, moxResinCost); + this.componentCost.put(PotionComponent.AGA, agaResinCost); + this.componentCost.put(PotionComponent.LYE, lyeResinCost); + } + + public String itemName() { + return itemName; + } + + public int itemId() { + return itemId; + } + + public Map componentCost() { + return componentCost; + } + + public int componentCost(PotionComponent component) { + return componentCost.get(component); + } +} \ No newline at end of file