diff --git a/runelite-api/src/main/java/net/runelite/api/VarClientStr.java b/runelite-api/src/main/java/net/runelite/api/VarClientStr.java index 9667c56e74..79cfe1982b 100644 --- a/runelite-api/src/main/java/net/runelite/api/VarClientStr.java +++ b/runelite-api/src/main/java/net/runelite/api/VarClientStr.java @@ -34,6 +34,7 @@ import lombok.Getter; @Getter public enum VarClientStr { + DUEL_OPPONENT_NAME(357), CHATBOX_TYPED_TEXT(335), INPUT_TEXT(359), PRIVATE_MESSAGE_TARGET(360), diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java index ccee29f4b8..72c361c27b 100644 --- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java +++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java @@ -34,6 +34,7 @@ import lombok.Getter; @Getter public enum VarPlayer { + DUEL_PENDING(286), ATTACK_STYLE(43), QUEST_POINTS(101), IS_POISONED(102), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java index d4566f89c8..53818a250e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastmine/BlastMineOreCountOverlay.java @@ -36,9 +36,9 @@ import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.game.ItemManager; import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; import net.runelite.client.ui.overlay.OverlayMenuEntry; -import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.components.ImageComponent; import net.runelite.client.ui.overlay.components.PanelComponent; @@ -70,7 +70,7 @@ class BlastMineOreCountOverlay extends Overlay { return null; } - + panelComponent.getChildren().clear(); if (config.showOreOverlay()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cerberus/CerberusOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/cerberus/CerberusOverlay.java index 5ddbf21f7a..43c16aee10 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/cerberus/CerberusOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cerberus/CerberusOverlay.java @@ -62,13 +62,13 @@ public class CerberusOverlay extends Overlay // Ghosts are already sorted plugin.getGhosts().stream() - // Iterate only through the correct amount of ghosts - .limit(CerberusGhost.values().length) - .forEach(npc -> CerberusGhost - .fromNPC(npc) - .ifPresent(ghost -> panelComponent - .getChildren() - .add(new ImageComponent(iconManager.getSkillImage(ghost.getType()))))); + // Iterate only through the correct amount of ghosts + .limit(CerberusGhost.values().length) + .forEach(npc -> CerberusGhost + .fromNPC(npc) + .ifPresent(ghost -> panelComponent + .getChildren() + .add(new ImageComponent(iconManager.getSkillImage(ghost.getType()))))); return panelComponent.render(graphics); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/inventoryviewer/InventoryViewerOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/inventoryviewer/InventoryViewerOverlay.java index d66718f4c0..6f322fd6b6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/inventoryviewer/InventoryViewerOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/inventoryviewer/InventoryViewerOverlay.java @@ -72,18 +72,18 @@ class InventoryViewerOverlay extends Overlay inventoryComponent.setOrientation(PanelComponent.Orientation.HORIZONTAL); inventoryComponent.setBackgroundColor(null); inventoryComponent.setBorder(new Rectangle( - 0, - ComponentConstants.STANDARD_BORDER, - 0, - ComponentConstants.STANDARD_BORDER)); + 0, + ComponentConstants.STANDARD_BORDER, + 0, + ComponentConstants.STANDARD_BORDER)); wrapperComponent.setOrientation(PanelComponent.Orientation.VERTICAL); wrapperComponent.setWrapping(2); wrapperComponent.setBorder(new Rectangle( - ComponentConstants.STANDARD_BORDER * 2, - ComponentConstants.STANDARD_BORDER, - ComponentConstants.STANDARD_BORDER * 2, - ComponentConstants.STANDARD_BORDER)); + ComponentConstants.STANDARD_BORDER * 2, + ComponentConstants.STANDARD_BORDER, + ComponentConstants.STANDARD_BORDER * 2, + ComponentConstants.STANDARD_BORDER)); this.itemManager = itemManager; this.client = client; @@ -94,7 +94,7 @@ class InventoryViewerOverlay extends Overlay public Dimension render(Graphics2D graphics) { if (config.hideWhenInvOpen() - && client.getVar(VarClientInt.PLAYER_INVENTORY_OPENED) == 3) + && client.getVar(VarClientInt.PLAYER_INVENTORY_OPENED) == 3) { return null; } @@ -188,4 +188,4 @@ class InventoryViewerOverlay extends Overlay ItemComposition itemComposition = itemManager.getItemComposition(item.getId()); return itemManager.getImage(item.getId(), item.getQuantity(), itemComposition.isStackable()); } -} +} \ 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 index ccd2274375..d688779b2f 100644 --- 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 @@ -57,6 +57,8 @@ import net.runelite.client.util.StackFormatter; class XpInfoBox extends JPanel { + private static final String REMOVE_STATE = "Remove from canvas"; + private static final String ADD_STATE = "Add to canvas"; private static final DecimalFormat TWO_DECIMAL_FORMAT = new DecimalFormat("0.00"); // Templates @@ -91,6 +93,7 @@ class XpInfoBox extends JPanel private final JMenuItem pauseSkill = new JMenuItem("Pause"); private final XpTrackerConfig xpTrackerConfig; + private final JMenuItem canvasItem = new JMenuItem(ADD_STATE); private boolean paused = false; @@ -128,6 +131,21 @@ class XpInfoBox extends JPanel popupMenu.add(reset); popupMenu.add(resetOthers); popupMenu.add(pauseSkill); + popupMenu.add(canvasItem); + + canvasItem.addActionListener(e -> + { + if (canvasItem.getText().equals(REMOVE_STATE)) + { + xpTrackerPlugin.removeOverlay(skill); + canvasItem.setText(ADD_STATE); + } + else + { + xpTrackerPlugin.addOverlay(skill); + canvasItem.setText(REMOVE_STATE); + } + }); JLabel skillIcon = new JLabel(new ImageIcon(iconManager.getSkillImage(skill))); skillIcon.setHorizontalAlignment(SwingConstants.CENTER); @@ -177,6 +195,7 @@ class XpInfoBox extends JPanel void reset() { + canvasItem.setText(ADD_STATE); container.remove(statsPanel); panel.remove(this); panel.revalidate(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java new file mode 100644 index 0000000000..0e16fd118b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018, Jasper Ketelaar + * 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.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.api.Skill; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.overlay.Overlay; +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.StackFormatter; + +class XpInfoBoxOverlay extends Overlay +{ + private static final int PANEL_PREFERRED_WIDTH = 155; + private static final int BORDER_SIZE = 7; + private static final int GAP_SIZE = 5; + + private final PanelComponent panel = new PanelComponent(); + private final XpTrackerPlugin plugin; + + @Getter(AccessLevel.PACKAGE) + private final Skill skill; + private final BufferedImage icon; + + XpInfoBoxOverlay(XpTrackerPlugin plugin, Skill skill, BufferedImage icon) + { + this.plugin = plugin; + this.skill = skill; + this.icon = icon; + panel.setBorder(new Rectangle(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE)); + panel.setGap(new Point(0, GAP_SIZE)); + panel.setPreferredSize(new Dimension(PANEL_PREFERRED_WIDTH, 0)); + } + + @Override + public Dimension render(Graphics2D graphics) + { + //Setting the font to rs small font so that the overlay isn't huge + graphics.setFont(FontManager.getRunescapeSmallFont()); + + final XpSnapshotSingle snapshot = plugin.getSkillSnapshot(skill); + panel.getChildren().clear(); + + final LineComponent xpLeft = LineComponent.builder() + .left("Xp Gained:") + .right(StackFormatter.quantityToRSDecimalStack(snapshot.getXpGainedInSession())) + .build(); + + final LineComponent xpHour = LineComponent.builder() + .left("Xp/Hour:") + .right(StackFormatter.quantityToRSDecimalStack(snapshot.getXpPerHour())) + .build(); + + final SplitComponent xpSplit = SplitComponent.builder() + .first(xpLeft) + .second(xpHour) + .orientation(ComponentOrientation.VERTICAL) + .build(); + + final ImageComponent imageComponent = new ImageComponent(icon); + final SplitComponent iconSplit = SplitComponent.builder() + .first(imageComponent) + .second(xpSplit) + .orientation(ComponentOrientation.HORIZONTAL) + .gap(new Point(GAP_SIZE, 0)) + .build(); + + final ProgressBarComponent progressBarComponent = new ProgressBarComponent(); + progressBarComponent.setValue(snapshot.getSkillProgressToGoal()); + + panel.getChildren().add(iconSplit); + panel.getChildren().add(progressBarComponent); + + return panel.render(graphics); + } +} 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 a8d65962a5..e4c369b01e 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 @@ -60,6 +60,10 @@ import net.runelite.client.task.Schedule; import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.ImageUtil; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldClient; +import net.runelite.http.api.worlds.WorldResult; import net.runelite.http.api.xp.XpClient; @PluginDescriptor( @@ -83,6 +87,9 @@ public class XpTrackerPlugin extends Plugin Skill.HITPOINTS, Skill.MAGIC); + private final XpState xpState = new XpState(); + private final XpClient xpClient = new XpClient(); + @Inject private ClientToolbar clientToolbar; @@ -98,16 +105,18 @@ public class XpTrackerPlugin extends Plugin @Inject private NPCManager npcManager; + @Inject + private OverlayManager overlayManager; + private NavigationButton navButton; private XpPanel xpPanel; + private WorldResult worlds; private XpWorldType lastWorldType; private String lastUsername; private long lastTickMillis = 0; private boolean fetchXp; private long lastXp = 0; - private final XpClient xpClient = new XpClient(); - private final XpState xpState = new XpState(); private final XpPauseState xpPauseState = new XpPauseState(); @Provides @@ -208,6 +217,27 @@ public class XpTrackerPlugin extends Plugin return xpType; } + /** + * Adds an overlay to the canvas for tracking a specific skill. + * + * @param skill the skill for which the overlay should be added + */ + void addOverlay(Skill skill) + { + removeOverlay(skill); + overlayManager.add(new XpInfoBoxOverlay(this, skill, skillIconManager.getSkillImage(skill))); + } + + /** + * Removes an overlay from the overlayManager if it's present. + * + * @param skill the skill for which the overlay should be removed. + */ + void removeOverlay(Skill skill) + { + overlayManager.removeIf(e -> e instanceof XpInfoBoxOverlay && ((XpInfoBoxOverlay) e).getSkill() == skill); + } + /** * Reset internal state and re-initialize all skills with XP currently cached by the RS client * This is called by the user manually clicking resetSkillState in the UI. @@ -230,6 +260,7 @@ public class XpTrackerPlugin extends Plugin } xpState.initializeSkill(skill, currentXp); + removeOverlay(skill); } } @@ -242,6 +273,7 @@ public class XpTrackerPlugin extends Plugin xpState.reset(); xpPanel.resetAllInfoBoxes(); xpPanel.updateTotal(new XpSnapshotSingle.XpSnapshotSingleBuilder().build()); + overlayManager.removeIf(e -> e instanceof XpInfoBoxOverlay); } /** @@ -254,6 +286,8 @@ public class XpTrackerPlugin extends Plugin int currentXp = client.getSkillExperience(skill); xpState.resetSkill(skill, currentXp); xpPanel.resetSkill(skill); + xpPanel.updateTotal(xpState.getTotalSnapshot()); + removeOverlay(skill); } /** @@ -268,6 +302,7 @@ public class XpTrackerPlugin extends Plugin if (skill != s && s != Skill.OVERALL) { resetSkillState(s); + removeOverlay(s); } } } diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/ComponentOrientation.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/ComponentOrientation.java new file mode 100644 index 0000000000..6e60700781 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/ComponentOrientation.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018, Jasper Ketelaar + * 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.ui.overlay.components; + +public enum ComponentOrientation +{ + HORIZONTAL, + VERTICAL +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/PanelComponent.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/PanelComponent.java index 5ea81f2006..8fc7ce0faf 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/PanelComponent.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/PanelComponent.java @@ -67,10 +67,10 @@ public class PanelComponent implements LayoutableRenderableEntity @Setter private Rectangle border = new Rectangle( - ComponentConstants.STANDARD_BORDER, - ComponentConstants.STANDARD_BORDER, - ComponentConstants.STANDARD_BORDER, - ComponentConstants.STANDARD_BORDER); + ComponentConstants.STANDARD_BORDER, + ComponentConstants.STANDARD_BORDER, + ComponentConstants.STANDARD_BORDER, + ComponentConstants.STANDARD_BORDER); @Setter private Point gap = new Point(0, 0); @@ -87,8 +87,8 @@ public class PanelComponent implements LayoutableRenderableEntity // Calculate panel dimension final Dimension dimension = new Dimension( - border.x + childDimensions.width + border.width, - border.y + childDimensions.height + border.height); + border.x + childDimensions.width + border.width, + border.y + childDimensions.height + border.height); // Render background if (backgroundColor != null) @@ -109,8 +109,8 @@ public class PanelComponent implements LayoutableRenderableEntity // Create child preferred size final Dimension childPreferredSize = new Dimension( - preferredSize.width - border.x - border.width, - preferredSize.height - border.y - border.height); + preferredSize.width - border.x - border.width, + preferredSize.height - border.y - border.height); // Calculate max width/height for infoboxes int totalHeight = 0; @@ -180,4 +180,4 @@ public class PanelComponent implements LayoutableRenderableEntity bounds.setSize(dimension); return dimension; } -} +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/SplitComponent.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/SplitComponent.java new file mode 100644 index 0000000000..9ad95cf46a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/SplitComponent.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018, Jasper Ketelaar + * 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.ui.overlay.components; + +import java.awt.*; + +import lombok.Builder; +import lombok.Setter; + +@Builder +@Setter +public class SplitComponent implements LayoutableRenderableEntity +{ + @Builder.Default + private Point preferredLocation = new Point(); + @Builder.Default + private Dimension preferredSize = new Dimension(ComponentConstants.STANDARD_WIDTH, 0); + @Builder.Default + private ComponentOrientation orientation = ComponentOrientation.VERTICAL; + @Builder.Default + private Point gap = new Point(0, 0); + + private LayoutableRenderableEntity first; + private LayoutableRenderableEntity second; + + @Override + public Dimension render(Graphics2D graphics) + { + graphics.translate(preferredLocation.x, preferredLocation.y); + first.setPreferredSize(preferredSize); + first.setPreferredLocation(new Point(0, 0)); + + final Dimension firstDimenson = first.render(graphics); + int x = 0, y = 0; + + if (orientation == ComponentOrientation.VERTICAL) + { + y = firstDimenson.height + gap.y; + } + else + { + x = firstDimenson.width + gap.x; + } + + second.setPreferredLocation(new Point(x, y)); + // Make the second component fit to whatever size is left after the first component is rendered + second.setPreferredSize(new Dimension(preferredSize.width - x, preferredSize.height - y)); + + // The total width/height need to be determined as they are now always the same as the + // individual width/height (for example image width/height will just be the height of the image + // and not the height of the area the image is in + final Dimension secondDimension = second.render(graphics); + int totalWidth, totalHeight; + + if (orientation == ComponentOrientation.VERTICAL) + { + totalWidth = Math.max(firstDimenson.width, secondDimension.width); + totalHeight = y + secondDimension.height; + } + else + { + totalHeight = Math.max(firstDimenson.height, secondDimension.height); + totalWidth = x + secondDimension.width; + } + + graphics.translate(-preferredLocation.x, -preferredLocation.y); + return new Dimension(totalWidth, totalHeight); + } + + @Override + public Rectangle getBounds() { + return null; + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_magic.png b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_magic.png new file mode 100644 index 0000000000..b71e1d395f Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_magic.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_missiles.png b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_missiles.png new file mode 100644 index 0000000000..210e0ff6d6 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_missiles.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_magic.png b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_magic.png new file mode 100644 index 0000000000..6367f6f830 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_magic.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_melee.png b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_melee.png new file mode 100644 index 0000000000..a41924d22f Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_melee.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_range.png b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_range.png new file mode 100644 index 0000000000..33a932f1ca Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_range.png differ