From c0a45cac92de55ae438f003b8fcd308a5a39db51 Mon Sep 17 00:00:00 2001 From: Toocanzs Date: Sun, 26 Nov 2017 11:07:29 -0500 Subject: [PATCH] Add special attack minimap orb --- .../client/plugins/specorb/SpecOrbConfig.java | 47 ++++++ .../plugins/specorb/SpecOrbOverlay.java | 136 ++++++++++++++++ .../client/plugins/specorb/SpecOrbPlugin.java | 98 ++++++++++++ .../client/ui/overlay/OverlayUtil.java | 146 +++++++++++++++++- .../specorb/minimap_orb_background.png | Bin 0 -> 2385 bytes .../plugins/specorb/special_orb_icon.png | Bin 0 -> 860 bytes 6 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbPlugin.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/specorb/minimap_orb_background.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/specorb/special_orb_icon.png diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbConfig.java new file mode 100644 index 0000000000..da8741f6ff --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017, 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.specorb; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "specorb", + name = "Special Attack Orb", + description = "Configuration for the Special Attack Orbplugin" +) +public interface SpecOrbConfig extends Config +{ + @ConfigItem( + keyName = "enabled", + name = "Enabled", + description = "Configures whether or not Special Attack Orb plugin is displayed" + ) + default boolean enabled() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbOverlay.java new file mode 100644 index 0000000000..0ac2c28597 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbOverlay.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2017, 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.specorb; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.annotation.Nullable; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.Point; +import net.runelite.api.Varbits; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.events.GameTick; +import net.runelite.client.events.VarbitChanged; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; + +public class SpecOrbOverlay extends Overlay +{ + private static final int RECHARGE_TIME_TICKS = 51; + + private static final int ORB_X_OFFSET = 60; + private static final int ORB_Y_OFFSET = 123; + private static final Color SPECIAL_ORB_BACKGROUND_COLOR = new Color(51, 102, 255); + private static final Color SPECIAL_ORB_RECHARGE_COLOR = new Color(153, 204, 255, 50); + + private final Client client; + private final SpecOrbConfig config; + private final SpecOrbPlugin plugin; + private int lastSpecialPercent = 0; + private int tickCounter = 0; + + @Inject + public SpecOrbOverlay(@Nullable Client client, SpecOrbConfig config, SpecOrbPlugin plugin) + { + super(OverlayPosition.DYNAMIC); + this.client = client; + this.config = config; + this.plugin = plugin; + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (client.getGameState() != GameState.LOGGED_IN || !config.enabled()) + { + return null; + } + + Widget xpOrb = client.getWidget(WidgetInfo.MINIMAP_XP_ORB); + if (xpOrb == null) + { + return null; + } + + graphics.setColor(SPECIAL_ORB_BACKGROUND_COLOR); + + boolean specialAttackEnabled = client.getSetting(Varbits.SPECIAL_ATTACK_ENABLED) == 1; + + // draw relative to the xp orb + Point xpOrbPoint = xpOrb.getCanvasLocation(); + Point specOrbPoint = new Point(xpOrbPoint.getX() + ORB_X_OFFSET, xpOrbPoint.getY() + ORB_Y_OFFSET); + + double specialPercent = client.getSetting(Varbits.SPECIAL_ATTACK_PERCENT) / 1000.0; + double specialRechargePercent = tickCounter / (double) RECHARGE_TIME_TICKS; + + OverlayUtil.drawMinimapOrb(graphics, specOrbPoint, specialPercent, + SPECIAL_ORB_RECHARGE_COLOR, specialRechargePercent, + plugin.getMinimapOrbBackground(), plugin.getSpecialAttackIcon(), + (int) (specialPercent * 100), specialAttackEnabled); + + return null; + } + + public void onVarbitChanged(VarbitChanged event) + { + int specialPercent = client.getSetting(Varbits.SPECIAL_ATTACK_PERCENT); + if (lastSpecialPercent != specialPercent) + { + int diff = specialPercent - lastSpecialPercent; + lastSpecialPercent = specialPercent; + onSpecialChange(diff); + } + } + + private void onSpecialChange(int diff) + { + // If we went from 500-600 for example + if (diff > 0) + { + // Reset the tick counter as we just recharged + tickCounter = 0; + } + } + + public void onTick(GameTick event) + { + // 1000 = 100%, 500 = 50%, 0 = 0% + int specialPercent = client.getSetting(Varbits.SPECIAL_ATTACK_PERCENT); + // The recharge doesn't tick when at 100% + if (specialPercent == 1000) + { + tickCounter = 0; + } + else + { + tickCounter++; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbPlugin.java new file mode 100644 index 0000000000..55c1577bdd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specorb/SpecOrbPlugin.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017, 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.specorb; + +import com.google.common.eventbus.Subscribe; +import com.google.inject.Binder; +import com.google.inject.Provides; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.events.GameTick; +import net.runelite.client.events.VarbitChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.Overlay; + +import javax.imageio.ImageIO; +import javax.inject.Inject; +import java.awt.Image; + +@PluginDescriptor( + name = "Special Attack Orb plugin" +) +public class SpecOrbPlugin extends Plugin +{ + private Image minimapOrbBackground; + private Image specialAttackIcon; + + @Inject + SpecOrbOverlay overlay; + + @Override + public void configure(Binder binder) + { + binder.bind(SpecOrbOverlay.class); + } + + @Provides + SpecOrbConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(SpecOrbConfig.class); + } + + @Override + public Overlay getOverlay() + { + return overlay; + } + + @Override + protected void startUp() throws Exception + { + minimapOrbBackground = ImageIO.read(getClass().getResourceAsStream("minimap_orb_background.png")); + specialAttackIcon = ImageIO.read(getClass().getResourceAsStream("special_orb_icon.png")); + } + + @Subscribe + public void onVarbitChanged(VarbitChanged event) + { + overlay.onVarbitChanged(event); + } + + @Subscribe + public void onTick(GameTick event) + { + overlay.onTick(event); + } + + public Image getMinimapOrbBackground() + { + return minimapOrbBackground; + } + + public Image getSpecialAttackIcon() + { + return specialAttackIcon; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java index 95c3d06c5e..a743042394 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java @@ -26,14 +26,20 @@ package net.runelite.client.ui.overlay; import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Paint; import java.awt.Polygon; +import java.awt.RadialGradientPaint; +import java.awt.geom.Point2D; import java.awt.image.BufferedImage; - import net.runelite.api.Actor; import net.runelite.api.Point; import net.runelite.api.SpritePixels; import net.runelite.api.TileObject; +import net.runelite.client.ui.FontManager; /** @@ -41,6 +47,7 @@ import net.runelite.api.TileObject; */ public class OverlayUtil { + public static void renderPolygon(Graphics2D graphics, Polygon poly, Color color) { graphics.setColor(color); @@ -50,6 +57,143 @@ public class OverlayUtil graphics.fillPolygon(poly); } + /** + * Draws a minimap orb like the prayer/HP/run orbs. The background color can be set by graphics.setColor() before calling this + * + * @param graphics graphics to draw to + * @param pos top left position of the orb + * @param percentFilled how far filled the orb should be + * @param rechargeColor color of the recharge meter of this orb. This can be used to track things like hp regen or special regen. + * @param rechargePercentFilled how far through the recharge this stat is + * @param minimapOrbBackground background image of the minimap orb + * @param overlayImage + * @param amount number to display on the orb + * @param enabled + */ + public static void drawMinimapOrb(Graphics2D graphics, Point pos, double percentFilled, Color rechargeColor, double rechargePercentFilled, Image minimapOrbBackground, Image overlayImage, int amount, boolean enabled) + { + Color startColor = graphics.getColor(); + + Point orbPos = new Point(pos.getX() + 26, pos.getY() + 2); + + graphics.setColor(new Color(20, 20, 20)); + // draw black background for the orb when it's partially missing + drawOrbPercent(graphics, orbPos, 1, 28, false, false); + graphics.setColor(startColor); + + // draw orb with shading + drawOrbPercent(graphics, orbPos, percentFilled, 28, true, enabled); + + graphics.setColor(rechargeColor); + // draw recharge + drawOrbPercent(graphics, orbPos, rechargePercentFilled, 28, false, false); + graphics.setColor(startColor); + + // draw background + graphics.drawImage(minimapOrbBackground, pos.getX(), pos.getY(), null); + // draw overlay + graphics.drawImage(overlayImage, pos.getX() + 32, pos.getY() + 9, null); + + drawOrbAmount(graphics, pos, amount);//draw number on orb + + } + + /** + * Draws the amount text on minimap orbs. For example HP number on the HP minimap orb + * + * @param graphics graphics to draw on + * @param pos start position + * @param amount number amount + */ + private static void drawOrbAmount(Graphics2D graphics, Point pos, int amount) + { + Color startColor = graphics.getColor(); + + Font font = FontManager.getRunescapeSmallFont(); + graphics.setFont(font); + + FontMetrics fm = graphics.getFontMetrics(); + + String numberString = Integer.toString(amount); + Point numberPos = new Point(pos.getX() + 22 - fm.stringWidth(numberString), pos.getY() + 26); + graphics.setColor(Color.black); + graphics.drawString(numberString, numberPos.getX() + 1, numberPos.getY() + 1);//black shadow on text + graphics.setColor(Color.green); + graphics.drawString(numberString, numberPos.getX(), numberPos.getY()); + + graphics.setColor(startColor); + } + + /** + * Renders a orb similar to the health/prayer orbs already in the client. + * This has a slight shadow effect similar to the clients orbs if shadow = true + * + * @param graphics graphics to draw to + * @param pos Top left position of the orb + * @param percent 0.0-1.0 percent of how filled the orb is + * @param diameter diameter of the orb + * @param shadow draw shadow + */ + private static void drawOrbPercent(Graphics2D graphics, Point pos, double percent, int diameter, boolean shadow, boolean enabled) + { + BufferedImage bufferedImage = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB); + Graphics2D bufferedImageGraphics = bufferedImage.createGraphics(); + float[] dist = {0.2f, 1f}; + + Color setColor = graphics.getColor(); + + int r = setColor.getRed(); + int g = setColor.getGreen(); + int b = setColor.getBlue(); + int a = setColor.getAlpha(); + + Paint startPaint = bufferedImageGraphics.getPaint(); + + if (shadow) + { + float darkenMultiplier = 0.4f; + Point2D center = new Point2D.Float(diameter / 2.7f, diameter / 2.7f); + + if (enabled) + { + // Darken and move center to make it look like the orb is pressed down + darkenMultiplier = 0.20f; + center = new Point2D.Float((diameter / 1.8f), (diameter / 1.8f)); + dist = new float[]{0.7f, 1f}; + } + + // Darken the base color to create a shadow effect on the edges of the orb + r *= darkenMultiplier; + g *= darkenMultiplier; + b *= darkenMultiplier; + + Color[] colors = {setColor, new Color(r, g, b, a)}; + + // Divided by 2.7f for a gradient in the top left of the orb + RadialGradientPaint p = new RadialGradientPaint(center, + diameter / 2.0f, dist, colors); + + bufferedImageGraphics.setPaint(p); + + } + else + { + bufferedImageGraphics.setColor(setColor); + } + + bufferedImageGraphics.fillArc(0, 0, diameter, diameter, 0, 360); + if (percent < 1) + { + // Clear the top part of the orb. if we input 90% we need to clear the top 10% + bufferedImageGraphics.setBackground(new Color(255, 255, 255, 0)); + bufferedImageGraphics.clearRect(0, 0, diameter, (int) ((diameter) * (1 - percent))); + } + + graphics.drawImage(bufferedImage, pos.getX(), pos.getY(), null); + + bufferedImageGraphics.setPaint(startPaint); + } + public static void renderMinimapLocation(Graphics2D graphics, Point mini, Color color) { graphics.setColor(color); diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/specorb/minimap_orb_background.png b/runelite-client/src/main/resources/net/runelite/client/plugins/specorb/minimap_orb_background.png new file mode 100644 index 0000000000000000000000000000000000000000..8c94c4b09f7580dab698ff08ec0280a42e9e3212 GIT binary patch literal 2385 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 mQ2_WQSw7;J`aLlre$Q|6M^6-aX&>kS0000