diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java index b946ba0a69..ca0abf243a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java @@ -24,62 +24,105 @@ */ package net.runelite.client.plugins.xptracker; -import net.runelite.api.Skill; import java.time.Duration; import java.time.Instant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Experience; +import net.runelite.api.Skill; -public class SkillXPInfo +@Data +@Slf4j +class SkillXPInfo { - private Skill skill; + private final Skill skill; private Instant skillTimeStart = null; + private int startXp = -1; private int xpGained = 0; - private int loginXp = 0; + private int actions = 0; + private int actionExp = 0; + private int nextLevelExp = 0; + private int startLevelExp = 0; - public SkillXPInfo(int loginXp, Skill skill) + int getXpHr() { - this.skill = skill; - this.loginXp = loginXp; + return toHourly(xpGained); } - public int getXpHr() + int getActionsHr() { - long timeElapsedInSeconds = Duration.between( - skillTimeStart, Instant.now()).getSeconds(); - return (int) ((1.0 / (timeElapsedInSeconds / 3600.0)) * xpGained); - + return toHourly(actions); } - public void reset(int loginXp) + private int toHourly(int value) { + if (skillTimeStart == null) + { + return 0; + } + + long timeElapsedInSeconds = Duration.between(skillTimeStart, Instant.now()).getSeconds(); + return (int) ((1.0 / (timeElapsedInSeconds / 3600.0)) * value); + } + + int getXpRemaining() + { + return nextLevelExp - (startXp + xpGained); + } + + int getActionsRemaining() + { + return getXpRemaining() / actionExp; + } + + int getSkillProgress() + { + int currentXp = startXp + xpGained; + + double xpGained = currentXp - startLevelExp; + double xpGoal = nextLevelExp - startLevelExp; + return (int) ((xpGained / xpGoal) * 100); + } + + void reset(int currentXp) + { + if (startXp != -1) + { + startXp = currentXp; + } + xpGained = 0; - this.loginXp = loginXp; + actions = 0; skillTimeStart = null; } - public void update(int currentXp) + boolean update(int currentXp) { - xpGained = currentXp - loginXp; + if (startXp == -1) + { + return false; + } + + int originalXp = xpGained + startXp; + + if (originalXp >= currentXp) + { + return false; + } + + actionExp = currentXp - originalXp; + actions++; + xpGained = currentXp - startXp; + startLevelExp = Experience.getXpForLevel(Experience.getLevelForXp(currentXp)); + + int currentLevel = Experience.getLevelForXp(currentXp); + nextLevelExp = currentLevel + 1 <= Experience.MAX_VIRT_LEVEL ? Experience.getXpForLevel(currentLevel + 1) : -1; + if (skillTimeStart == null) + { skillTimeStart = Instant.now(); - } + } - public Instant getSkillTimeStart() - { - return skillTimeStart; + return true; } - - public int getXpGained() - { - return xpGained; - } - - public int getLoginXp() - { - return loginXp; - } - - public Skill getSkill() - { - return this.skill; - } -} +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java new file mode 100644 index 0000000000..0b005fa14e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2018, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.xptracker; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.io.IOException; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; + +@Slf4j +class XpInfoBox extends JPanel +{ + + private static final Color[] PROGRESS_COLORS = new Color[] { Color.RED, Color.YELLOW, Color.GREEN }; + + @Getter(AccessLevel.PACKAGE) + private final SkillXPInfo xpInfo; + + private final JProgressBar progressBar = new JProgressBar(); + private final JLabel xpHr = new JLabel(); + private final JLabel xpGained = new JLabel(); + private final JLabel actionsHr = new JLabel(); + private final JLabel actions = new JLabel(); + private final Client client; + private final JPanel panel; + + XpInfoBox(Client client, JPanel panel, SkillXPInfo xpInfo) throws IOException + { + this.client = client; + this.panel = panel; + this.xpInfo = xpInfo; + + setLayout(new BorderLayout()); + setBorder(BorderFactory.createLineBorder(getBackground().brighter(), 1, true)); + + final JPanel container = new JPanel(); + container.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + container.setLayout(new BorderLayout(3, 3)); + + // Create skill/reset icon + final String skillIcon = "/skill_icons/" + xpInfo.getSkill().getName().toLowerCase() + ".png"; + final JButton resetIcon = new JButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream(skillIcon)))); + resetIcon.setToolTipText("Reset " + xpInfo.getSkill().getName() + " tracker"); + resetIcon.setPreferredSize(new Dimension(64, 64)); + resetIcon.addActionListener(e -> reset()); + + // Create info panel + final JPanel rightPanel = new JPanel(); + rightPanel.setLayout(new GridLayout(4, 1)); + rightPanel.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 0)); + rightPanel.add(xpHr); + rightPanel.add(xpGained); + rightPanel.add(actionsHr); + rightPanel.add(actions); + + // Create progress bar + progressBar.setStringPainted(true); + progressBar.setValue(0); + progressBar.setMinimum(0); + progressBar.setMaximum(100); + progressBar.setBackground(Color.RED); + + container.add(resetIcon, BorderLayout.LINE_START); + container.add(rightPanel, BorderLayout.CENTER); + container.add(progressBar, BorderLayout.SOUTH); + add(container, BorderLayout.CENTER); + } + + void reset() + { + xpInfo.reset(client.getSkillExperience(xpInfo.getSkill())); + panel.remove(this); + panel.revalidate(); + } + + void init() + { + if (xpInfo.getStartXp() != -1) + { + return; + } + + xpInfo.setStartXp(client.getSkillExperience(xpInfo.getSkill())); + } + + void update() + { + if (xpInfo.getStartXp() == -1) + { + return; + } + + boolean updated = xpInfo.update(client.getSkillExperience(xpInfo.getSkill())); + + SwingUtilities.invokeLater(() -> + { + if (updated) + { + if (getParent() != panel) + { + panel.add(this); + panel.revalidate(); + } + + xpHr.setText(XpPanel.formatLine(xpInfo.getXpGained(), "xp gained")); + actions.setText(XpPanel.formatLine(xpInfo.getActions(), "actions")); + + final int progress = xpInfo.getSkillProgress(); + + progressBar.setValue(progress); + progressBar.setBackground(interpolateColors(PROGRESS_COLORS, (double)progress / 100d)); + + progressBar.setToolTipText("" + + XpPanel.formatLine(xpInfo.getXpRemaining(), "xp remaining") + + "
" + + XpPanel.formatLine(xpInfo.getActionsRemaining(), "actions remaining") + + ""); + } + + // Always update xp/hr and actions/hr as time always changes + xpGained.setText(XpPanel.formatLine(xpInfo.getXpHr(), "xp/hr")); + actionsHr.setText(XpPanel.formatLine(xpInfo.getActionsHr(), "actions/hr")); + }); + } + + + /** + * Interpolate between array of colors using Normal (Gaussian) distribution + * @see Normal distribution on Wikipedia + * @param colors array of colors + * @param x distribution factor + * @return interpolated color + */ + private static Color interpolateColors(Color[] colors, double x) + { + double r = 0.0, g = 0.0, b = 0.0; + double total = 0.0; + double step = 1.0 / (double)(colors.length - 1); + double mu = 0.0; + double sigma2 = 0.035; + + for (Color ignored : colors) + { + total += Math.exp(-(x - mu) * (x - mu) / (2.0 * sigma2)) / Math.sqrt(2.0 * Math.PI * sigma2); + mu += step; + } + + mu = 0.0; + + for (Color color : colors) + { + double percent = Math.exp(-(x - mu) * (x - mu) / (2.0 * sigma2)) / Math.sqrt(2.0 * Math.PI * sigma2); + mu += step; + + r += color.getRed() * percent / total; + g += color.getGreen() * percent / total; + b += color.getBlue() * percent / total; + } + + return new Color((int)r, (int)g, (int)b); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java index aa542e83c5..adee74ae5d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java @@ -25,40 +25,61 @@ package net.runelite.client.plugins.xptracker; import java.awt.BorderLayout; -import java.awt.Dimension; +import java.awt.GridLayout; import java.io.IOException; import java.text.NumberFormat; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import javax.imageio.ImageIO; -import javax.inject.Inject; -import javax.swing.ImageIcon; +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.SwingUtilities; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.Skill; import net.runelite.client.ui.PluginPanel; @Slf4j -public class XpPanel extends PluginPanel +class XpPanel extends PluginPanel { - private Map labelMap = new HashMap<>(); - private final XpTrackerPlugin xpTracker; + private final Map infoBoxes = new HashMap<>(); + private final JLabel totalXpGained = new JLabel(); + private final JLabel totalXpHr = new JLabel(); - @Inject - Client client; - @Inject - ScheduledExecutorService executor; - - @Inject - public XpPanel(XpTrackerPlugin xpTracker) + XpPanel(Client client) { super(); - this.xpTracker = xpTracker; + + final JPanel layoutPanel = new JPanel(); + layoutPanel.setLayout(new BorderLayout(0, 3)); + add(layoutPanel); + + final JPanel totalPanel = new JPanel(); + totalPanel.setLayout(new BorderLayout()); + totalPanel.setBorder(BorderFactory.createLineBorder(getBackground().brighter(), 1, true)); + + final JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new GridLayout(3, 1)); + infoPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + + final JButton resetButton = new JButton("Reset All"); + resetButton.addActionListener(e -> resetAllInfoBoxes()); + + totalXpGained.setText(formatLine(0, "total xp gained")); + totalXpHr.setText(formatLine(0, "total xp/hr")); + + infoPanel.add(totalXpGained); + infoPanel.add(totalXpHr); + infoPanel.add(resetButton); + totalPanel.add(infoPanel, BorderLayout.CENTER); + layoutPanel.add(totalPanel, BorderLayout.NORTH); + + final JPanel infoBoxPanel = new JPanel(); + infoBoxPanel.setLayout(new GridLayout(0,1,0,3)); + layoutPanel.add(infoBoxPanel, BorderLayout.CENTER); try { @@ -69,88 +90,56 @@ public class XpPanel extends PluginPanel break; } - JLabel skillLabel = new JLabel(); - labelMap.put(skill, makeSkillPanel(skill, skillLabel)); + infoBoxes.put(skill, new XpInfoBox(client, infoBoxPanel, new SkillXPInfo(skill))); } } catch (IOException e) { log.warn(null, e); } - - JButton resetButton = new JButton("Reset All"); - resetButton.addActionListener((e) -> executor.execute(this::resetAllSkillXpHr)); - resetButton.setPreferredSize(new Dimension(0, 32)); - add(resetButton); } - private JButton makeSkillResetButton(Skill skill) throws IOException + void resetAllInfoBoxes() { - ImageIcon resetIcon = new ImageIcon(ImageIO.read(getClass().getResourceAsStream("reset.png"))); - JButton resetButton = new JButton(resetIcon); - resetButton.setPreferredSize(new Dimension(32, 32)); - resetButton.addActionListener(actionEvent -> resetSkillXpHr(skill)); - return resetButton; + infoBoxes.forEach((skill, xpInfoBox) -> xpInfoBox.reset()); + updateTotal(); } - private JPanel makeSkillPanel(Skill skill, JLabel levelLabel) throws IOException + void updateAllInfoBoxes() { - BorderLayout borderLayout = new BorderLayout(); - borderLayout.setHgap(12); - JPanel iconLevel = new JPanel(borderLayout); - iconLevel.setPreferredSize(new Dimension(0, 32)); - - String skillIcon = "/skill_icons/" + skill.getName().toLowerCase() + ".png"; - log.debug("Loading skill icon from {}", skillIcon); - JLabel icon = new JLabel(new ImageIcon(ImageIO.read(XpPanel.class.getResourceAsStream(skillIcon)))); - iconLevel.add(icon, BorderLayout.LINE_START); - iconLevel.add(levelLabel, BorderLayout.CENTER); - iconLevel.add(makeSkillResetButton(skill), BorderLayout.LINE_END); - - return iconLevel; + infoBoxes.forEach((skill, xpInfoBox) -> xpInfoBox.update()); + updateTotal(); } - public void resetSkillXpHr(Skill skill) + void updateSkillExperience(Skill skill) { - int skillIdx = skill.ordinal(); - xpTracker.getXpInfos()[skillIdx].reset(client.getSkillExperience(skill)); - remove(labelMap.get(skill)); - revalidate(); + final XpInfoBox xpInfoBox = infoBoxes.get(skill); + xpInfoBox.update(); + xpInfoBox.init(); + updateTotal(); } - public void resetAllSkillXpHr() + private void updateTotal() { - for (SkillXPInfo skillInfo : xpTracker.getXpInfos()) + final AtomicInteger totalXpGainedVal = new AtomicInteger(); + final AtomicInteger totalXpHrVal = new AtomicInteger(); + + for (XpInfoBox xpInfoBox : infoBoxes.values()) { - if (skillInfo != null && skillInfo.getSkillTimeStart() != null) - { - resetSkillXpHr(skillInfo.getSkill()); - } + totalXpGainedVal.addAndGet(xpInfoBox.getXpInfo().getXpGained()); + totalXpHrVal.addAndGet(xpInfoBox.getXpInfo().getXpHr()); } - } - public void updateAllSkillXpHr() - { - for (SkillXPInfo skillInfo : xpTracker.getXpInfos()) + SwingUtilities.invokeLater(() -> { - if (skillInfo != null && skillInfo.getSkillTimeStart() != null - && skillInfo.getXpGained() != 0) - { - updateSkillXpHr(skillInfo); - } - } + totalXpGained.setText(formatLine(totalXpGainedVal.get(), "total xp gained")); + totalXpHr.setText(formatLine(totalXpHrVal.get(), "total xp/hr")); + }); } - public void updateSkillXpHr(SkillXPInfo skillXPInfo) + static String formatLine(double number, String description) { - JPanel skillPanel = labelMap.get(skillXPInfo.getSkill()); - JLabel xpHr = (JLabel) skillPanel.getComponent(1); - xpHr.setText(NumberFormat.getInstance().format(skillXPInfo.getXpHr()) + " xp/hr"); - if (skillPanel.getParent() != this) - { - add(skillPanel); - revalidate(); - } + return NumberFormat.getInstance().format(number) + " " + description; } -} +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java index f70ec36b23..6a66a5d377 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java @@ -24,10 +24,9 @@ */ package net.runelite.client.plugins.xptracker; +import static net.runelite.client.plugins.xptracker.XpWorldType.NORMAL; import com.google.common.eventbus.Subscribe; import java.io.IOException; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.EnumSet; import java.util.Objects; import javax.imageio.ImageIO; @@ -35,13 +34,11 @@ import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.GameState; -import net.runelite.api.Skill; import net.runelite.api.events.ExperienceChanged; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; -import static net.runelite.client.plugins.xptracker.XpWorldType.NORMAL; -import net.runelite.client.task.Schedule; import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.NavigationButton; import net.runelite.http.api.worlds.World; @@ -55,8 +52,6 @@ import net.runelite.http.api.worlds.WorldType; @Slf4j public class XpTrackerPlugin extends Plugin { - private static final int NUMBER_OF_SKILLS = Skill.values().length - 1; //ignore overall - @Inject ClientUI ui; @@ -65,9 +60,7 @@ public class XpTrackerPlugin extends Plugin private NavigationButton navButton; private XpPanel xpPanel; - private final SkillXPInfo[] xpInfos = new SkillXPInfo[NUMBER_OF_SKILLS]; private WorldResult worlds; - private XpWorldType lastWorldType; private String lastUsername; @@ -85,7 +78,7 @@ public class XpTrackerPlugin extends Plugin log.warn("Error looking up worlds list", e); } - xpPanel = injector.getInstance(XpPanel.class); + xpPanel = new XpPanel(client); navButton = new NavigationButton( "XP Tracker", ImageIO.read(getClass().getResourceAsStream("xp.png")), @@ -125,9 +118,7 @@ public class XpTrackerPlugin extends Plugin lastUsername = client.getUsername(); lastWorldType = type; - - xpPanel.resetAllSkillXpHr(); - Arrays.fill(xpInfos, null); + xpPanel.resetAllInfoBoxes(); } } } @@ -149,33 +140,12 @@ public class XpTrackerPlugin extends Plugin @Subscribe public void onXpChanged(ExperienceChanged event) { - Skill skill = event.getSkill(); - int skillIdx = skill.ordinal(); - - //To catch login ExperienceChanged event. - if (xpInfos[skillIdx] != null) - { - xpInfos[skillIdx].update(client.getSkillExperience(skill)); - } - else - { - xpInfos[skillIdx] = new SkillXPInfo(client.getSkillExperience(skill), - skill); - } + xpPanel.updateSkillExperience(event.getSkill()); } - @Schedule( - period = 600, - unit = ChronoUnit.MILLIS - ) - public void updateXp() + @Subscribe + public void onGameTick(GameTick event) { - xpPanel.updateAllSkillXpHr(); + xpPanel.updateAllInfoBoxes(); } - - public SkillXPInfo[] getXpInfos() - { - return xpInfos; - } - -} +} \ No newline at end of file