diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 6a46378c4d..d39d4f44fa 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -47,6 +47,7 @@ import net.runelite.client.plugins.mousehighlight.MouseHighlight; import net.runelite.client.plugins.opponentinfo.OpponentInfo; import net.runelite.client.plugins.pestcontrol.PestControl; import net.runelite.client.plugins.runecraft.Runecraft; +import net.runelite.client.plugins.xpglobes.XpGlobes; import net.runelite.client.plugins.xtea.Xtea; import net.runelite.client.plugins.zulrah.Zulrah; import org.slf4j.Logger; @@ -83,6 +84,7 @@ public class PluginManager plugins.add(new ConfigPlugin()); plugins.add(new GroundItems()); plugins.add(new Implings()); + plugins.add(new XpGlobes()); if (RuneLite.getOptions().has("developer-mode")) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java new file mode 100644 index 0000000000..95ad93f3ef --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import net.runelite.api.Skill; + +import java.time.Instant; + +public class XpGlobe +{ + + private Skill skill; + private int currentXp; + private int currentLevel; + private int goalXp; + private Instant time; + private double skillProgressRadius; + + public XpGlobe(Skill skill, int currentXp, int currentLevel, int goalXp, Instant time) + { + this.skill = skill; + this.currentXp = currentXp; + this.currentLevel = currentLevel; + this.goalXp = goalXp; + this.time = time; + } + + public Skill getSkill() + { + return skill; + } + + public void setSkill(Skill skill) + { + this.skill = skill; + } + + public int getCurrentXp() + { + return currentXp; + } + + public void setCurrentXp(int currentXp) + { + this.currentXp = currentXp; + } + + public int getCurrentLevel() + { + return currentLevel; + } + + public void setCurrentLevel(int currentLevel) + { + this.currentLevel = currentLevel; + } + + public int getGoalXp() + { + return goalXp; + } + + public void setGoalXp(int goalXp) + { + this.goalXp = goalXp; + } + + public int getGoalLevel() + { + return this.currentLevel++; + } + + public String getSkillName() + { + return skill.getName(); + } + + public double getSkillProgressRadius() + { + return skillProgressRadius; + } + + public void setSkillProgressRadius(int startXp, int currentXp, int goalXp) + { + double xpGained = currentXp - startXp; + double xpGoal = goalXp - startXp; + this.skillProgressRadius = -(3.6 * ((xpGained / xpGoal) * 100)); //arc goes backwards + } + + public Instant getTime() + { + return time; + } + + public void setTime(Instant time) + { + this.time = time; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobes.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobes.java new file mode 100644 index 0000000000..d4484323b6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobes.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import com.google.common.eventbus.Subscribe; +import net.runelite.api.Client; +import net.runelite.api.Experience; +import net.runelite.api.Skill; +import net.runelite.client.RuneLite; +import net.runelite.client.events.ExperienceChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.Overlay; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +public class XpGlobes extends Plugin +{ + + private final XpGlobesConfig config = RuneLite.getRunelite().getConfigManager() + .getConfig(XpGlobesConfig.class); + private final Overlay overlay = new XpGlobesOverlay(this); + private final Client client = RuneLite.getClient(); + private final XpGlobe[] globeCache = new XpGlobe[Skill.values().length - 1]; //overall does not trigger xp change event + private final List xpGlobes = new ArrayList<>(); + private static final int SECONDS_TO_SHOW_GLOBE = 10; + private static final int MAXIMUM_SHOWN_GLOBES = 5; + + @Override + protected void startUp() throws Exception + { + + } + + @Override + protected void shutDown() throws Exception + { + + } + + @Override + public Overlay getOverlay() + { + return overlay; + } + + @Subscribe + private void onExperienceChanged(ExperienceChanged event) + { + if (!config.enabled()) + { + return; + } + + Skill skill = event.getSkill(); + int currentXp = client.getSkillExperience(skill); + int currentLevel = Experience.getLevelForXp(currentXp); + int startingXp = 0; + if (currentLevel > 1) + { + startingXp = Experience.getXpForLevel(currentLevel); + } + int goalXp = Experience.getXpForLevel(currentLevel+1); + + int skillIdx = skill.ordinal(); + XpGlobe cachedGlobe = globeCache[skillIdx]; + if (cachedGlobe != null) + { + cachedGlobe.setSkill(skill); + cachedGlobe.setCurrentXp(currentXp); + cachedGlobe.setCurrentLevel(currentLevel); + cachedGlobe.setGoalXp(goalXp); + cachedGlobe.setTime(Instant.now()); + cachedGlobe.setSkillProgressRadius(startingXp, currentXp, goalXp); + } + else + { + globeCache[skillIdx] = new XpGlobe(skill, currentXp, currentLevel, goalXp, Instant.now()); + } + + this.addXpGlobe(globeCache[skillIdx], MAXIMUM_SHOWN_GLOBES); + } + + public List getXpGlobes() + { + return xpGlobes; + } + + public void addXpGlobe(XpGlobe xpGlobe, int maxLength) + { + if (xpGlobes.contains(xpGlobe)) + { + //remove the old globe, allowing it to be readded as the most recent (right) side when drawn + xpGlobes.remove(xpGlobe); + } + if (getXpGlobesSize() >= maxLength) + { + xpGlobes.remove(0); + } + xpGlobes.add(xpGlobe); + } + + public int getXpGlobesSize() + { + return xpGlobes.size(); + } + + public void removeExpiredXpGlobes() + { + if (!xpGlobes.isEmpty()) + { + Instant currentTime = Instant.now(); + for (Iterator it = xpGlobes.iterator(); it.hasNext();) + { + XpGlobe globe = it.next(); + Instant globeCreationTime = globe.getTime(); + if (currentTime.isBefore(globeCreationTime.plusSeconds(SECONDS_TO_SHOW_GLOBE))) + { + //if a globe is not expired, stop checking newer globes + return; + } + it.remove(); + } + } + } + + public XpGlobesConfig getConfig() + { + return config; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java new file mode 100644 index 0000000000..b102f4720c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "xpglobes", + name = "XP Globes", + description = "Configuration for the xp globes plugin" +) +public interface XpGlobesConfig +{ + @ConfigItem( + keyName = "enabled", + name = "Enabled", + description = "Configures whether or not xp globes are displayed" + ) + default boolean enabled() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java new file mode 100644 index 0000000000..822642f784 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.Skill; +import net.runelite.client.RuneLite; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.imageio.ImageIO; +import java.awt.geom.Arc2D; +import java.awt.geom.Ellipse2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class XpGlobesOverlay extends Overlay +{ + + private final XpGlobes plugin; + private final Client client = RuneLite.getClient(); + private final XpGlobesConfig config; + private static final Logger logger = LoggerFactory.getLogger(XpGlobesOverlay.class); + + private static final int DEFAULT_CIRCLE_WIDTH = 40; + private static final int DEFAULT_CIRCLE_HEIGHT = 40; + private static final int MINIMUM_STEP_WIDTH = DEFAULT_CIRCLE_WIDTH + 10; + + private static final int PROGRESS_RADIUS_START = 270; + private static final int PROGRESS_RADIUS_REMAINDER = 0; + + private static final Color DEFAULT_XPGLOBE_BACKGROUND_COLOR = new Color(Color.gray.getRed(), Color.gray.getGreen(), Color.gray.getBlue(), 127); + private static final Color DEFAULT_PROGRESS_ARC_COLOR = Color.ORANGE; + private static final Color DEFAULT_PROGRESS_REMAINDER_ARC_COLOR = Color.BLACK; + + private static final int DEFAULT_START_Y = 10; + + private final BufferedImage[] imgCache = new BufferedImage[Skill.values().length - 1]; + + public XpGlobesOverlay(XpGlobes plugin) + { + super(OverlayPosition.DYNAMIC); + this.config = plugin.getConfig(); + this.plugin = plugin; + } + + @Override + public Dimension render(Graphics2D graphics) + { + + // won't draw if not logged in or not enabled + if (client.getGameState() != GameState.LOGGED_IN || !config.enabled()) + { + return null; + } + + int queueSize = plugin.getXpGlobesSize(); + if (queueSize > 0) + { + List xpChangedQueue = plugin.getXpGlobes(); + int markersLength = (queueSize * (DEFAULT_CIRCLE_WIDTH)) + ((MINIMUM_STEP_WIDTH - DEFAULT_CIRCLE_WIDTH) * (queueSize - 1)); + int startDrawX; + if (client.isResized()) + { + startDrawX = (client.getClientWidth() - markersLength) / 2; + } + else + { + startDrawX = (client.getViewportHeight() - markersLength) / 2; + } + for (XpGlobe xpGlobe : xpChangedQueue) + { + renderProgressCircle(graphics, xpGlobe, startDrawX, DEFAULT_START_Y); + startDrawX += MINIMUM_STEP_WIDTH; + } + + plugin.removeExpiredXpGlobes(); + } + + return null; + } + + private void renderProgressCircle(Graphics2D graphics, XpGlobe skillToDraw, int x, int y) + { + double radiusCurrentXp = skillToDraw.getSkillProgressRadius(); + double radiusToGoalXp = 360; //draw a circle + + drawEllipse(graphics, x, y); + drawProgressArc( + graphics, + x, y, + DEFAULT_CIRCLE_WIDTH, DEFAULT_CIRCLE_HEIGHT, + PROGRESS_RADIUS_REMAINDER, radiusToGoalXp, + 5, + DEFAULT_PROGRESS_REMAINDER_ARC_COLOR + ); + drawProgressArc( + graphics, + x, y, + DEFAULT_CIRCLE_WIDTH, DEFAULT_CIRCLE_HEIGHT, + PROGRESS_RADIUS_START, radiusCurrentXp, + 2, + DEFAULT_PROGRESS_ARC_COLOR); + + drawSkillImage(graphics, skillToDraw, x, y); + } + + private void drawProgressArc(Graphics2D graphics, int x, int y, int w, int h, double radiusStart, double radiusEnd, int strokeWidth, Color color) + { + graphics.setStroke(new BasicStroke(strokeWidth)); + graphics.setColor(color); + graphics.draw(new Arc2D.Double( + x, y, + w, h, + radiusStart, radiusEnd, + Arc2D.OPEN)); + } + + private void drawEllipse(Graphics2D graphics, int x, int y) + { + graphics.setColor(DEFAULT_XPGLOBE_BACKGROUND_COLOR); + Ellipse2D backgroundCircle = new Ellipse2D.Double(x, y, DEFAULT_CIRCLE_WIDTH, DEFAULT_CIRCLE_HEIGHT); + graphics.fill(backgroundCircle); + graphics.draw(backgroundCircle); + } + + private void drawSkillImage(Graphics2D graphics, XpGlobe xpGlobe, int x, int y) + { + BufferedImage skillImage = getSkillImage(xpGlobe); + graphics.drawImage( + getSkillImage(xpGlobe), + x + (DEFAULT_CIRCLE_WIDTH / 2) - (skillImage.getWidth() / 2), + y + (DEFAULT_CIRCLE_HEIGHT / 2) - (skillImage.getHeight() / 2), + null + ); + } + + private BufferedImage getSkillImage(XpGlobe xpGlobe) + { + int skillIdx = xpGlobe.getSkill().ordinal(); + BufferedImage skillImage = null; + + if (imgCache[skillIdx] != null) + { + return imgCache[skillIdx]; + } + + try + { + String skillIconPath = "/skill_icons/" + xpGlobe.getSkillName().toLowerCase() + ".png"; + logger.debug("Loading skill icon from {}", skillIconPath); + skillImage = ImageIO.read(XpGlobesOverlay.class.getResourceAsStream(skillIconPath)); + imgCache[skillIdx] = skillImage; + } + catch (IOException e) + { + logger.debug("Error Loading skill icons {}", e); + } + + return skillImage; + } +}