From bed6c919b0c049b3417e208accba4526d1601a1e Mon Sep 17 00:00:00 2001 From: James <38226001+james-munson@users.noreply.github.com> Date: Mon, 29 Apr 2019 13:23:52 -0700 Subject: [PATCH] Added xp tracker to overlay (#147) * Added a split component to be able to put layout elements above eachother or next to eachother easily * Modified xp tracker plugin to allow infoboxes to be added to canvas * Formatted experience numbers using StackFormatter * Cleaned up swing code as a suggestion in discord to use string states * Forgot to initialize the menu item with the ADD_STATE * Added final back to popupMenu * Extracted duplicate enum Orientation from PanelComponent and SplitComponent into a seperate class named ComponentOrientation. Also changed the plugins that used the previous PanelComponent.Orientation to use ComponentOrientation * Syntax and code convention fixes from deathbeams review * Fixed a bug where logging into an other account did not reset the tracker overlay * Removed useless methods and fixed some code convention issues * fix * fix * fix2 * fix3 --- .../java/net/runelite/api/VarClientStr.java | 1 + .../main/java/net/runelite/api/VarPlayer.java | 1 + .../blastmine/BlastMineOreCountOverlay.java | 4 +- .../plugins/cerberus/CerberusOverlay.java | 14 +-- .../InventoryViewerOverlay.java | 20 ++-- .../client/plugins/xptracker/XpInfoBox.java | 19 +++ .../plugins/xptracker/XpInfoBoxOverlay.java | 109 ++++++++++++++++++ .../plugins/xptracker/XpTrackerPlugin.java | 39 ++++++- .../components/ComponentOrientation.java | 31 +++++ .../ui/overlay/components/PanelComponent.java | 18 +-- .../ui/overlay/components/SplitComponent.java | 96 +++++++++++++++ .../plugins/zulrah/protect_from_magic.png | Bin 0 -> 254 bytes .../plugins/zulrah/protect_from_missiles.png | Bin 0 -> 220 bytes .../client/plugins/zulrah/zulrah_magic.png | Bin 0 -> 6400 bytes .../client/plugins/zulrah/zulrah_melee.png | Bin 0 -> 6867 bytes .../client/plugins/zulrah/zulrah_range.png | Bin 0 -> 6314 bytes 16 files changed, 322 insertions(+), 30 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/components/ComponentOrientation.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/components/SplitComponent.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_magic.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/protect_from_missiles.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_magic.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_melee.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/zulrah/zulrah_range.png 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 0000000000000000000000000000000000000000..b71e1d395f683ca6150ae39b926c29d074e1555a GIT binary patch literal 254 zcmV8GPN`dnpChx$8M{uAowI;xEI0b!SxPD#W9s{jB107*qoM6N<$ Eg4{1=YybcN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..210e0ff6d639b57de416f970b83b1573e2abd54b GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gji#=T&Ln>}1CoEtvFf#aayiIZi zV?x{7lD&Z)Rv&8D{;EOR4Kragx$ZHBpPTvlJLQZY}t? UbQRlWpmP~KUHx3vIVCg!02zE#GXMYp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6367f6f8304bfcacfc39b712cdf44e2b66e2f270 GIT binary patch literal 6400 zcmV+b8UN;qP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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