diff --git a/src/main/java/com/questhelper/ItemCollections.java b/src/main/java/com/questhelper/ItemCollections.java index 93c32b480b..5bc9a6a5a4 100644 --- a/src/main/java/com/questhelper/ItemCollections.java +++ b/src/main/java/com/questhelper/ItemCollections.java @@ -273,9 +273,10 @@ public class ItemCollections @Getter private static final List waterStaff = ImmutableList.of( - ItemID.FIRE_BATTLESTAFF, - ItemID.MYSTIC_FIRE_STAFF, - ItemID.STAFF_OF_FIRE, + ItemID.KODAI_WAND, + ItemID.WATER_BATTLESTAFF, + ItemID.MYSTIC_WATER_STAFF, + ItemID.STAFF_OF_WATER, ItemID.MUD_BATTLESTAFF, ItemID.MYSTIC_MUD_STAFF, ItemID.MIST_BATTLESTAFF, @@ -305,6 +306,42 @@ public class ItemCollections ItemID.MYSTIC_LAVA_STAFF ); + @Getter + private static final List lavaStaff = ImmutableList.of( + ItemID.LAVA_BATTLESTAFF, + ItemID.MYSTIC_LAVA_STAFF + ); + + @Getter + private static final List mudStaff = ImmutableList.of( + ItemID.MUD_BATTLESTAFF, + ItemID.MYSTIC_MUD_STAFF + ); + + @Getter + private static final List steamStaff = ImmutableList.of( + ItemID.STEAM_BATTLESTAFF, + ItemID.MYSTIC_STEAM_STAFF + ); + + @Getter + private static final List smokeStaff = ImmutableList.of( + ItemID.SMOKE_BATTLESTAFF, + ItemID.MYSTIC_SMOKE_STAFF + ); + + @Getter + private static final List mistStaff = ImmutableList.of( + ItemID.MIST_BATTLESTAFF, + ItemID.MYSTIC_MIST_STAFF + ); + + @Getter + private static final List dustStaff = ImmutableList.of( + ItemID.DUST_BATTLESTAFF, + ItemID.MYSTIC_DUST_STAFF + ); + // Potions @Getter diff --git a/src/main/java/com/questhelper/ItemSearch.java b/src/main/java/com/questhelper/ItemSearch.java new file mode 100644 index 0000000000..d52118b381 --- /dev/null +++ b/src/main/java/com/questhelper/ItemSearch.java @@ -0,0 +1,183 @@ +/* + * + * * Copyright (c) 2021 + * * 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 com.questhelper; + +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.requirements.util.InventorySlots; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Stream; +import lombok.experimental.UtilityClass; +import net.runelite.api.Client; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemContainer; + +@UtilityClass +public class ItemSearch +{ + + public boolean hasItemAnywhere(Client client, int itemID) + { + return hasItemOnPlayer(client, itemID) || hasItemInBank(client, itemID); + } + + public boolean hasItemAmountAnywhere(Client client, int itemID, int requiredAmount) + { + return hasItemAmountOnPlayer(client, itemID, requiredAmount) || hasItemAmountInBank(client, itemID, requiredAmount); + } + + public boolean hasItemAmountAnywhere(Client client, int itemID, int requiredAmount, Item[] bankItems) + { + return hasItemAmountOnPlayer(client, itemID, requiredAmount) || hasItemAmountInBank(itemID, requiredAmount, bankItems); + } + + public boolean hasItemOnPlayer(Client client, int itemID) + { + return hasItemInInventory(client, itemID) || hasItemEquipped(client, itemID); + } + + public boolean hasItemAmountOnPlayer(Client client, int itemID, int requiredAmount) + { + return hasItemAmountInInventory(client, itemID, requiredAmount) || hasItemAmountEquipped(client, itemID, requiredAmount); + } + + public boolean hasItemInInventory(Client client, int itemID) + { + return checkItem(client, InventorySlots.INVENTORY_SLOTS, itemID); + } + + public boolean hasItemAmountInInventory(Client client, int itemID, int requiredAmount) + { + return getItemAmountExact(client, InventoryID.INVENTORY, itemID) >= requiredAmount; + } + + public boolean hasItemEquipped(Client client, int itemID) + { + return checkItem(client, InventorySlots.EQUIPMENT_SLOTS, itemID); + } + + public boolean hasItemAmountEquipped(Client client, int itemID, int requiredAmount) + { + return getItemAmountExact(client, InventoryID.EQUIPMENT, itemID) >= requiredAmount; + } + + public boolean hasItemInBank(Client client, int itemID) + { + return checkItem(client, InventorySlots.BANK, itemID); + } + + public boolean hasItemAmountInBank(Client client, int itemID, int requiredAmount) + { + return getItemAmountExact(client, InventoryID.BANK, itemID) >= requiredAmount; + } + + public long getItemCount(Client client, int itemID) + { + return getItemCountOnPlayer(client, itemID) + getItemCountInBank(client, itemID); + } + + public long getItemCountOnPlayer(Client client, int itemID) + { + return getItemAmountExact(client, InventoryID.INVENTORY, itemID) + getItemAmountExact(client, InventoryID.EQUIPMENT, itemID); + } + + public long getItemCountInBank(Client client, int itemID) + { + return getItemAmountExact(client, InventoryID.BANK, itemID); + } + + public boolean hasItemsAnywhere(Client client, ItemRequirement requirement) + { + return hasItemsOnPlayer(client, requirement) || hasItemsInBank(client, requirement); + } + + public boolean hasItemsAnywhere(Client client, ItemRequirement requirement, Item[] bankItems) + { + return hasItemsOnPlayer(client, requirement) || hasItemsInBank(requirement, bankItems); + } + + public boolean hasItemsOnPlayer(Client client, ItemRequirement requirement) + { + return requirement.getAllIds().stream().anyMatch(id -> hasItemAmountOnPlayer(client, id, requirement.getQuantity())); + } + + public boolean hasItemsInBank(Client client, ItemRequirement requirement) + { + return requirement.getAllIds().stream().anyMatch(id -> hasItemAmountInBank(client, id, requirement.getQuantity())); + } + + public boolean hasItemsInBank(ItemRequirement requirement, Item[] items) + { + if (items == null) + { + return false; + } + return Stream.of(items) + .filter(Objects::nonNull) + .filter(i -> i.getId() > -1 && i.getQuantity() > -1) // filter out invalid/empty items + .anyMatch(i -> requirement.getAllIds().contains(i.getId()) && i.getQuantity() >= requirement.getQuantity()); + } + + public boolean hasItemInBank(int itemID, Item[] items) + { + return hasItemsInBank(new ItemRequirement("", itemID), items); + } + + public boolean hasItemAmountInBank(int itemID, int amount, Item[] bankItems) + { + return hasItemsInBank(new ItemRequirement("", itemID, amount), bankItems); + } + + public boolean checkItem(Client client, InventorySlots slot, int itemID) + { + return slot.contains(client, i -> i.getId() == itemID); + } + + public long getItemAmountExact(Client client, InventoryID inventoryID, int itemID) + { + ItemContainer container = client.getItemContainer(inventoryID); + if (container == null) + { + return 0L; + } + return Stream.of(container.getItems()) + .filter(Objects::nonNull) + .filter(item -> item.getId() == itemID) + .mapToLong(Item::getQuantity) + .sum(); + } + + public int findFirstItem(Client client, Collection itemIDs, int amount) + { + return itemIDs.stream() + .filter(id -> hasItemAmountAnywhere(client, id, amount)) + .findFirst() + .orElse(-1); + } +} diff --git a/src/main/java/com/questhelper/QuestHelperConfig.java b/src/main/java/com/questhelper/QuestHelperConfig.java index 794758c1ff..58ae830eaf 100644 --- a/src/main/java/com/questhelper/QuestHelperConfig.java +++ b/src/main/java/com/questhelper/QuestHelperConfig.java @@ -27,6 +27,7 @@ import com.questhelper.panel.questorders.QuestOrders; import com.questhelper.questhelpers.Quest; import com.questhelper.questhelpers.QuestHelper; +import com.questhelper.spells.SpellComponentPreference; import java.awt.Color; import java.util.Collection; import java.util.Comparator; @@ -147,7 +148,19 @@ default boolean showOverlay() { return true; } - + + + @ConfigItem( + keyName = "bankSearchSpellPreference", + name = "Spell Component Preference", + description = "Choose whether runes or staves should be preferred when filtering spell components.", + hidden = true + ) + default SpellComponentPreference bankFilterSpellPreference() + { + return SpellComponentPreference.RUNES; + } + @ConfigSection( position = 1, name = "Colors", diff --git a/src/main/java/com/questhelper/QuestHelperPlugin.java b/src/main/java/com/questhelper/QuestHelperPlugin.java index d03714f18f..04750b5a9d 100644 --- a/src/main/java/com/questhelper/QuestHelperPlugin.java +++ b/src/main/java/com/questhelper/QuestHelperPlugin.java @@ -135,6 +135,7 @@ public class QuestHelperPlugin extends Plugin private static final Zone PHOENIX_START_ZONE = new Zone(new WorldPoint(3204, 3488, 0), new WorldPoint(3221, 3501, 0)); + @Getter private final BankItems bankItems = new BankItems(); @Getter @@ -311,6 +312,7 @@ public void onItemContainerChanged(ItemContainerChanged event) { bankItems.setItems(null); bankItems.setItems(event.getItemContainer().getItems()); + clientThread.invokeLater(() -> panel.updateItemRequirements(client, bankItems)); } if (event.getItemContainer() == client.getItemContainer(InventoryID.INVENTORY)) { diff --git a/src/main/java/com/questhelper/banktab/BankItemHolder.java b/src/main/java/com/questhelper/banktab/BankItemHolder.java new file mode 100644 index 0000000000..5a8081008c --- /dev/null +++ b/src/main/java/com/questhelper/banktab/BankItemHolder.java @@ -0,0 +1,54 @@ +/* + * + * * Copyright (c) 2021, Senmori + * * 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 com.questhelper.banktab; + +import com.questhelper.BankItems; +import com.questhelper.QuestHelperConfig; +import com.questhelper.QuestHelperPlugin; +import com.questhelper.requirements.item.ItemRequirement; +import java.util.List; +import net.runelite.api.Client; + +/** + * Represents anything that holds {@link ItemRequirement}s that are to be used + * for displaying via the {@link QuestBankTab}. + *
+ * Most requirements will not need this interface, however this interface does allow + * that requirement to specify which {@link ItemRequirement} should be displayed + * via the quest bank tab. + */ +public interface BankItemHolder +{ + /** + * Get a list of {@link ItemRequirement} to be displayed. + * + * @param client the {@link Client} + * @param plugin + * @return a list of {@link ItemRequirement} that should be displayed, or an empty list if none are found + */ + List getRequirements(Client client, QuestHelperPlugin plugin); +} diff --git a/src/main/java/com/questhelper/banktab/QuestBankTab.java b/src/main/java/com/questhelper/banktab/QuestBankTab.java index d6d8b4aef0..1ed9249b8a 100644 --- a/src/main/java/com/questhelper/banktab/QuestBankTab.java +++ b/src/main/java/com/questhelper/banktab/QuestBankTab.java @@ -30,7 +30,11 @@ import com.google.common.primitives.Shorts; import com.questhelper.QuestHelperPlugin; import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; import java.awt.Point; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,6 +47,7 @@ import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.FontID; +import net.runelite.api.FontTypeFace; import net.runelite.api.ItemID; import net.runelite.api.ScriptEvent; import net.runelite.api.ScriptID; @@ -64,6 +69,7 @@ import net.runelite.client.chat.ChatMessageBuilder; import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.FontType; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.game.ItemManager; import net.runelite.client.game.ItemVariationMapping; @@ -333,6 +339,12 @@ private int addPluginTabSection(Widget itemContainer, List items, L return 0; } + // Get FontMetrics so we can accurately get font height/width + BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + Graphics graphics = img.getGraphics(); + FontMetrics fm = graphics.getFontMetrics(FontType.SMALL.getFont()); + img = null; + graphics = null; for (BankTabItem bankTabItem : items) { boolean foundItem = false; @@ -356,21 +368,22 @@ private int addPluginTabSection(Widget itemContainer, List items, L if (bankTabItem.getQuantity() > 0) { String quantityString = QuantityFormatter.quantityToStackSize(bankTabItem.getQuantity()); - int extraLength = - QuantityFormatter.quantityToStackSize(widget.getItemQuantity()).length() * 6; - int requirementLength = quantityString.length() * 6; + int itemStackSizeLength = fm.stringWidth(QuantityFormatter.quantityToStackSize(widget.getItemQuantity())); + int requirementLength = fm.stringWidth(quantityString); - int xPos = point.x + 2 + extraLength; + int xPos = point.x + 2 + itemStackSizeLength; int yPos = point.y - 1; - if (extraLength + requirementLength > 24) - { + if (itemStackSizeLength + requirementLength > 24) + { // put text on next line xPos = point.x; - yPos = point.y + 9; + yPos = point.y + fm.getHeight(); } + Color color = questHelper.getConfig().textHighlightColor(); + addedWidgets.add(createText(itemContainer, "/ " + quantityString, - Color.WHITE.getRGB(), + color.getRGB(), ITEM_HORIZONTAL_SPACING, TEXT_HEIGHT - 3, xPos, @@ -400,18 +413,19 @@ private int addPluginTabSection(Widget itemContainer, List items, L if (bankTabItem.getQuantity() > 0) { String quantityString = QuantityFormatter.quantityToStackSize(bankTabItem.getQuantity()); - int requirementLength = quantityString.length() * 5; + int requirementLength = fm.stringWidth(quantityString); int xPos = adjXOffset + 8; int yPos = adjYOffset - 1; if (requirementLength > 20) - { + { // put text on next line xPos = adjXOffset; - yPos = adjYOffset + 9; + yPos = adjYOffset + fm.getHeight(); } + Color color = questHelper.getConfig().textHighlightColor(); addedWidgets.add(createText(itemContainer, "/ " + quantityString, - Color.WHITE.getRGB(), + color.getRGB(), ITEM_HORIZONTAL_SPACING, TEXT_HEIGHT - 3, xPos, diff --git a/src/main/java/com/questhelper/banktab/QuestHelperBankTagService.java b/src/main/java/com/questhelper/banktab/QuestHelperBankTagService.java index 7e6babe3bf..3bb2b330e4 100644 --- a/src/main/java/com/questhelper/banktab/QuestHelperBankTagService.java +++ b/src/main/java/com/questhelper/banktab/QuestHelperBankTagService.java @@ -24,6 +24,7 @@ */ package com.questhelper.banktab; +import com.questhelper.ItemSearch; import com.questhelper.QuestHelperPlugin; import com.questhelper.panel.PanelDetails; import com.questhelper.requirements.item.ItemRequirement; @@ -31,12 +32,12 @@ import com.questhelper.requirements.util.LogicType; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import javax.inject.Inject; -import net.runelite.api.InventoryID; -import net.runelite.api.ItemContainer; +import javax.swing.SwingUtilities; public class QuestHelperBankTagService { @@ -127,15 +128,33 @@ private void getItemsFromRequirement(BankTabItems pluginItems, ItemRequirement i getItemsFromRequirement(pluginItems, match); } } + else if (itemRequirement instanceof BankItemHolder) + { + BankItemHolder holder = (BankItemHolder) itemRequirement; + // Force run on client thread even though it's not as responsive as not doing that, however it + // ensures we run on the client thread and never run into threading issues. + plugin.getClientThread().invoke(() -> { + List reqs = holder.getRequirements(plugin.getClient(), plugin); + makeBankHolderItems(reqs, pluginItems); // callback because we can't halt on the client thread); + }); + } else { - if (itemRequirement.getDisplayItemId() != null) + makeBankHolderItems(Collections.singletonList(itemRequirement), pluginItems); + } + } + + private void makeBankHolderItems(List requirements, BankTabItems pluginItems) + { + for (ItemRequirement req : requirements) + { + if (req.getDisplayItemId() != null) { - pluginItems.addItems(new BankTabItem(itemRequirement)); + pluginItems.addItems(new BankTabItem(req)); } - else if (!itemRequirement.getDisplayItemIds().contains(-1)) + else if (!req.getDisplayItemIds().contains(-1)) { - pluginItems.addItems(makeBankTabItem(itemRequirement)); + pluginItems.addItems(makeBankTabItem(req)); } } } @@ -151,12 +170,6 @@ private BankTabItem makeBankTabItem(ItemRequirement item) public boolean hasItemInBank(int itemID) { - ItemContainer bankContainer = plugin.getClient().getItemContainer(InventoryID.BANK); - if (bankContainer == null) - { - return false; - } - - return bankContainer.contains(itemID); + return ItemSearch.hasItemInBank(itemID, plugin.getBankItems().getItems()); } } diff --git a/src/main/java/com/questhelper/panel/QuestOverviewPanel.java b/src/main/java/com/questhelper/panel/QuestOverviewPanel.java index 38f0a6dd6a..8ceb82cafe 100644 --- a/src/main/java/com/questhelper/panel/QuestOverviewPanel.java +++ b/src/main/java/com/questhelper/panel/QuestOverviewPanel.java @@ -506,6 +506,8 @@ public void updateRequirementPanels(Client client, List r { newColor = itemRequirement.getColorConsideringBank(client, false, bankItems.getItems()); } + String updatedTooltip = itemRequirement.getUpdatedTooltip(client); + requirementPanel.setInfoButtonTooltip(updatedTooltip); } else { diff --git a/src/main/java/com/questhelper/panel/QuestRequirementPanel.java b/src/main/java/com/questhelper/panel/QuestRequirementPanel.java index a444f22ac8..c7f824c325 100644 --- a/src/main/java/com/questhelper/panel/QuestRequirementPanel.java +++ b/src/main/java/com/questhelper/panel/QuestRequirementPanel.java @@ -31,6 +31,7 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Insets; +import javax.annotation.Nullable; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; @@ -38,6 +39,7 @@ import javax.swing.border.EmptyBorder; import lombok.Getter; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; public class QuestRequirementPanel extends JPanel { @@ -51,6 +53,10 @@ public class QuestRequirementPanel extends JPanel @Getter private final Requirement requirement; + @Nullable + @Getter + private JButton infoButton; + public QuestRequirementPanel(Requirement requirement) { this.requirement = requirement; @@ -84,20 +90,37 @@ public QuestRequirementPanel(Requirement requirement) } } + public void setInfoButtonTooltip(String text) + { + if (infoButton != null) + { + if (StringUtils.isBlank(text)) + { + infoButton.setToolTipText(""); + infoButton.setVisible(false); + } + else + { + String html1 = ""; + String html2 = ""; + text = text.replaceAll("\\n", "
"); + infoButton.setToolTipText(html1 + text + html2); + } + } + } + private void addButtonToPanel(String tooltipText) { - String html1 = ""; - String html2 = ""; - tooltipText = tooltipText.replaceAll("\\n", "
"); - JButton b = new JButton(INFO_ICON); - b.setPreferredSize(new Dimension(10, 10)); - b.setToolTipText(html1 + tooltipText + html2); - b.setBorderPainted(false); - b.setFocusPainted(false); - b.setBorderPainted(false); - b.setContentAreaFilled(false); - b.setMargin(new Insets(0, 0, 0, 0)); - add(b); + + infoButton = new JButton(INFO_ICON); + infoButton.setPreferredSize(new Dimension(10, 10)); + setInfoButtonTooltip(tooltipText); + infoButton.setBorderPainted(false); + infoButton.setFocusPainted(false); + infoButton.setBorderPainted(false); + infoButton.setContentAreaFilled(false); + infoButton.setMargin(new Insets(0, 0, 0, 0)); + add(infoButton); } } diff --git a/src/main/java/com/questhelper/questhelpers/QuestUtil.java b/src/main/java/com/questhelper/questhelpers/QuestUtil.java index f9adde4873..98f9d3ee42 100644 --- a/src/main/java/com/questhelper/questhelpers/QuestUtil.java +++ b/src/main/java/com/questhelper/questhelpers/QuestUtil.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; public class QuestUtil @@ -45,4 +46,27 @@ public static List toArrayList(@Nonnull T... elements) { return Collectors.toCollection(ArrayList::new); } + + /** + * Removes all the duplicate elements from a stream and collects them into a + * mutable ArrayList + * + * @param stream stream to remove duplicates from + * @return a mutable list containing all the remaining elements of the stream + */ + public static List collectAndRemoveDuplicates(Stream stream) + { + return stream.distinct().collect(collectToArrayList()); + } + + /** + * Remove duplicates in the given list. + * + * @param list the list + * @return a mutable list without duplicates. + */ + public static List removeDuplicates(List list) + { + return collectAndRemoveDuplicates(list.stream()); + } } diff --git a/src/main/java/com/questhelper/requirements/AbstractRequirement.java b/src/main/java/com/questhelper/requirements/AbstractRequirement.java index 543afa29da..9a81f43681 100644 --- a/src/main/java/com/questhelper/requirements/AbstractRequirement.java +++ b/src/main/java/com/questhelper/requirements/AbstractRequirement.java @@ -24,6 +24,7 @@ */ package com.questhelper.requirements; +import com.questhelper.QuestHelperPlugin; import java.util.List; import javax.annotation.Nullable; import net.runelite.api.Client; @@ -52,18 +53,18 @@ public void setTooltip(String tooltip) } @Override - public List getDisplayTextWithChecks(Client client) + public List getDisplayTextWithChecks(Client client, QuestHelperPlugin plugin) { if (getOverlayReplacement() != null && !this.check(client)) { - return getOverlayReplacement().getDisplayTextWithChecks(client); + return getOverlayReplacement().getDisplayTextWithChecks(client, plugin); } - return getOverlayDisplayText(client); + return getOverlayDisplayText(client, plugin); } - protected List getOverlayDisplayText(Client client) + protected List getOverlayDisplayText(Client client, QuestHelperPlugin plugin) { - return Requirement.super.getDisplayTextWithChecks(client); + return Requirement.super.getDisplayTextWithChecks(client, plugin); } public void appendToTooltip(String text) diff --git a/src/main/java/com/questhelper/requirements/Requirement.java b/src/main/java/com/questhelper/requirements/Requirement.java index 88a6172c39..3bf21839fe 100644 --- a/src/main/java/com/questhelper/requirements/Requirement.java +++ b/src/main/java/com/questhelper/requirements/Requirement.java @@ -27,6 +27,7 @@ package com.questhelper.requirements; +import com.questhelper.QuestHelperPlugin; import java.awt.Color; import java.util.ArrayList; import java.util.List; @@ -87,7 +88,7 @@ default String getTooltip() */ default void setTooltip(@Nullable String tooltip) {} - default List getDisplayTextWithChecks(Client client) + default List getDisplayTextWithChecks(Client client, QuestHelperPlugin plugin) { List lines = new ArrayList<>(); diff --git a/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java b/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java index fe0a6de49c..77d523209b 100644 --- a/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java +++ b/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java @@ -28,6 +28,7 @@ import com.questhelper.requirements.util.LogicType; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; import lombok.Getter; import lombok.Setter; import net.runelite.api.Client; @@ -62,9 +63,10 @@ public void updateHandler() .forEach(req -> ((InitializableRequirement) req).updateHandler()); } + @Nonnull @Override public String getDisplayText() // conditions don't need display text (yet?) { - return null; + return ""; } } diff --git a/src/main/java/com/questhelper/requirements/item/ItemRequirement.java b/src/main/java/com/questhelper/requirements/item/ItemRequirement.java index 3d3dd17f27..36dd968584 100644 --- a/src/main/java/com/questhelper/requirements/item/ItemRequirement.java +++ b/src/main/java/com/questhelper/requirements/item/ItemRequirement.java @@ -26,6 +26,9 @@ */ package com.questhelper.requirements.item; +import com.questhelper.BankItems; +import com.questhelper.ItemSearch; +import com.questhelper.QuestHelperPlugin; import com.questhelper.requirements.AbstractRequirement; import com.questhelper.requirements.Requirement; import com.questhelper.requirements.util.InventorySlots; @@ -34,15 +37,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; +import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import net.runelite.api.Client; import net.runelite.api.InventoryID; import net.runelite.api.Item; -import net.runelite.api.ItemContainer; import net.runelite.client.ui.overlay.components.LineComponent; public class ItemRequirement extends AbstractRequirement @@ -50,7 +51,8 @@ public class ItemRequirement extends AbstractRequirement @Getter private final int id; - private final String name; + @Setter + private String name; @Setter @Getter @@ -69,6 +71,7 @@ public class ItemRequirement extends AbstractRequirement protected final List alternateItems = new ArrayList<>(); + @Getter @Setter protected boolean exclusiveToOneItemType; @@ -203,7 +206,7 @@ public List getAllIds() } @Override - protected List getOverlayDisplayText(Client client) + public List getOverlayDisplayText(Client client, QuestHelperPlugin plugin) { List lines = new ArrayList<>(); @@ -228,7 +231,7 @@ protected List getOverlayDisplayText(Client client) text.append(this.getName()); } - Color color = getColor(client); + Color color = getColorForOverlay(client, plugin.getBankItems()); lines.add(LineComponent.builder() .left(text.toString()) .leftColor(color) @@ -237,6 +240,27 @@ protected List getOverlayDisplayText(Client client) return lines; } + protected LineComponent getInBankLine() + { + return LineComponent.builder() + .left(" - In Bank") + .leftColor(Color.WHITE) + .build(); + } + + public Color getColorForOverlay(Client client, BankItems bankItems) + { + if (ItemSearch.hasItemsOnPlayer(client, this)) + { + return Color.GREEN; + } + if (ItemSearch.hasItemsInBank(this, bankItems.getItems())) + { + return Color.WHITE; + } + return Color.RED; + } + @Override public String getDisplayText() { @@ -261,7 +285,7 @@ else if (this.check(client)) /** Find the first item that this requirement allows that the player has, or -1 if they don't have any item(s) */ private int findItemID(Client client, boolean checkConsideringSlotRestrictions) { - int remainder = getRequiredItemDifference(client, id, checkConsideringSlotRestrictions, null); + int remainder = getRequiredItemDifference(client, id, checkConsideringSlotRestrictions, false); if (remainder <= 0) { return id; @@ -273,7 +297,7 @@ private int findItemID(Client client, boolean checkConsideringSlotRestrictions) { remainder = quantity; } - remainder -= (quantity - getRequiredItemDifference(client, alternate, checkConsideringSlotRestrictions, null)); + remainder -= (quantity - getRequiredItemDifference(client, alternate, checkConsideringSlotRestrictions, false)); if (remainder <= 0) { return alternate; @@ -289,14 +313,15 @@ public Color getColorConsideringBank(Client client, boolean checkConsideringSlot { color = Color.GRAY; } - else if (this.check(client, checkConsideringSlotRestrictions)) + else if (ItemSearch.hasItemsOnPlayer(client, this)) { color = Color.GREEN; } - if (color == Color.RED && bankItems != null) + if (color == Color.RED) { - if (check(client, false, bankItems)) + boolean hasInCachedBank = (bankItems != null && ItemSearch.hasItemsInBank(this, bankItems)); + if (ItemSearch.hasItemsInBank(client, this) || hasInCachedBank) { color = Color.WHITE; } @@ -346,7 +371,7 @@ public boolean check(Client client, boolean checkConsideringSlotRestrictions, It { remainder = quantity; } - remainder -= (quantity - getRequiredItemDifference(client, alternate, checkConsideringSlotRestrictions, items)); + remainder -= (quantity - getRequiredItemDifference(client, alternate, checkConsideringSlotRestrictions, items != null)); if (remainder <= 0) { return true; @@ -359,47 +384,23 @@ public boolean check(Client client, boolean checkConsideringSlotRestrictions, It * Get the difference between the required quantity for this requirement and the amount the client has. * Any value <= 0 indicates they have the required amount */ - public int getRequiredItemDifference(Client client, int itemID, boolean checkConsideringSlotRestrictions, Item[] items) + public int getRequiredItemDifference(Client client, int itemID, boolean respectSlotRestrictions, boolean checkBank) { - ItemContainer equipped = client.getItemContainer(InventoryID.EQUIPMENT); int tempQuantity = quantity; + tempQuantity -= ItemSearch.getItemAmountExact(client, InventoryID.EQUIPMENT, itemID); - if (equipped != null) + if (!respectSlotRestrictions || !equip) { - tempQuantity -= getNumMatches(equipped, itemID); + tempQuantity -= ItemSearch.getItemAmountExact(client, InventoryID.INVENTORY, itemID); } - if (!checkConsideringSlotRestrictions || !equip) + if (checkBank) { - ItemContainer inventory = client.getItemContainer(InventoryID.INVENTORY); - if (inventory != null) - { - tempQuantity -= getNumMatches(inventory, itemID); - } + tempQuantity -= ItemSearch.getItemAmountExact(client, InventoryID.BANK, itemID); } - - if (items != null) - { - tempQuantity -= getNumMatches(items, itemID); - } - return tempQuantity; } - public int getNumMatches(ItemContainer items, int itemID) - { - return getNumMatches(items.getItems(), itemID); - } - - public int getNumMatches(Item[] items, int itemID) - { - return Stream.of(items) - .filter(Objects::nonNull) // Runelite loves to sneak in null objects - .filter(i -> i.getId() == itemID) - .mapToInt(Item::getQuantity) - .sum(); - } - public boolean check(Client client) { return check(client, false); @@ -425,6 +426,14 @@ public List getDisplayItemIds() return Collections.singletonList(displayItemId); } + @Nullable + public String getUpdatedTooltip(Client client) + { + return getTooltip(); + } + + + public boolean shouldRenderItemHighlights(Client client) { return conditionToHide == null || !conditionToHide.check(client); diff --git a/src/main/java/com/questhelper/requirements/item/ItemRequirements.java b/src/main/java/com/questhelper/requirements/item/ItemRequirements.java index 3034c6eb37..4da87e4390 100644 --- a/src/main/java/com/questhelper/requirements/item/ItemRequirements.java +++ b/src/main/java/com/questhelper/requirements/item/ItemRequirements.java @@ -65,6 +65,18 @@ public ItemRequirements(LogicType logicType, String name, ItemRequirement... ite this.logicType = logicType; } + public ItemRequirements(LogicType logicType, String name, List itemRequirements) + { + super(name, itemRequirements.get(0).getId(), -1); + this.itemRequirements.addAll(itemRequirements); + this.logicType = logicType; + } + + protected ItemRequirements(LogicType logicType, String name) + { + super(name, -1); + } + public ItemRequirements(LogicType logicType, ItemRequirement... requirements) { this(logicType, "", requirements); diff --git a/src/main/java/com/questhelper/requirements/magic/RuneRequirement.java b/src/main/java/com/questhelper/requirements/magic/RuneRequirement.java new file mode 100644 index 0000000000..59fb53271d --- /dev/null +++ b/src/main/java/com/questhelper/requirements/magic/RuneRequirement.java @@ -0,0 +1,157 @@ +/* + * + * * Copyright (c) 2021 + * * 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 com.questhelper.requirements.magic; + +import com.questhelper.ItemSearch; +import com.questhelper.QuestHelperPlugin; +import com.questhelper.banktab.BankItemHolder; +import com.questhelper.questhelpers.QuestUtil; +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.spells.Rune; +import com.questhelper.spells.Staff; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.Item; + +/* + * LOGIC: + * We need to be able to get the first rune/staff that was found in the player's inventory/bank so + * we can display that item as a bank icon. + * + * Prioritize runes over staves because most quests require some sort of combat gear and that would, + * most likely, eliminate stave as an option for most quests. + */ + +/** + * Represents a single rune requirement that is used in {@link SpellRequirement}. + */ +@Getter +public class RuneRequirement extends ItemRequirement implements BankItemHolder +{ + private final Rune rune; + private int costPerCast; + private int requiredAmount; + + private final ItemRequirement runeItemRequirement; + private ItemRequirement staffItemRequirement; + + public RuneRequirement(Rune rune, int costPerCast) + { + this(rune, costPerCast, 1); + } + + public RuneRequirement(Rune rune, int costPerCast, int numberOfCasts) + { + super(rune.getRuneName(), rune.getItemID(), (costPerCast * numberOfCasts)); + this.rune = rune; + this.costPerCast = costPerCast; + this.requiredAmount = costPerCast * numberOfCasts; + this.runeItemRequirement = new ItemRequirement("", rune.getRunes(), this.requiredAmount); + if (rune.getStaff() != Staff.UNKNOWN) + { + this.staffItemRequirement = new ItemRequirement(rune.getStaff().getName(), rune.getStaff().getStaves(), 1); + } + } + + /** + * Set the new number of times the spell will be cast. + * + * @param numberOfCasts the new number of casts + */ + public void setNumberOfCasts(int numberOfCasts) + { + this.requiredAmount = costPerCast * numberOfCasts; + updateRequirements(this.requiredAmount); + } + + private void updateRequirements(int numRunesRequired) + { + this.runeItemRequirement.setQuantity(numRunesRequired); + setQuantity(numRunesRequired); + } + + @Override + public boolean isActualItem() + { + return true; + } + + @Override + public Integer getDisplayItemId() + { + return null; // use our requirements to determine the id to display + } + + @Override + public void setDisplayItemId(Integer displayItemId) + { + // Don't set so we use our requirements to determine what to show in the bank + } + + @Override + public List getAllIds() + { + List ids = new LinkedList<>(runeItemRequirement.getAllIds()); + if (staffItemRequirement != null) + { + ids.addAll(staffItemRequirement.getAllIds()); + } + return QuestUtil.removeDuplicates(ids); + } + + @Override + public boolean checkBank(Client client) + { + boolean hasStaves = (staffItemRequirement != null && ItemSearch.hasItemsInBank(client, staffItemRequirement)); + return ItemSearch.hasItemsInBank(client, runeItemRequirement) || hasStaves; + } + + public boolean checkCachedBank(Item[] items) + { + boolean hasStaves = (staffItemRequirement != null && ItemSearch.hasItemsInBank(this, items)); + return ItemSearch.hasItemsInBank(this, items) || hasStaves; + } + + @Override + public boolean check(Client client, boolean checkWithSlotRestrictions, Item[] items) + { + boolean hasStaves = (staffItemRequirement != null && ItemSearch.hasItemsAnywhere(client, staffItemRequirement)); + return ItemSearch.hasItemsAnywhere(client, runeItemRequirement) || hasStaves; + } + + @Override + public List getRequirements(Client client, QuestHelperPlugin plugin) + { + boolean hasRunes = ItemSearch.hasItemsAnywhere(client, runeItemRequirement); + boolean hasStaves = staffItemRequirement != null && ItemSearch.hasItemsAnywhere(client, staffItemRequirement); + ItemRequirement requirement = plugin.getConfig().bankFilterSpellPreference().getPreference(this, () -> hasRunes, () -> hasStaves); + return Collections.singletonList(requirement); + } +} diff --git a/src/main/java/com/questhelper/requirements/magic/SpellRequirement.java b/src/main/java/com/questhelper/requirements/magic/SpellRequirement.java new file mode 100644 index 0000000000..3debc97123 --- /dev/null +++ b/src/main/java/com/questhelper/requirements/magic/SpellRequirement.java @@ -0,0 +1,675 @@ +/* + * + * * Copyright (c) 2021 + * * 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 com.questhelper.requirements.magic; + +import com.google.common.base.Predicates; +import com.questhelper.ItemSearch; +import com.questhelper.QuestHelperPlugin; +import com.questhelper.banktab.BankItemHolder; +import com.questhelper.questhelpers.QuestUtil; +import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.requirements.player.SkillRequirement; +import com.questhelper.requirements.player.SpellbookRequirement; +import com.questhelper.requirements.quest.QuestRequirement; +import com.questhelper.spells.MagicSpell; +import com.questhelper.spells.Rune; +import com.questhelper.spells.Staff; +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.Item; +import net.runelite.api.ItemComposition; +import net.runelite.api.Skill; +import net.runelite.client.ui.overlay.components.LineComponent; +import org.apache.commons.lang3.StringUtils; + +/* + * LOGIC: + * + * Order of priority for requirements: + * 1. Player has tablet + * 2. Player has runes + * 3. Player has staff + * + * We can ignore the requirements if the player has the tablet because those + * have no requirements in order to be used (other than quests). + */ + +/** + * Represents a spell that can be cast. + * This will check if the user has the required magic level, quest requirements, + * any extra items (i.e. unpowered orb for charge orb spells) and any extra requirements + * that are added. + *
+ * If the player has this spell's tablet, then the only requirement that is checked is the + * quest requirement. This is because player's still cannot use a tablet whose + * spell counterpart is locked behind quest progression. + *
+ * This spell requirements prioritizes using tablets over runes/staves since + * it is more compact and has less requirements. + */ +@Slf4j +public class SpellRequirement extends ItemRequirement implements BankItemHolder +{ + @Getter + private final MagicSpell spell; + + private ItemRequirement tabletRequirement = null; + private ItemRequirement staffRequirement; + private boolean useStaff = true; + + private int numberOfCasts; + + private final Map runeCostMap; + /** @return all {@link Requirement}s on this SpellRequirement */ + private final List requirements = new ArrayList<>(); + private final List runeRequirements = new LinkedList<>(); + + public SpellRequirement(MagicSpell spell, Map runeCostMap, List requirements) + { + this(spell, 1, runeCostMap, requirements); + } + + public SpellRequirement(MagicSpell spell, int numberOfCasts, Map runeCostMap, List requirements) + { + super(spell.getName(), -1, numberOfCasts); + this.spell = spell; + this.numberOfCasts = numberOfCasts; + this.runeCostMap = runeCostMap; + this.requirements.addAll(requirements); + this.requirements.add(new SkillRequirement(Skill.MAGIC, spell.getRequiredMagicLevel())); + this.requirements.add(new SpellbookRequirement(spell.getSpellbook())); + setNumberOfCasts(numberOfCasts); + } + + /** + * Add an additional {@link Requirement}. + * If this is an {@link ItemRequirement}, it's assumed that it's directly needed to cast this spell. + * Thus, it's quantity will be adjusted to match the number of casts this requirement has. + * + * @param requirement requirement to add + */ + public void addRequirement(@Nonnull Requirement requirement) + { + if (requirement instanceof RuneRequirement) + { + runeRequirements.add((RuneRequirement) requirement); + } + else + { + requirements.add(requirement); + } + } + + /** + * Set the tablet's item id. If the item id is less than 0, the tablet will be removed + * and this requirement will no longer check for it. + * + * @param itemID the tablet's item id + */ + public void setTablet(int itemID) + { + if (itemID < 0) + { + this.tabletRequirement = null; + } + else + { + this.tabletRequirement = new ItemRequirement("", itemID, this.numberOfCasts); + } + } + + /** + * Set the new staff item id this requirement should use. + * + * @param staffID the new staff item id. + * + * @throws UnsupportedOperationException if staff use is disabled. + */ + public void setStaff(int staffID) + { + if (!useStaff) + { + throw new UnsupportedOperationException("Cannot require a staff and then require no staff"); + } + if (staffID < 0) + { + this.staffRequirement = null; + } + else + { + this.staffRequirement = new ItemRequirement("", staffID); + doNotUseTablet(); + } + } + + public void setStaff(ItemRequirement staff) + { + if (!useStaff) + { + throw new UnsupportedOperationException("Cannot require a staff and then require no staff"); + } + this.staffRequirement = staff; + doNotUseTablet(); + } + + /** + * @return true if this requirement currently has a staff and if staves are enabled. + */ + public boolean hasStaff() + { + return staffRequirement != null && useStaff && staffRequirement.getId() > -1; + } + + public boolean hasTablet() + { + return tabletRequirement != null && tabletRequirement.getId() > -1; + } + + /** + * A convenience method to better indicate to not use a tablet for this spell requirement + */ + public void doNotUseTablet() + { + setTablet(-1); + } + + /** + * Set if this requirement should use staffs. + * + * @param useStaff true to use staffs. + * + * @throws UnsupportedOperationException if staff use is disabled while there is still a staff required + */ + public void setStaffUse(boolean useStaff) + { + if (!useStaff && staffRequirement != null) + { + throw new UnsupportedOperationException("Cannot require a staff and then require no staff"); + } + this.useStaff = useStaff; + } + + /** + * Set the number of casts this requirement should account for. + * + * @param numberOfCasts the number of casts + */ + public void setNumberOfCasts(int numberOfCasts) + { + this.numberOfCasts = numberOfCasts; + setQuantity(numberOfCasts); + runeRequirements.clear(); + for (Map.Entry entry : runeCostMap.entrySet()) + { + Rune rune = entry.getKey(); + int costPerCast = entry.getValue(); + RuneRequirement runeRequirement = new RuneRequirement(rune, costPerCast, numberOfCasts); + runeRequirements.add(runeRequirement); + } + if (tabletRequirement != null) + { + tabletRequirement.setQuantity(this.numberOfCasts); + } + getItemRequirements(this.requirements).forEach(item -> item.setQuantity(this.numberOfCasts)); + } + + @Override + public boolean isActualItem() + { + return true; + } + + @Override + public boolean showQuantity() + { + return true; + } + + @Override + public Integer getDisplayItemId() + { + return tabletRequirement != null ? tabletRequirement.getId() : (staffRequirement != null ? staffRequirement.getId() : null); + } + + @Override + public void setDisplayItemId(Integer displayItemId) + { + // Don't set so we use our requirements to determine what to show in the bank + } + + @Override + public List getOverlayDisplayText(Client client, QuestHelperPlugin plugin) + { + updateInternalState(client, getItemRequirements(this.requirements)); + List lines = new ArrayList<>(); + + StringBuilder text = new StringBuilder(); + if (this.showQuantity()) + { + text.append(this.numberOfCasts).append(" x "); + } + + String name = spell.getName(); + if (hasTablet()) + { + name = tabletRequirement.getName(); + } + text.append(name); + Color color = getPanelColor(client, plugin.getBankItems().getItems()); + // N x + lines.add(LineComponent.builder() + .left(text.toString()) + .leftColor(color) + .build() + ); + if (hasTablet()) + { + return lines; + } + if (hasStaff()) + { + int firstStaffID = ItemSearch.findFirstItem(client, staffRequirement.getAllIds(), staffRequirement.getQuantity()); + String staffName = staffRequirement.getName(); + if (StringUtils.isBlank(staffName)) + { + staffName = client.getItemDefinition(firstStaffID).getName(); + } + + // + Color staffColor = getStaffColor(client, plugin.getBankItems().getItems()); + lines.add(LineComponent.builder() + .left(staffName) + .leftColor(staffColor) + .build()); + + // Add '(equipped)' + // We have to check for an ID of -1 because if someone submits an invalid staff id then the hasItemEquipped + // will check if the slot has an id of -1, which is empty, so it will return true for an empty slot. + staffColor = firstStaffID < 0 ? Color.RED : (ItemSearch.hasItemEquipped(client, firstStaffID) ? Color.GREEN : Color.RED); + lines.add(LineComponent.builder() + .left("(equipped)") + .leftColor(staffColor) + .build()); + } + return lines; + } + + private Color getPanelColor(Client client, Item[] bankItems) + { + if (hasTablet()) + { + return getPanelColorWithTablet(client, bankItems); + } + if (hasStaff()) + { + return getPanelColorWithStaff(client, bankItems); + } + List itemRequirements = getItemRequirements(this.requirements); + boolean hasRunes, hasItems; + hasRunes = runeRequirements.stream().allMatch(req -> ItemSearch.hasItemsOnPlayer(client, req)); + hasItems = hasItemsOnPlayer(client, itemRequirements); + if (hasRunes && hasItems) + { + return Color.GREEN; + } + // Don't use ItemSearch for RuneRequirement because RuneRequirement overrides checkBank + hasRunes = runeRequirements.stream().allMatch(req -> req.checkCachedBank(bankItems)); + hasItems = hasItemsInBank(itemRequirements, bankItems); + return hasRunes && hasItems ? Color.WHITE : Color.RED; + } + + private Color getPanelColorWithStaff(Client client, Item[] bankItems) + { + List itemRequirements = getItemRequirements(this.requirements); + int staffID = ItemSearch.findFirstItem(client, staffRequirement.getAllIds(), 1); + Staff requiredStaff = Staff.getByItemID(staffID); + if (requiredStaff != Staff.UNKNOWN) + { + List nonStaffRunes = new ArrayList<>(); + for (RuneRequirement rune : runeRequirements) + { + Rune currentRune = rune.getRune(); + ItemRequirement runeItem = rune.getRuneItemRequirement(); + boolean source = requiredStaff.isSourceOf(currentRune); + if (!source) + { + nonStaffRunes.add(runeItem); + } + } + boolean hasRunes = hasItemsOnPlayer(client, nonStaffRunes); + boolean hasItems = hasItemsOnPlayer(client, itemRequirements); + boolean hasStaff = ItemSearch.hasItemOnPlayer(client, staffID); + if (hasRunes && hasItems && hasStaff) + { + return Color.GREEN; + } + hasRunes = hasItemsInBank(nonStaffRunes, bankItems); + hasItems = hasItemsInBank(itemRequirements, bankItems); + hasStaff = ItemSearch.hasItemInBank(staffID, bankItems) || ItemSearch.hasItemOnPlayer(client, staffID); + return hasRunes && hasItems && hasStaff ? Color.WHITE : Color.RED; + } + return Color.RED; + } + + private Color getPanelColorWithTablet(Client client, Item[] bankItems) + { + updateTabletRequirement(client); + int tabletID = tabletRequirement.getId(); + int required = tabletRequirement.getQuantity(); + if (ItemSearch.hasItemAmountOnPlayer(client, tabletID, required)) + { + return Color.GREEN; + } + if (ItemSearch.hasItemsInBank(tabletRequirement, bankItems)) + { + return Color.WHITE; + } + return Color.RED; + } + + + private boolean hasItemsOnPlayer(Client client, Collection requirements) + { + return requirements.stream().allMatch(req -> ItemSearch.hasItemsOnPlayer(client, req)); + } + + private boolean hasItemsInBank(Collection requirements, Item[] bankItems) + { + return requirements.stream().allMatch(req -> ItemSearch.hasItemsInBank(req, bankItems)); + } + + private Color getStaffColor(Client client, Item[] bankItems) + { + Color staffColor = Color.RED; + if (ItemSearch.hasItemsOnPlayer(client, staffRequirement)) + { + staffColor = Color.GREEN; + } + else if (ItemSearch.hasItemsInBank(client, staffRequirement) || ItemSearch.hasItemsInBank(staffRequirement, bankItems)) + { + staffColor = Color.WHITE; + } + return staffColor; + } + + @Override + public List getAllIds() + { + List ids = requirements.stream() + .filter(ItemRequirement.class::isInstance) + .map(ItemRequirement.class::cast) + .map(ItemRequirement::getAllIds) + .flatMap(Collection::stream) + .collect(QuestUtil.collectToArrayList()); + List runeIDs = runeRequirements.stream() + .map(RuneRequirement::getAllIds) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + ids.addAll(runeIDs); + if (tabletRequirement != null && tabletRequirement.getId() >= 0) + { + ids.add(tabletRequirement.getId()); + } + if (staffRequirement != null && staffRequirement.getId() >= 0) + { + ids.add(staffRequirement.getId()); + } + return ids; + } + + @Override + public boolean check(Client client) + { + return this.check(client, false, null); + } + + @Override + public boolean checkBank(Client client) + { + updateInternalState(client, getItemRequirements(this.requirements)); + if (tabletRequirement != null && ItemSearch.hasItemAmountInBank(client, tabletRequirement.getId(), this.numberOfCasts)) + { + return true; + } + boolean hasRunes = runeRequirements.stream().allMatch(req -> req.checkBank(client)); + boolean hasItems = requirements.stream().allMatch(req -> req.check(client)); + boolean hasStaff = !hasStaff(); + if (hasStaff()) + { + int firstStaffID = ItemSearch.findFirstItem(client, staffRequirement.getAllIds(), staffRequirement.getQuantity()); + hasStaff = ItemSearch.hasItemAmountInBank(client, firstStaffID, staffRequirement.getQuantity()); + } + return hasRunes && hasItems && hasStaff; + } + + @Override + public Color getColorConsideringBank(Client client, boolean checkWithSlotRestrictions, Item[] bankItems) + { + if (hasTablet()) + { + Color tabletColor = getPanelColorWithTablet(client, bankItems); + if (tabletColor != Color.RED) + { + return tabletColor; + } + } + boolean hasOtherReqs = getNonItemRequirements(this.requirements).stream().allMatch(req -> req.check(client)); + if (!hasOtherReqs) + { + return Color.RED; // abort early if they can't even cast the spell + } + updateInternalState(client, getItemRequirements(this.requirements)); + return getPanelColor(client, bankItems); + } + + + @Override + public boolean check(Client client, boolean checkWithSlotRestrictions, Item[] items) + { + if (tabletRequirement != null && ItemSearch.hasItemsAnywhere(client, tabletRequirement)) + { + updateTabletRequirement(client); + return true; + } + boolean hasStaff = true; + if (hasStaff()) + { + hasStaff = ItemSearch.hasItemsAnywhere(client, staffRequirement); + } + boolean hasItems, hasOther, hasRunes; + List itemRequirements = getItemRequirements(this.requirements); + updateInternalState(client, itemRequirements); + if (!itemRequirements.isEmpty()) + { + hasItems = itemRequirements.stream().allMatch(req -> req.check(client, checkWithSlotRestrictions, items)); + } + else + { + hasItems = true; + } + hasOther = getNonItemRequirements(this.requirements).stream().allMatch(req -> req.check(client)); + hasRunes = runeRequirements.stream().allMatch(req -> req.check(client, checkWithSlotRestrictions, items)); + return hasItems && hasOther && hasRunes && hasStaff; + } + + @Override + public List getRequirements(Client client, QuestHelperPlugin plugin) + { + updateInternalState(client, getItemRequirements(this.requirements)); + if (tabletRequirement != null && ItemSearch.hasItemsAnywhere(client, tabletRequirement, plugin.getBankItems().getItems())) + { + return Collections.singletonList(tabletRequirement); + } + List bankRequirements = new LinkedList<>(); + Map runeItemRequirements = new HashMap<>(); + if (staffRequirement != null) + { + bankRequirements.add(staffRequirement); + } + for (RuneRequirement rune : runeRequirements) + { + if (runeItemRequirements.containsKey(rune.getRune())) + { + continue; + } + Rune currentRune = rune.getRune(); + ItemRequirement staves = rune.getStaffItemRequirement(); + ItemRequirement runeItem = rune.getRuneItemRequirement(); + + BooleanSupplier hasRunes = () -> ItemSearch.hasItemsAnywhere(client, runeItem, plugin.getBankItems().getItems()); + BooleanSupplier hasStaves = () -> hasStaff(client, staves); + + ItemRequirement toAdd = plugin.getConfig().bankFilterSpellPreference().getPreference(rune, hasRunes, hasStaves); + + boolean itemIsRune = toAdd.getAllIds().stream().allMatch(Rune::isRuneItem); + if (staffRequirement != null && itemIsRune && currentRune.getStaff() != Staff.UNKNOWN) + { + // there is a staff present, can it replace the current rune? + int staffID = ItemSearch.findFirstItem(client, staffRequirement.getAllIds(), 1); + Staff requiredStaff = Staff.getByItemID(staffID); + boolean isSourceOf = requiredStaff.isSourceOf(currentRune); + if (!isSourceOf) + { + runeItemRequirements.put(currentRune, toAdd); + } + } + else + { + runeItemRequirements.put(currentRune, toAdd); + } + } + + bankRequirements.addAll(runeItemRequirements.values()); + bankRequirements.addAll(getItemRequirements(this.requirements)); + bankRequirements.stream() + .filter(req -> StringUtils.isBlank(req.getName())) + .forEach(req -> { + int firstID = ItemSearch.findFirstItem(client, req.getAllIds(), req.getQuantity()); + if (firstID < 0) firstID = req.getId(); + req.setName(client.getItemDefinition(firstID).getName()); + }); + return bankRequirements; + } + + private boolean hasStaff(Client client, ItemRequirement staves) + { + if (staves == null || staffRequirement != null || !useStaff) + { + return false; + } + boolean hasStaff = staves.getAllIds().stream().anyMatch(staffID -> ItemSearch.hasItemAmountAnywhere(client, staffID, 1)); + if (useStaff && hasStaff) + { + staffRequirement = staves; + } + return hasStaff; + } + + @Nullable + @Override + public String getUpdatedTooltip(Client client) + { + StringBuilder text = new StringBuilder(); + if (tabletRequirement != null && ItemSearch.hasItemAnywhere(client, tabletRequirement.getId())) + { // only show tooltip for tablet if they actually have it + AtomicInteger count = new AtomicInteger(); + getNonItemRequirements(this.requirements).stream() + .filter(r -> r instanceof QuestRequirement) + .map(QuestRequirement.class::cast) + .filter(r -> r.check(client)) + .peek(q -> count.incrementAndGet()) + .forEach(q -> text.append(q.getDisplayText())); + return count.get() > 0 ? text.toString() : null; // no requirements to use a tablet + } + getNonItemRequirements(this.requirements).stream() + .filter(req -> !req.getDisplayText().isEmpty()) + .filter(req -> !req.check(client)) + .forEach(req -> text.append(req.getDisplayText()).append("\n")); + if (text.length() <= 0) + { + return text.toString(); + } + return text.insert(0, "This spell requires: \n").toString(); + } + + private List getItemRequirements(List requirements) + { + return requirements.stream() + .filter(ItemRequirement.class::isInstance) + .map(ItemRequirement.class::cast) + .collect(Collectors.toList()); + } + + private List getNonItemRequirements(List requirements) + { + return requirements.stream() + .filter(Predicates.not(ItemRequirement.class::isInstance)) + .collect(Collectors.toList()); + } + + private void updateInternalState(Client client, List requirements) + { + updateItemRequirements(client, requirements); + updateTabletRequirement(client); + if (staffRequirement != null && StringUtils.isBlank(staffRequirement.getName())) + { + staffRequirement.setName(client.getItemDefinition(staffRequirement.getId()).getName()); + } + } + + private void updateItemRequirements(Client client, List requirements) + { + requirements.stream() + .filter(req -> StringUtils.isBlank(req.getName())) + .forEach(req -> req.setName(client.getItemDefinition(req.getId()).getName())); + requirements.forEach(item -> item.setQuantity(this.numberOfCasts)); + } + + private void updateTabletRequirement(Client client) + { + if (tabletRequirement != null && StringUtils.isBlank(tabletRequirement.getName())) + { + ItemComposition tablet = client.getItemDefinition(tabletRequirement.getId()); + tabletRequirement = new ItemRequirement(tablet.getName(), tablet.getId(), this.numberOfCasts); + } + } +} diff --git a/src/main/java/com/questhelper/requirements/quest/QuestRequirement.java b/src/main/java/com/questhelper/requirements/quest/QuestRequirement.java index 50417aae52..44f27c3f6c 100644 --- a/src/main/java/com/questhelper/requirements/quest/QuestRequirement.java +++ b/src/main/java/com/questhelper/requirements/quest/QuestRequirement.java @@ -30,9 +30,11 @@ import com.questhelper.QuestHelperQuest; import com.questhelper.requirements.AbstractRequirement; import java.util.Locale; +import javax.annotation.Nullable; import lombok.Getter; import net.runelite.api.Client; import net.runelite.api.QuestState; +import org.apache.commons.text.WordUtils; /** * Requirement that checks if a {@link net.runelite.api.Quest} has a certain state. @@ -53,8 +55,7 @@ public class QuestRequirement extends AbstractRequirement */ public QuestRequirement(QuestHelperQuest quest, QuestState requiredState) { - this.quest = quest; - this.requiredState = requiredState; + this(quest, requiredState, null); } /** @@ -64,12 +65,18 @@ public QuestRequirement(QuestHelperQuest quest, QuestState requiredState) * @param requiredState the required quest state * @param displayText display text */ - public QuestRequirement(QuestHelperQuest quest, QuestState requiredState, String displayText) + public QuestRequirement(QuestHelperQuest quest, QuestState requiredState, @Nullable String displayText) { - this(quest, requiredState); + this.quest = quest; + this.requiredState = requiredState; this.displayText = displayText; } + public QuestRequirement(QuestHelperQuest quest) + { + this(quest, QuestState.FINISHED, null); + } + @Override public boolean check(Client client) { @@ -88,7 +95,7 @@ public String getDisplayText() { return displayText; } - String text = Character.toUpperCase(requiredState.name().charAt(0)) + requiredState.name().toLowerCase(Locale.ROOT).substring(1); - return text.replaceAll("_", " ") + " " + quest.getName(); + String text = WordUtils.capitalizeFully(requiredState.name().toLowerCase(Locale.ROOT).replaceAll("_", " ")); + return text + " " + quest.getName(); } } diff --git a/src/main/java/com/questhelper/spells/MagicSpell.java b/src/main/java/com/questhelper/spells/MagicSpell.java new file mode 100644 index 0000000000..3076114e6e --- /dev/null +++ b/src/main/java/com/questhelper/spells/MagicSpell.java @@ -0,0 +1,81 @@ +/* + * + * * Copyright (c) 2021, Senmori + * * 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 com.questhelper.spells; + +import com.questhelper.requirements.magic.SpellRequirement; +import com.questhelper.requirements.util.Spellbook; + +/** + * Represents a magic spell that can be cast by a player. + */ +public interface MagicSpell +{ + /** + * @return the formatted display name + */ + String getName(); + + /** + * @return the widget ID + */ + int getWidgetID(); + + /** + * @return the group ID + */ + int getGroupID(); + + /** + * @return the sprite ID + * + * @see net.runelite.api.SpriteID + */ + int getSpriteID(); + + /** + * @return the required {@link net.runelite.api.Skill#MAGIC)} level to cast this spell. + */ + int getRequiredMagicLevel(); + + /** + * @return the {@link Spellbook} this spell is contained within. + */ + Spellbook getSpellbook(); + + /** + * @return a new {@link SpellRequirement} for a single cast of this spell. + */ + SpellRequirement getSpellRequirement(); + + /** + * Create a new {@link SpellRequirement} with the given number of casts. + * + * @param numberOfCasts the number of casts + * @return a new {@link SpellRequirement} + */ + SpellRequirement getSpellRequirement(int numberOfCasts); +} diff --git a/src/main/java/com/questhelper/spells/Rune.java b/src/main/java/com/questhelper/spells/Rune.java new file mode 100644 index 0000000000..d858b65760 --- /dev/null +++ b/src/main/java/com/questhelper/spells/Rune.java @@ -0,0 +1,119 @@ +/* + * + * * Copyright (c) 2021, Senmori + * * 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 com.questhelper.spells; + +import com.questhelper.ItemCollections; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import lombok.Getter; +import net.runelite.api.ItemID; + +/** + * Represents a rune that can be used to cast spells. + */ +@Getter +public enum Rune +{ + AIR("Air Rune", ItemCollections.getAirRune(), () -> Staff.AIR), + WATER("Water Rune", ItemCollections.getWaterRune(), () -> Staff.WATER), + EARTH("Earth Rune", ItemCollections.getEarthRune(), () -> Staff.EARTH), + FIRE("Fire Rune", ItemCollections.getFireRune(), () -> Staff.FIRE), + MIND("Mind Rune", ItemID.MIND_RUNE), + BODY("Body Rune", ItemID.BODY_RUNE), + COSMIC("Cosmic Rune", ItemID.COSMIC_RUNE), + CHAOS("Chaos Rune", ItemID.CHAOS_RUNE), + NATURE("Nature Rune", ItemID.NATURE_RUNE), + LAW("Law Rune", ItemID.LAW_RUNE), + DEATH("Death Rune", ItemID.DEATH_RUNE), + ASTRAL("Astral Rune", ItemID.ASTRAL_RUNE), + BLOOD("Blood Rune", ItemID.BLOOD_RUNE), + SOUL("Soul Rune", ItemID.SOUL_RUNE), + WRATH("Wrath Rune", ItemID.WRATH_RUNE), + // Keep combination runes after the non-combination runes + LAVA("Lava Rune", ItemID.LAVA_RUNE, () -> Staff.LAVA), + MUD("Mud Rune", ItemID.MUD_RUNE, () -> Staff.MUD), + STEAM("Steam Rune", ItemID.STEAM_RUNE, () -> Staff.STEAM), + SMOKE("Smoke Rune", ItemID.SMOKE_RUNE, () -> Staff.SMOKE), + MIST("Mist Rune", ItemID.MIST_RUNE, () -> Staff.MIST), + DUST("Dust Rune", ItemID.DUST_RUNE, () -> Staff.DUST), + UNKNOWN("Null Rune", -1), + ; + + @Nonnull + private final String runeName; + @Nonnull + private final List runes; + private final Supplier staffSupplier; + Rune(@Nonnull String runeName, @Nonnull List runes, Supplier staffSupplier) + { + this.runeName = runeName; + this.runes = runes; + this.staffSupplier = staffSupplier; + } + + Rune(@Nonnull String runeName, int itemID) + { + this.runeName = runeName; + this.runes = Collections.singletonList(itemID); + this.staffSupplier = () -> Staff.UNKNOWN; + } + + Rune(@Nonnull String runeName, int itemID, Supplier staffSupplier) + { + this.runeName = runeName; + this.runes = Collections.singletonList(itemID); + this.staffSupplier = staffSupplier; + } + + public Staff getStaff() + { + return staffSupplier.get(); + } + + public int getItemID() + { + return runes.get(0); + } + + public static boolean isRuneItem(int itemID) + { + return getByItemID(itemID) != Rune.UNKNOWN; + } + + public static Rune getByItemID(int itemID) + { + return Stream.of(values()) + .sorted(Collections.reverseOrder()) + .filter(r -> r.getRunes().contains(itemID)) + .findFirst() + .orElse(Rune.UNKNOWN); + } +} diff --git a/src/main/java/com/questhelper/spells/SpellComponentPreference.java b/src/main/java/com/questhelper/spells/SpellComponentPreference.java new file mode 100644 index 0000000000..bca133be88 --- /dev/null +++ b/src/main/java/com/questhelper/spells/SpellComponentPreference.java @@ -0,0 +1,53 @@ +/* + * + * * Copyright (c) 2021 + * * 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 com.questhelper.spells; + +import com.questhelper.requirements.magic.RuneRequirement; +import com.questhelper.requirements.item.ItemRequirement; +import java.util.function.BooleanSupplier; + +public enum SpellComponentPreference +{ + RUNES { + @Override + public ItemRequirement getPreference(RuneRequirement requirement, BooleanSupplier runes, BooleanSupplier staff) + { + return runes.getAsBoolean() ? requirement.getRuneItemRequirement() : requirement.getStaffItemRequirement(); + } + }, + STAVES { + @Override + public ItemRequirement getPreference(RuneRequirement requirement, BooleanSupplier runes, BooleanSupplier staff) + { + return staff.getAsBoolean() ? requirement.getStaffItemRequirement() : requirement.getRuneItemRequirement(); + } + } + ; + + public abstract ItemRequirement getPreference(RuneRequirement requirement, BooleanSupplier runes, BooleanSupplier staff); +} diff --git a/src/main/java/com/questhelper/spells/Staff.java b/src/main/java/com/questhelper/spells/Staff.java new file mode 100644 index 0000000000..d76b7d30c1 --- /dev/null +++ b/src/main/java/com/questhelper/spells/Staff.java @@ -0,0 +1,89 @@ +/* + * + * * Copyright (c) 2021 + * * 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 com.questhelper.spells; + +import com.google.common.collect.ImmutableSet; +import com.questhelper.ItemCollections; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; +import lombok.Getter; + +@Getter +public enum Staff +{ + AIR("Air Staff", ItemCollections.getAirStaff(), () -> ImmutableSet.of(Rune.AIR)), + WATER("Water Staff", ItemCollections.getWaterStaff(), () -> ImmutableSet.of(Rune.WATER)), + EARTH("Earth Staff", ItemCollections.getEarthStaff(), () -> ImmutableSet.of(Rune.EARTH)), + FIRE("Fire Staff", ItemCollections.getFireStaff(), () -> ImmutableSet.of(Rune.FIRE)), + // Keep combination staves after the elemental staves + LAVA("Lava Staff", ItemCollections.getLavaStaff(), () -> ImmutableSet.of(Rune.LAVA, Rune.FIRE, Rune.EARTH)), + MUD("Mud Staff", ItemCollections.getMudStaff(), () -> ImmutableSet.of(Rune.MUD, Rune.WATER, Rune.EARTH)), + STEAM("Steam Staff", ItemCollections.getSteamStaff(), () -> ImmutableSet.of(Rune.STEAM, Rune.WATER, Rune.FIRE)), + SMOKE("Smoke Staff", ItemCollections.getSmokeStaff(), () -> ImmutableSet.of(Rune.SMOKE, Rune.AIR, Rune.FIRE)), + MIST("Mist Staff", ItemCollections.getMistStaff(), () -> ImmutableSet.of(Rune.MIST, Rune.WATER, Rune.AIR)), + DUST("Dust Staff", ItemCollections.getDustStaff(), () -> ImmutableSet.of(Rune.DUST, Rune.EARTH, Rune.AIR)), + UNKNOWN("Null Staff", Collections.emptyList(), () -> ImmutableSet.of(Rune.UNKNOWN)), + ; + + private final String name; + private final List staves; + private final Supplier> sourceRunesSupplier; + Staff(String name, List staves, Supplier> sourceRunesSupplier) + { + this.name = name; + this.staves = staves; + this.sourceRunesSupplier = sourceRunesSupplier; + } + + public int getItemID() + { + return staves.get(0); + } + + public boolean isSourceOf(Rune rune) + { + return sourceRunesSupplier.get().contains(rune); + } + + public static Staff getByItemID(int itemID) + { + return Stream.of(Staff.values()) + .sorted(Collections.reverseOrder()) + .filter(staff -> staff.getStaves().contains(itemID)) + .findFirst() + .orElse(Staff.UNKNOWN); + } + + public static boolean isStaff(int itemID) + { + return getByItemID(itemID) != Staff.UNKNOWN; + } +} diff --git a/src/main/java/com/questhelper/spells/StandardSpell.java b/src/main/java/com/questhelper/spells/StandardSpell.java new file mode 100644 index 0000000000..cb5ffd94d1 --- /dev/null +++ b/src/main/java/com/questhelper/spells/StandardSpell.java @@ -0,0 +1,199 @@ +/* + * + * * Copyright (c) 2021, Senmori + * * 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 com.questhelper.spells; + +import static com.questhelper.QuestHelperQuest.PLAGUE_CITY; +import static com.questhelper.QuestHelperQuest.THE_MAGE_ARENA; +import static com.questhelper.QuestHelperQuest.EADGARS_RUSE; +import static com.questhelper.QuestHelperQuest.UNDERGROUND_PASS; +import com.questhelper.requirements.magic.SpellRequirement; +import com.questhelper.requirements.util.Spellbook; +import static com.questhelper.spells.Rune.FIRE; +import java.util.Locale; +import java.util.function.UnaryOperator; +import lombok.Getter; + +import static com.questhelper.spells.Rune.*; +import net.runelite.api.ItemID; +import static net.runelite.api.ItemID.GUTHIX_STAFF; +import static net.runelite.api.ItemID.IBANS_STAFF; +import static net.runelite.api.ItemID.IBANS_STAFF_U; +import static net.runelite.api.ItemID.SARADOMIN_STAFF; +import static net.runelite.api.ItemID.SLAYERS_STAFF; +import static net.runelite.api.ItemID.SLAYERS_STAFF_E; +import static net.runelite.api.ItemID.STAFF_OF_BALANCE; +import static net.runelite.api.ItemID.STAFF_OF_LIGHT; +import static net.runelite.api.ItemID.STAFF_OF_THE_DEAD; +import static net.runelite.api.ItemID.STAFF_OF_THE_DEAD_23613; +import static net.runelite.api.ItemID.TOXIC_STAFF_OF_THE_DEAD; +import static net.runelite.api.ItemID.VOID_KNIGHT_MACE; +import static net.runelite.api.ItemID.ZAMORAK_STAFF; +import net.runelite.api.Skill; +import net.runelite.api.Varbits; +import org.apache.commons.text.WordUtils; + +@Getter +public enum StandardSpell implements MagicSpell +{ + LUMBRIDGE_HOME_TELEPORT(356, 5, 0), + WIND_STRIKE(15, 6, 1, b -> b.rune(AIR).rune(MIND)), + CONFUSE(16, 7, 3, b -> b.rune(BODY).rune(2, EARTH).rune(3, WATER)), + ENCHANT_OPAL_BOLT(358, 8, 4, b -> b.rune(COSMIC).rune(2, AIR)), + WATER_STRIKE(17, 9, 5, b -> b.rune(AIR).rune(WATER).rune(MIND)), + LVL_1_ENCHANT(18, 10, 7, b -> b.rune(WATER).rune(COSMIC)), + ENCHANT_SAPPHIRE_BOLT(358, 8, 9, b -> b.rune(WATER).rune(COSMIC)), + EARTH_STRIKE(19, 11, 9, b -> b.rune(AIR).rune(MIND).rune(2, EARTH)), + WEAKEN(20, 12, 11, b -> b.rune(BODY).rune(2, EARTH).rune(3, WATER)), + FIRE_STRIKE(21, 13, 13, b -> b.rune(MIND).rune(2, AIR).rune(3, FIRE)), + ENCHANT_JADE_BOLT(358, 8, 14, b -> b.rune(COSMIC).rune(2, EARTH)), + BONES_TO_BANANAS(22, 14, 15, b -> b.rune(NATURE).rune(2, EARTH).rune(2, WATER)), + WIND_BOLT(23, 15, 17, b -> b.rune(CHAOS).rune(2, AIR)), + CURSE(24, 16, 19, b -> b.rune(BODY).rune(2, WATER).rune(3, EARTH)), + BIND(319, 17, 20, b -> b.rune(2, NATURE).rune(3, WATER).rune(3, EARTH)), + LOW_LVL_ALCHEMY(25, 18, 21, b -> b.rune(NATURE).rune(3, FIRE)), + WATER_BOLT(26, 19, 23, b -> b.rune(CHAOS).rune(2, WATER).rune(2, AIR)), + ENCHANT_PEARL_BOLT(358, 8, 24, b -> b.rune(COSMIC).rune(2, WATER)), + VARROCK_TELEPORT(27, 20, 25, b -> b.rune(LAW).rune(FIRE).rune(3, AIR).tablet(ItemID.VARROCK_TELEPORT)), + LVL_2_ENCHANT(28, 21, 27, b -> b.rune(COSMIC).rune(3, AIR)), + ENCHANT_EMERALD_BOLT(358, 8, 27, b -> b.rune(NATURE).rune(COSMIC).rune(3, AIR)), + EARTH_BOLT(29, 22, 9, b -> b.rune(CHAOS).rune(EARTH).rune(3, AIR)), + ENCHANT_RED_TOPAZ_BOLT(358, 8, 29, b -> b.rune(CHAOS).rune(2, FIRE)), + LUMBRIDGE_TELEPORT(30, 23, 31, b -> b.rune(LAW).rune(EARTH).rune(3, AIR).tablet(ItemID.LUMBRIDGE_TELEPORT)), + TELEKINETIC_GRAB(31, 24, 33, b -> b.rune(LAW).rune(AIR)), + FIRE_BOLT(32, 25, 35, b -> b.rune(CHAOS).rune(3, AIR).rune(4, FIRE)), + FALADOR_TELEPORT(33, 26, 37, b -> b.rune(LAW).rune(WATER).rune(3, AIR).tablet(ItemID.FALADOR_TELEPORT)), + CRUMBLE_UNDEAD(34, 27, 39, b -> b.rune(CHAOS).rune(2, EARTH).rune(2, AIR)), + TELEPORT_TO_HOUSE(355, 28, 40, b -> b.rune(LAW).rune(EARTH).rune(AIR).tablet(ItemID.TELEPORT_TO_HOUSE)), + WIND_BLAST(35, 29, 41, b -> b.rune(DEATH).rune(3, AIR)), + SUPERHEAT_ITEM(36, 30, 43, b -> b.rune(NATURE).rune(4, FIRE)), + CAMELOT_TELEPORT(37, 31, 45, b -> b.rune(LAW).rune(5, AIR).tablet(ItemID.CAMELOT_TELEPORT)), + WATER_BLAST(38, 32, 47, b -> b.rune(DEATH).rune(3, AIR).rune(3, WATER)), + LVL_3_ENCHANT(39, 33, 49, b -> b.rune(COSMIC).rune(5, FIRE)), + ENCHANT_RUBY_BOLT(358, 8, 49, b -> b.rune(COSMIC).rune(BLOOD).rune(5, FIRE)), + IBAN_BLAST(53, 34, 50, b -> b.rune(DEATH).rune(5, FIRE).skill(Skill.ATTACK, 50).quest(UNDERGROUND_PASS).staff(IBANS_STAFF, IBANS_STAFF_U)), + SNARE(320, 35, 50, b -> b.rune(3, NATURE).rune(4, WATER).rune(4, EARTH)), + MAGIC_DART(324, 36, 50, b -> b.rune(DEATH).rune(4, MIND).staff(SLAYERS_STAFF, SLAYERS_STAFF_E, STAFF_OF_THE_DEAD, STAFF_OF_THE_DEAD_23613, TOXIC_STAFF_OF_THE_DEAD, STAFF_OF_LIGHT, STAFF_OF_BALANCE)), + ARDOUGNE_TELEPORT(54, 37, 51, b -> b.rune(2, LAW).rune(2, WATER).tablet(ItemID.ARDOUGNE_TELEPORT).quest(PLAGUE_CITY)), + EARTH_BLAST(40, 38, 43, b -> b.rune(DEATH).rune(3, AIR).rune(4, EARTH)), + HIGH_LVL_ALCHEMY(41, 39, 55, b -> b.rune(NATURE).rune(5, FIRE)), + CHARGE_WATER_ORB(42, 40, 56, b -> b.rune(3, COSMIC).rune(30, WATER).item(ItemID.UNPOWERED_ORB)), + LVL_4_ENCHANT(43, 41, 57, b -> b.rune(COSMIC).rune(10, EARTH)), + ENCHANT_DIAMOND_BOLT(358, 8, 57, b -> b.rune(COSMIC).rune(2, LAW).rune(10, EARTH)), + WATCHTOWER_TELEPORT(55, 42, 58, b -> b.rune(2, LAW).rune(2, EARTH).tablet(ItemID.WATCHTOWER_TELEPORT)), + FIRE_BLAST(44, 43, 59, b -> b.rune(DEATH).rune(4, AIR).rune(5, FIRE)), + CHARGE_EARTH_ORB(45, 44, 60, b -> b.rune(3, COSMIC).rune(30, EARTH).item(ItemID.UNPOWERED_ORB)), + BONES_TO_PEACHES(354, 45, 60, b -> b.rune(2, NATURE).rune(2, EARTH).rune(4, WATER)), //TODO: MAGE TRAINING ARENA + SARADOMIN_STRIKE(61, 46, 60, b -> b.rune(2, BLOOD).rune(2, FIRE).rune(4, AIR).quest(THE_MAGE_ARENA).staff(SARADOMIN_STAFF, STAFF_OF_LIGHT)), + CLAWS_OF_GUTHIX(60, 47, 60, b -> b.rune(FIRE).rune(2, BLOOD).rune(4, AIR).quest(THE_MAGE_ARENA).staff(GUTHIX_STAFF, VOID_KNIGHT_MACE, STAFF_OF_BALANCE)), + FLAMES_OF_ZAMORAK(59, 48, 60, b -> b.rune(AIR).rune(2, BLOOD).rune(4, FIRE).quest(THE_MAGE_ARENA).staff(ZAMORAK_STAFF, STAFF_OF_THE_DEAD, STAFF_OF_THE_DEAD_23613, TOXIC_STAFF_OF_THE_DEAD)), + TROLLHEIM_TELEPORT(323, 49, 61, b -> b.rune(2, LAW).rune(2, FIRE).quest(EADGARS_RUSE)), + WIND_WAVE(46, 50, 62, b -> b.rune(BLOOD).rune(5, AIR)), + CHARGE_FIRE_ORB(47, 51, 63, b -> b.rune(3, COSMIC).rune(30, FIRE).item(ItemID.UNPOWERED_ORB)), + APE_ATOLL_TELEPORT(357, 52, 64, b -> b.rune(2, LAW).rune(2, WATER).rune(2, FIRE).item(ItemID.BANANA)), + WATER_WAVE(48, 53, 54, b -> b.rune(BLOOD).rune(5, AIR).rune(7, WATER)), + CHARGE_AIR_ORB(49, 54, 66, b -> b.rune(3, COSMIC).rune(30, AIR).item(ItemID.UNPOWERED_ORB)), + VULNERABILITY(56, 55, 66, b -> b.rune(SOUL).rune(5, WATER).rune(5, EARTH)), + LVL_5_ENCHANT(50, 56, 68, b -> b.rune(COSMIC).rune(15, WATER).rune(15, EARTH)), + ENCHANT_DRAGONSTONE_BOLT(358, 8, 68, b -> b.rune(SOUL).rune(COSMIC).rune(12, EARTH)), + KOUREND_CASTLE_TELEPORT(360, 57, 69, b -> b.rune(2, SOUL).rune(2, LAW).rune(4,WATER).rune(5, FIRE).varbit(10019, 1)), + EARTH_WAVE(51, 58, 70, b -> b.rune(BLOOD).rune(5, AIR).rune(7, EARTH)), + ENFEEBLE(57, 59, 73, b -> b.rune(SOUL).rune(8, WATER).rune(8, EARTH)), + TELEOTHER_LUMBRIDGE(349, 60, 74, b -> b.rune(SOUL).rune(LAW).rune(EARTH)), + FIRE_WAVE(52, 61, 75, b -> b.rune(BLOOD).rune(5, AIR).rune(7, FIRE)), + ENTANGLE(321, 62, 79, b -> b.rune(4, NATURE).rune(5, WATER).rune(5, EARTH)), + STUN(58, 63, 80, b -> b.rune(SOUL).rune(12, WATER).rune(12, EARTH)), + CHARGE(322, 64, 80, b -> b.rune(3, BLOOD).rune(3, FIRE).rune(3, AIR)), + WIND_SURGE(362, 65, 81, b -> b.rune(WRATH).rune(7, AIR)), + TELEOTHER_FALADOR(350, 66, 82, b -> b.rune(SOUL).rune(LAW).rune(WATER)), + WATER_SURGE(363, 67, 85, b -> b.rune(WRATH).rune(7, AIR).rune(10, WATER)), + TELE_BLOCK(352, 68, 85, b -> b.rune(LAW).rune(DEATH).rune(CHAOS).var(Varbits.IN_WILDERNESS, 1)), + TELEPORT_TO_TARGET(359, 69, 85, b -> b.rune(LAW).rune(DEATH).rune(CHAOS)), //TODO: HAVE READ TARGET TELEPORT SCROLL + LVL_6_ENCHANT(353, 70, 87, b -> b.rune(COSMIC).rune(20, FIRE).rune(20, EARTH)), + ENCHANT_ONYX_BOLT(358, 8, 87, b -> b.rune(DEATH).rune(COSMIC).rune(20, FIRE)), + TELEOTHER_CAMELOT(351, 71, 90, b -> b.rune(LAW).rune(2, SOUL)), + EARTH_SURGE(364, 72, 90, b -> b.rune(WRATH).rune(7, AIR).rune(10, EARTH)), + LVL_7_ENCHANT(361, 73, 93, b -> b.rune(COSMIC).rune(20, SOUL).rune(20, BLOOD)), + FIRE_SURGE(365, 74, 95, b -> b.rune(WRATH).rune(7, AIR).rune(10, FIRE)), + ; + + private final int groupID = 218; + private final int spriteID; + private final int widgetID; + private final int requiredMagicLevel; + private final UnaryOperator operator; + StandardSpell(int spriteID, int widgetID, int requiredMagicLevel) + { + this(spriteID, widgetID, requiredMagicLevel, UnaryOperator.identity()); + } + + StandardSpell(int spriteID, int widgetID, int requiredMagicLevel, UnaryOperator operator) + { + this.spriteID = spriteID; + this.widgetID = widgetID; + this.requiredMagicLevel = requiredMagicLevel; + this.operator = operator; + } + + @Override + public SpellRequirement getSpellRequirement() + { + return operator.apply(StandardSpellBuilder.builder(this)).build(); + } + + @Override + public SpellRequirement getSpellRequirement(int numberOfCasts) + { + SpellRequirement requirement = getSpellRequirement(); + requirement.setNumberOfCasts(numberOfCasts); + return requirement; + } + + @Override + public String getName() + { + String spellName = name().toLowerCase(Locale.ROOT).replaceAll("_", " "); + return WordUtils.capitalizeFully(spellName); + } + + @Override + public int getGroupID() + { + return groupID; + } + + @Override + public int getSpriteID() + { + return spriteID; + } + + @Override + public Spellbook getSpellbook() + { + return Spellbook.NORMAL; + } +} diff --git a/src/main/java/com/questhelper/spells/StandardSpellBuilder.java b/src/main/java/com/questhelper/spells/StandardSpellBuilder.java new file mode 100644 index 0000000000..d830d38677 --- /dev/null +++ b/src/main/java/com/questhelper/spells/StandardSpellBuilder.java @@ -0,0 +1,251 @@ +/* + * + * * Copyright (c) 2021, Senmori + * * 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 com.questhelper.spells; + +import com.questhelper.QuestHelperQuest; +import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.magic.SpellRequirement; +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.requirements.item.ItemRequirements; +import com.questhelper.requirements.player.SkillRequirement; +import com.questhelper.requirements.quest.QuestRequirement; +import com.questhelper.requirements.var.VarbitRequirement; +import com.questhelper.requirements.var.VarplayerRequirement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import net.runelite.api.QuestState; +import net.runelite.api.Skill; +import net.runelite.api.VarPlayer; +import net.runelite.api.Varbits; + +public class StandardSpellBuilder +{ + private final MagicSpell spell; + private final List requirements = new LinkedList<>(); + private final Map runeList = new HashMap<>(); + private final List staffIDList = new ArrayList<>(); + private int tabletItemID = -1; + + private StandardSpellBuilder(MagicSpell spell) + { + this.spell = spell; + } + + /** + * Get a new instance of this StandardSpellBuilder + * + * @param spell the spell to build + * @return this + */ + public static StandardSpellBuilder builder(MagicSpell spell) + { + return new StandardSpellBuilder(spell); + } + + /** + * Add a specified quantity of a given {@link Rune} + * + * @param quantity the number of Rune(s) required + * @param rune the Rune + * @return this + */ + public StandardSpellBuilder rune(int quantity, Rune rune) + { + runeList.put(rune, quantity); + return this; + } + + /** + * Add a single Rune to this spell + * + * @param rune the Rune + * @return this + */ + public StandardSpellBuilder rune(Rune rune) + { + return rune(1, rune); + } + + public StandardSpellBuilder staff(int staffID) + { + this.staffIDList.add(staffID); + return this; + } + + public StandardSpellBuilder staff(Integer... staves) + { + this.staffIDList.addAll(Arrays.asList(staves)); + return this; + } + + /** + * Add an item this spell needs in order to be cast. + * + * @param quantity the number of items required + * @param itemID the item required + * @return this + */ + public StandardSpellBuilder item(int quantity, int itemID) + { + requirements.add(new ItemRequirement("", itemID, quantity)); + return this; + } + + /** + * Add a single item required for this spell to be cast + * + * @param itemID the item required + * @return this + */ + public StandardSpellBuilder item(int itemID) + { + return item(1, itemID); + } + + /** + * Add an item (and it's suitable equivalents) that should be equipped in order to cast this spell + * + * @param equipped if this item should be equipped + * @param itemIDs the items that should be equipped (only one is required to be equipped) + * @return this + */ + public StandardSpellBuilder item(boolean equipped, Integer... itemIDs) + { + requirements.add(new ItemRequirement("", Arrays.asList(itemIDs), 1, equipped)); + return this; + } + + /** + * Set the tablet that can be used to cast this spell. + * + * @param itemID the tablet item id + * @return this + */ + public StandardSpellBuilder tablet(int itemID) + { + this.tabletItemID = itemID; + return this; + } + + /** + * Add a {@link QuestHelperQuest} requirement to this spell. + * By default, this requires the quest to be finished.
+ * See {@link #quest(QuestHelperQuest, boolean)} for specifying if the quest should only + * be started. + * + * @param quest the quest that should be completed in order to cast this spell + * @return this + */ + public StandardSpellBuilder quest(QuestHelperQuest quest) + { + return quest(quest, false); + } + + /** + * Add a {@link QuestHelperQuest} requirement to this spell. + * If {@param started} is true, this will mean the {@link QuestHelperQuest} is only required to be + * {@link QuestState#IN_PROGRESS}, not {@link QuestState#FINISHED}. + * + * @param quest the quest requirement to add + * @param started if the quest should be started, or finished + * @return this + */ + public StandardSpellBuilder quest(QuestHelperQuest quest, boolean started) + { + QuestState state = started ? QuestState.IN_PROGRESS : QuestState.FINISHED; + requirements.add(new QuestRequirement(quest, state)); + return this; + } + + /** + * Add a {@link Varbits} requirement to this spell. + * + * @param varbit the {@link Varbits} that is required + * @param value the varbit value that is required + * @return this + */ + public StandardSpellBuilder var(Varbits varbit, int value) + { + requirements.add(new VarbitRequirement(varbit.getId(), value)); + return this; + } + + /** + * Add a {@link VarPlayer} requirement to this spell + * + * @param varPlayer the {@link VarPlayer} that is needed + * @param value the value that is required + * @return + */ + public StandardSpellBuilder var(VarPlayer varPlayer, int value) + { + requirements.add(new VarplayerRequirement(varPlayer.getId(), value)); + return this; + } + + /** + * Set a varbit that is not defined via the {@link Varbits} or {@link VarPlayer} enum(s). + * + * @param varbit the varbit to test for + * @param value the value it should be + * @return this + */ + public StandardSpellBuilder varbit(int varbit, int value) + { + requirements.add(new VarbitRequirement(varbit, value)); + return this; + } + + public StandardSpellBuilder skill(Skill skill, int level) + { + requirements.add(new SkillRequirement(skill, level)); + return this; + } + + /** + * @return a new {@link ItemRequirements} containing all this spell information + */ + public SpellRequirement build() + { + SpellRequirement requirement = new SpellRequirement(spell, runeList, requirements); + if (tabletItemID != -1) + { + requirement.setTablet(tabletItemID); + } + if (!staffIDList.isEmpty()) + { + requirement.setStaffUse(true); + requirement.setStaff(new ItemRequirement("", staffIDList, 1, true)); + } + return requirement; + } + +} diff --git a/src/main/java/com/questhelper/steps/DetailedQuestStep.java b/src/main/java/com/questhelper/steps/DetailedQuestStep.java index 329b9325be..5dcbc1a648 100644 --- a/src/main/java/com/questhelper/steps/DetailedQuestStep.java +++ b/src/main/java/com/questhelper/steps/DetailedQuestStep.java @@ -261,6 +261,7 @@ public void renderArrow(Graphics2D graphics) @Override public void makeWidgetOverlayHint(Graphics2D graphics, QuestHelperPlugin plugin) { + super.makeWidgetOverlayHint(graphics, plugin); renderInventory(graphics); if (!hideMinimapLines) { @@ -338,7 +339,7 @@ public void makeOverlayHint(PanelComponent panelComponent, QuestHelperPlugin plu } stream .distinct() - .map(req -> req.getDisplayTextWithChecks(client)) + .map(req -> req.getDisplayTextWithChecks(client, plugin)) .flatMap(Collection::stream) .forEach(line -> panelComponent.getChildren().add(line)); diff --git a/src/main/java/com/questhelper/steps/DigStep.java b/src/main/java/com/questhelper/steps/DigStep.java index 2d27e67430..c114543c95 100644 --- a/src/main/java/com/questhelper/steps/DigStep.java +++ b/src/main/java/com/questhelper/steps/DigStep.java @@ -44,7 +44,7 @@ public class DigStep extends DetailedQuestStep { private final ItemRequirement SPADE = new ItemRequirement("Spade", ItemID.SPADE); - private Predicate expectedItemPredicate = i -> i.getId() == -1; + private Predicate expectedItemPredicate = i -> true; private boolean hasExpectedItem = false; public DigStep(QuestHelper questHelper, WorldPoint worldPoint, String text, Requirement... requirements) { diff --git a/src/main/java/com/questhelper/steps/NpcStep.java b/src/main/java/com/questhelper/steps/NpcStep.java index 478b6a9df7..7a87d2fc5a 100644 --- a/src/main/java/com/questhelper/steps/NpcStep.java +++ b/src/main/java/com/questhelper/steps/NpcStep.java @@ -28,6 +28,8 @@ import com.questhelper.QuestHelperPlugin; import com.questhelper.questhelpers.QuestHelper; import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.magic.SpellRequirement; +import com.questhelper.spells.MagicSpell; import com.questhelper.steps.overlay.DirectionArrow; import com.questhelper.steps.tools.QuestPerspective; import java.awt.Graphics2D; diff --git a/src/main/java/com/questhelper/steps/ObjectStep.java b/src/main/java/com/questhelper/steps/ObjectStep.java index 7b7973356c..cfdf0e13bc 100644 --- a/src/main/java/com/questhelper/steps/ObjectStep.java +++ b/src/main/java/com/questhelper/steps/ObjectStep.java @@ -27,6 +27,8 @@ import com.questhelper.QuestHelperPlugin; import com.questhelper.questhelpers.QuestHelper; import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.magic.SpellRequirement; +import com.questhelper.spells.MagicSpell; import com.questhelper.steps.overlay.DirectionArrow; import com.questhelper.steps.tools.QuestPerspective; import net.runelite.api.Point; @@ -181,6 +183,16 @@ public void addAlternateObjects(Integer... alternateObjectIDs) this.alternateObjectIDs.addAll(Arrays.asList(alternateObjectIDs)); } + public void requireSpellCast(SpellRequirement spellRequirement) + { + addSpell(spellRequirement.getSpell()); + } + + public void requireSpellCast(MagicSpell spell) + { + addSpell(spell); + } + @Subscribe public void onGameObjectSpawned(GameObjectSpawned event) { @@ -284,7 +296,7 @@ public void makeWorldOverlayHint(Graphics2D graphics, QuestHelperPlugin plugin) } } - if (iconItemID != -1 && object != null && questHelper.getConfig().showSymbolOverlay()) + if (icon != null && object != null && questHelper.getConfig().showSymbolOverlay()) { Shape clickbox = object.getClickbox(); if (clickbox != null && !inCutscene) diff --git a/src/main/java/com/questhelper/steps/QuestStep.java b/src/main/java/com/questhelper/steps/QuestStep.java index 4b25ad5389..6b64e4f40d 100644 --- a/src/main/java/com/questhelper/steps/QuestStep.java +++ b/src/main/java/com/questhelper/steps/QuestStep.java @@ -34,12 +34,15 @@ import com.questhelper.questhelpers.QuestUtil; import com.questhelper.requirements.Requirement; import com.questhelper.requirements.conditional.ConditionForStep; +import com.questhelper.spells.MagicSpell; import com.questhelper.steps.choice.DialogChoiceStep; import com.questhelper.steps.choice.DialogChoiceSteps; import com.questhelper.steps.choice.WidgetChoiceStep; import com.questhelper.steps.choice.WidgetChoiceSteps; import com.questhelper.steps.overlay.IconOverlay; import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; @@ -49,10 +52,13 @@ import lombok.Getter; import lombok.Setter; import net.runelite.api.Client; +import net.runelite.api.Point; import net.runelite.api.SpriteID; import net.runelite.api.events.VarbitChanged; import net.runelite.api.events.WidgetLoaded; +import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.callback.ClientThread; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.game.ItemManager; @@ -107,6 +113,7 @@ public abstract class QuestStep implements Module protected boolean allowInCutscene = false; protected int iconItemID = -1; + protected MagicSpell spell; protected BufferedImage icon; @Getter @@ -356,12 +363,39 @@ public void addIcon(int iconItemID) this.iconItemID = iconItemID; } + public void addSpell(MagicSpell spell) + { + this.spell = spell; + } + public void makeWorldOverlayHint(Graphics2D graphics, QuestHelperPlugin plugin) { } public void makeWidgetOverlayHint(Graphics2D graphics, QuestHelperPlugin plugin) { + if (!plugin.getConfig().showSymbolOverlay()) + { + return; + } + if (spell != null) + { + Widget widget = client.getWidget(spell.getGroupID(), spell.getWidgetID()); + + if (widget != null) + { + if (widget.isHidden()) + { + return; + } + graphics.setColor(plugin.getConfig().targetOverlayColor()); + + Rectangle rect = widget.getBounds(); + int x = (int) rect.getX(); + int y = (int) rect.getY(); + graphics.drawRect(x, y, widget.getWidth(), widget.getHeight()); + } + } } public void setLockedManually(boolean isLocked) @@ -396,6 +430,18 @@ protected void setupIcon() { icon = IconOverlay.createIconImage(itemManager.getImage(iconItemID)); } + else if (spell != null && spell.getSpriteID() != -1 && icon == null) + { + BufferedImage sprite = spriteManager.getSprite(spell.getSpriteID(), 0); + if (sprite != null) + { + icon = IconOverlay.createIconImage(sprite); + } + else + { + throw new UnsupportedOperationException("Unknown spell sprite ID \"" + spell.getSpriteID() + "\" for spell " + spell.getName()); + } + } else if (icon == null) { icon = getQuestImage();