diff --git a/build.gradle b/build.gradle index a56ad58040..62c4fb9a7d 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { testImplementation group: "org.junit.jupiter", name: "junit-jupiter-engine", version: junitVersion testImplementation group: 'org.mockito', name:'mockito-core', version: "4.11.0" // runelite uses 3.1.0 + testImplementation group: 'org.mockito', name:'mockito-inline', version: "4.11.0" testImplementation(group: 'com.google.inject.extensions', name:'guice-testlib', version: "4.1.0") { exclude group: 'com.google.inject', module: 'guice' // already provided by runelite } diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/MetalDoorSolver.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/MetalDoorSolver.java new file mode 100644 index 0000000000..53339034ed --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/MetalDoorSolver.java @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav; + +import com.questhelper.QuestHelperPlugin; +import com.questhelper.requirements.ManualRequirement; +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.requirements.widget.WidgetModelRequirement; +import com.questhelper.requirements.widget.WidgetPresenceRequirement; +import com.questhelper.requirements.widget.WidgetTextRequirement; +import com.questhelper.steps.ConditionalStep; +import com.questhelper.steps.DetailedOwnerStep; +import com.questhelper.steps.DetailedQuestStep; +import com.questhelper.steps.ItemStep; +import com.questhelper.steps.ObjectStep; +import com.questhelper.steps.QuestStep; +import java.awt.*; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import javax.inject.Inject; +import com.questhelper.steps.WidgetStep; +import com.questhelper.steps.widget.WidgetDetails; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.ItemID; +import net.runelite.api.ObjectID; +import net.runelite.api.annotations.Interface; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.ui.FontManager; +import static com.questhelper.requirements.util.LogicHelper.and; +import static com.questhelper.requirements.util.LogicHelper.not; + +@Slf4j +public class MetalDoorSolver extends DetailedOwnerStep +{ + private static final int STRIP_1_INDEX = 2; + private static final int STRIP_2_INDEX = 1; + private static final int STRIP_3_INDEX = 3; + private static final int STRIP_4_INDEX = 0; + /** + * Maps the relevant numbers from the Metal door puzzle to their corresponding rows. + * The first column is for strip 4 + * The second column is for strip 2 + * The third column is for strip 1 + * The fourth column is for strip 3 + */ + private static final int[][] CODE_MAPPING = new int[][]{ + { // A + 7, 9, 6, 4, + }, + { // B + 5, 3, 1, 0, + }, + { // C + 2, 8, 6, 3, + }, + { // D + 0, 6, 4, 7, + }, + { // E + 4, 3, 6, 4, + }, + { // F + 2, 2, 1, 9, + }, + { // G + 2, 3, 2, 6, + }, + { // H + 4, 3, 8, 1, + }, + { // I + 9, 3, 0, 9, + }, + }; + + private static final @Interface int PUZZLE_GROUP_ID = 903; + private static final int PUZZLE_BTN_UP_CHILD_ID = 21; + private static final int PUZZLE_BTN_DOWN_CHILD_ID = 22; + + private static final int PUZZLE_NUMBER_0_MODEL_ID = 50567; + private static final int PUZZLE_NUMBER_1_MODEL_ID = 50542; + private static final int PUZZLE_NUMBER_2_MODEL_ID = 50540; + private static final int PUZZLE_NUMBER_3_MODEL_ID = 50557; + private static final int PUZZLE_NUMBER_4_MODEL_ID = 50562; + private static final int PUZZLE_NUMBER_5_MODEL_ID = 50555; + private static final int PUZZLE_NUMBER_6_MODEL_ID = 50568; + private static final int PUZZLE_NUMBER_7_MODEL_ID = 50566; + private static final int PUZZLE_NUMBER_8_MODEL_ID = 50575; + private static final int PUZZLE_NUMBER_9_MODEL_ID = 50553; + private static final int[] PUZZLE_NUMBERS = new int[]{PUZZLE_NUMBER_0_MODEL_ID, PUZZLE_NUMBER_1_MODEL_ID, PUZZLE_NUMBER_2_MODEL_ID, PUZZLE_NUMBER_3_MODEL_ID, PUZZLE_NUMBER_4_MODEL_ID, PUZZLE_NUMBER_5_MODEL_ID, PUZZLE_NUMBER_6_MODEL_ID, PUZZLE_NUMBER_7_MODEL_ID, PUZZLE_NUMBER_8_MODEL_ID, PUZZLE_NUMBER_9_MODEL_ID}; + + private static final int PUZZLE_ENTER_CHILD_ID = 23; + private static final int PUZZLE_BACK_CHILD_ID = 24; + private static final int PUZZLE_PASSWORD_CURRENT_CHILD_ID = 25; + private static final int PUZZLE_PASSWORD_1_CHILD_ID = 26; + private static final int PUZZLE_PASSWORD_2_CHILD_ID = 27; + private static final int PUZZLE_PASSWORD_3_CHILD_ID = 28; + private static final int PUZZLE_PASSWORD_4_CHILD_ID = 29; + + /** + * Group ID of the "MESBOX" widget containing our code + */ + private static final @Interface int MESBOX_GROUP_ID = 229; + + /** + * Child ID of the "MESBOX" widget containing our code + */ + private static final int MESBOX_CHILD_ID = 1; + + private static final Pattern CODE_PATTERN = Pattern.compile("It reads ([A-I]{4})."); + + @Inject + Client client; + + /** + * The code read from the Code key + */ + private String code = null; + + /** + * The final password to the metal door, calculating using the code + */ + private int[] doorPassword = null; + + private ItemStep readCode; + private ObjectStep clickMetalDoors; + private ConditionalStep solvePuzzle; + private QuestStep solvePuzzleFallback; + private WidgetTextRequirement firstNumberCorrect; + private WidgetTextRequirement secondNumberCorrect; + private WidgetTextRequirement thirdNumberCorrect; + private WidgetTextRequirement fourthNumberCorrect; + private WidgetModelRequirement inputFirstCorrect; + private WidgetModelRequirement inputSecondCorrect; + private WidgetModelRequirement inputThirdCorrect; + private WidgetModelRequirement inputFourthCorrect; + + private int distanceUp = 69; + private int distanceDown = 69; + private ManualRequirement shouldClickDownInteadOfUp; + + public MetalDoorSolver(TheCurseOfArrav theCurseOfArrav) + { + super(theCurseOfArrav, "Solve the Metal door puzzle by following the instructions in the overlay."); + } + + /** + * The numbers of the metal door puzzle are always the same, and in the same position. + * The decoder strips always keep the hole in the same position. + * Because of this, given a code, we can automatically figure out the final password. + *

+ * STRIP 4 + * | + * | STRIP 2 + * | | + * | | STRIP 1 + * | | | + * | | | STRIP 3 + * | | | | + * V V V V + * A 3 7 2 9 1 6 5 4 3 + * B 6 5 4 3 2 1 9 0 4 + * C 7 2 1 8 7 6 4 3 2 + * D 9 0 7 6 5 4 3 7 1 + * E 9 4 1 3 3 6 2 4 8 + * F 6 2 3 2 4 1 6 9 7 + * G 0 2 1 3 7 2 5 6 3 + * H 9 4 6 3 8 8 0 1 9 + * I 4 9 2 3 1 0 4 9 2 + * + * @param code The code from the "Code key" (e.g. IFCB) + * @return 4 ints corresponding to the final password for the metal door + */ + public static int[] calculate(String code) + { + if (code == null || code.length() != 4) + { + return null; + } + + // In case they throw us lowercase codes in some random patch + code = code.toUpperCase(); + + // The first char of the code (e.g. 'I' in "IFCB") + var pos1 = code.charAt(0); + // The second char of the code (e.g. 'F' in "IFCB") + var pos2 = code.charAt(1); + // The third char of the code (e.g. 'C' in "IFCB") + var pos3 = code.charAt(2); + // The fourth char of the code (e.g. 'B' in "IFCB") + var pos4 = code.charAt(3); + + // Convert those chars to indexes in our `CODE_MAPPING` array + var pos1RowIndex = pos1 - 'A'; + var pos2RowIndex = pos2 - 'A'; + var pos3RowIndex = pos3 - 'A'; + var pos4RowIndex = pos4 - 'A'; + + // Ensure the characters we've received have correctly mapped to indexes we support + if ( + pos1RowIndex < 0 || pos1RowIndex > 8 + || pos2RowIndex < 0 || pos2RowIndex > 8 + || pos3RowIndex < 0 || pos3RowIndex > 8 + || pos4RowIndex < 0 || pos4RowIndex > 8 + ) + { + return null; + } + + var pos1Result = CODE_MAPPING[pos1RowIndex][STRIP_1_INDEX]; + var pos2Result = CODE_MAPPING[pos2RowIndex][STRIP_2_INDEX]; + var pos3Result = CODE_MAPPING[pos3RowIndex][STRIP_3_INDEX]; + var pos4Result = CODE_MAPPING[pos4RowIndex][STRIP_4_INDEX]; + return new int[]{ + pos1Result, + pos2Result, + pos3Result, + pos4Result + }; + } + + public static int calculateDistanceUp(int currentNumber, int targetNumber) + { + if (currentNumber == targetNumber) + { + return 0; + } + + if (currentNumber > targetNumber) + { + return targetNumber - currentNumber + 10; + } + else + { + return targetNumber - currentNumber; + } + } + + public static int calculateDistanceDown(int currentNumber, int targetNumber) + { + if (currentNumber == targetNumber) + { + return 0; + } + + if (currentNumber < targetNumber) + { + return currentNumber - targetNumber + 10; + } + else + { + return currentNumber - targetNumber; + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + if (this.code != null) + { + var currentNumberWidget = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_CURRENT_CHILD_ID); + if (currentNumberWidget != null) + { + var currentNumberModel = currentNumberWidget.getModelId(); + var currentNumber = IntStream.range(0, PUZZLE_NUMBERS.length) + .filter(i -> PUZZLE_NUMBERS[i] == currentNumberModel) + .findFirst() + .orElse(-1); + + var input1 = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_1_CHILD_ID); + var input2 = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_2_CHILD_ID); + var input3 = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_3_CHILD_ID); + var input4 = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_4_CHILD_ID); + + if (input1 == null || input2 == null || input3 == null || input4 == null) + { + // something very wrong + this.distanceUp = -1; + this.distanceDown = -1; + return; + } + + var input1Text = input1.getText(); + var input2Text = input2.getText(); + var input3Text = input3.getText(); + var input4Text = input4.getText(); + + var targetNumber = -1; + + if (Objects.equals(input1Text, "-") || Integer.parseInt(input1.getText()) != this.doorPassword[0]) + { + targetNumber = this.doorPassword[0]; + } + else if (Objects.equals(input2Text, "-") || Integer.parseInt(input2.getText()) != this.doorPassword[1]) + { + targetNumber = this.doorPassword[1]; + } + else if (Objects.equals(input3Text, "-") || Integer.parseInt(input3.getText()) != this.doorPassword[2]) + { + targetNumber = this.doorPassword[2]; + } + else if (Objects.equals(input4Text, "-") || Integer.parseInt(input4.getText()) != this.doorPassword[3]) + { + targetNumber = this.doorPassword[3]; + } + + if (currentNumber == -1 || targetNumber == -1) + { + // something very wrong + this.distanceUp = -1; + this.distanceDown = -1; + } + else + { + this.distanceUp = calculateDistanceUp(currentNumber, targetNumber); + this.distanceDown = calculateDistanceDown(currentNumber, targetNumber); + + this.shouldClickDownInteadOfUp.setShouldPass(this.distanceDown < this.distanceUp); + } + } + return; + } + + var textWidget = client.getWidget(MESBOX_GROUP_ID, MESBOX_CHILD_ID); + if (textWidget == null) + { + return; + } + + var matcher = CODE_PATTERN.matcher(textWidget.getText()); + if (matcher.find()) + { + this.code = matcher.group(1); + this.doorPassword = calculate(this.code); + if (this.doorPassword != null) + { + firstNumberCorrect.setText(String.valueOf(this.doorPassword[0])); + inputFirstCorrect.setId(PUZZLE_NUMBERS[this.doorPassword[0]]); + secondNumberCorrect.setText(String.valueOf(this.doorPassword[1])); + inputSecondCorrect.setId(PUZZLE_NUMBERS[this.doorPassword[1]]); + thirdNumberCorrect.setText(String.valueOf(this.doorPassword[2])); + inputThirdCorrect.setId(PUZZLE_NUMBERS[this.doorPassword[2]]); + fourthNumberCorrect.setText(String.valueOf(this.doorPassword[3])); + inputFourthCorrect.setId(PUZZLE_NUMBERS[this.doorPassword[3]]); + } + updateSteps(); + } + + } + + @Override + public void startUp() + { + updateSteps(); + } + + @Override + protected void setupSteps() + { + this.shouldClickDownInteadOfUp = new ManualRequirement(); + var codeKey = new ItemRequirement("Code key", ItemID.CODE_KEY); + readCode = new ItemStep(getQuestHelper(), "Read the Code key in your inventory.", codeKey.highlighted()); + + clickMetalDoors = new ObjectStep(getQuestHelper(), ObjectID.METAL_DOORS, new WorldPoint(3612, 4582, 0), "Open the metal doors and solve the puzzle.", codeKey); + + var puzzleWidgetOpen = new WidgetPresenceRequirement(PUZZLE_GROUP_ID, PUZZLE_BTN_UP_CHILD_ID); + + solvePuzzleFallback = new DetailedQuestStep(getQuestHelper(), "solve the puzzle somehow"); + + var firstNumberEmpty = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_1_CHILD_ID, "-"); + var secondNumberEmpty = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_2_CHILD_ID, "-"); + var thirdNumberEmpty = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_3_CHILD_ID, "-"); + var fourthNumberEmpty = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_4_CHILD_ID, "-"); + firstNumberCorrect = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_1_CHILD_ID, "X"); + secondNumberCorrect = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_2_CHILD_ID, "X"); + thirdNumberCorrect = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_3_CHILD_ID, "X"); + fourthNumberCorrect = new WidgetTextRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_4_CHILD_ID, "X"); + + inputFirstCorrect = new WidgetModelRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_CURRENT_CHILD_ID, -1); + inputSecondCorrect = new WidgetModelRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_CURRENT_CHILD_ID, -1); + inputThirdCorrect = new WidgetModelRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_CURRENT_CHILD_ID, -1); + inputFourthCorrect = new WidgetModelRequirement(PUZZLE_GROUP_ID, PUZZLE_PASSWORD_CURRENT_CHILD_ID, -1); + + var clickUp = new WidgetStep(getQuestHelper(), "Click the Up button.", new WidgetDetails(PUZZLE_GROUP_ID, PUZZLE_BTN_UP_CHILD_ID)); + clickUp.addExtraWidgetOverlayHintFunction(this::drawDistanceUp); + var clickDown = new WidgetStep(getQuestHelper(), "Click the Down button.", new WidgetDetails(PUZZLE_GROUP_ID, PUZZLE_BTN_DOWN_CHILD_ID)); + clickDown.addExtraWidgetOverlayHintFunction(this::drawDistanceDown); + var submitNumber = new WidgetStep(getQuestHelper(), "Click the Enter button.", new WidgetDetails(PUZZLE_GROUP_ID, PUZZLE_ENTER_CHILD_ID)); + var pressBack = new WidgetStep(getQuestHelper(), "Click the Back button.", new WidgetDetails(PUZZLE_GROUP_ID, PUZZLE_BACK_CHILD_ID)); + + var clickUpOrDown = new ConditionalStep(getQuestHelper(), clickUp); + clickUpOrDown.addStep(shouldClickDownInteadOfUp, clickDown); + + solvePuzzle = new ConditionalStep(getQuestHelper(), pressBack); + solvePuzzle.addStep(not(puzzleWidgetOpen), clickMetalDoors); + solvePuzzle.addStep(and(fourthNumberCorrect, thirdNumberCorrect, secondNumberCorrect, firstNumberCorrect), submitNumber); + solvePuzzle.addStep(and(fourthNumberEmpty, inputFourthCorrect, firstNumberCorrect, secondNumberCorrect, thirdNumberCorrect), submitNumber); + solvePuzzle.addStep(and(fourthNumberEmpty, firstNumberCorrect, secondNumberCorrect, thirdNumberCorrect), clickUpOrDown); + solvePuzzle.addStep(and(thirdNumberEmpty, inputThirdCorrect, firstNumberCorrect, secondNumberCorrect), submitNumber); + solvePuzzle.addStep(and(thirdNumberEmpty, firstNumberCorrect, secondNumberCorrect), clickUpOrDown); + solvePuzzle.addStep(and(secondNumberEmpty, inputSecondCorrect, firstNumberCorrect), submitNumber); + solvePuzzle.addStep(and(secondNumberEmpty, firstNumberCorrect), clickUpOrDown); + solvePuzzle.addStep(and(firstNumberEmpty, inputFirstCorrect), submitNumber); + solvePuzzle.addStep(firstNumberEmpty, clickUpOrDown); + } + + public void drawDistanceUp(Graphics2D graphics, QuestHelperPlugin plugin) + { + super.makeWidgetOverlayHint(graphics, plugin); + + var arrow = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_BTN_DOWN_CHILD_ID); + if (arrow == null) + { + return; + } + + int widgetX = arrow.getCanvasLocation().getX() + (arrow.getWidth() / 2) - 30; + int widgetY = arrow.getCanvasLocation().getY() + (arrow.getHeight() / 2) + 4; + Font font = FontManager.getRunescapeFont().deriveFont(Font.BOLD, 16); + graphics.setFont(font); + graphics.drawString(Integer.toString(this.distanceUp), widgetX, widgetY); + } + + public void drawDistanceDown(Graphics2D graphics, QuestHelperPlugin plugin) + { + super.makeWidgetOverlayHint(graphics, plugin); + + var arrow = client.getWidget(PUZZLE_GROUP_ID, PUZZLE_BTN_DOWN_CHILD_ID); + if (arrow == null) + { + return; + } + + int widgetX = arrow.getCanvasLocation().getX() + (arrow.getWidth() / 2) - 30; + int widgetY = arrow.getCanvasLocation().getY() + (arrow.getHeight() / 2) + 4; + Font font = FontManager.getRunescapeFont().deriveFont(Font.BOLD, 16); + graphics.setFont(font); + graphics.drawString(Integer.toString(this.distanceDown), widgetX, widgetY); + } + + protected void updateSteps() + { + if (this.code == null) + { + startUpStep(readCode); + return; + } + + if (this.doorPassword == null) + { + startUpStep(solvePuzzleFallback); + return; + } + + startUpStep(solvePuzzle); + } + + @Override + public List getSteps() + { + return List.of( + this.readCode, + this.clickMetalDoors, + this.solvePuzzleFallback, + this.solvePuzzle + ); + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TheCurseOfArrav.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TheCurseOfArrav.java new file mode 100644 index 0000000000..5db2c36fab --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TheCurseOfArrav.java @@ -0,0 +1,835 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav; + +import com.questhelper.bank.banktab.BankSlotIcons; +import com.questhelper.collections.ItemCollections; +import com.questhelper.helpers.quests.thecurseofarrav.rubblesolvers.RubbleSolverFour; +import com.questhelper.helpers.quests.thecurseofarrav.rubblesolvers.RubbleSolverOne; +import com.questhelper.helpers.quests.thecurseofarrav.rubblesolvers.RubbleSolverThree; +import com.questhelper.helpers.quests.thecurseofarrav.rubblesolvers.RubbleSolverTwo; +import com.questhelper.panel.PanelDetails; +import com.questhelper.questhelpers.BasicQuestHelper; +import com.questhelper.questinfo.QuestHelperQuest; +import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.requirements.item.TeleportItemRequirement; +import com.questhelper.requirements.player.CombatLevelRequirement; +import com.questhelper.requirements.player.FreeInventorySlotRequirement; +import com.questhelper.requirements.player.SkillRequirement; +import com.questhelper.requirements.quest.QuestRequirement; +import static com.questhelper.requirements.util.LogicHelper.and; +import static com.questhelper.requirements.util.LogicHelper.not; +import static com.questhelper.requirements.util.LogicHelper.or; + +import com.questhelper.requirements.util.Operation; +import com.questhelper.requirements.var.VarbitRequirement; +import com.questhelper.requirements.zone.Zone; +import com.questhelper.requirements.zone.ZoneRequirement; +import com.questhelper.rewards.ExperienceReward; +import com.questhelper.rewards.QuestPointReward; +import com.questhelper.rewards.UnlockReward; +import com.questhelper.steps.ConditionalStep; +import com.questhelper.steps.DetailedQuestStep; +import com.questhelper.steps.ItemStep; +import com.questhelper.steps.NpcStep; +import com.questhelper.steps.ObjectStep; +import com.questhelper.steps.PuzzleWrapperStep; +import com.questhelper.steps.QuestStep; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.runelite.api.ItemID; +import net.runelite.api.NpcID; +import net.runelite.api.NullObjectID; +import net.runelite.api.ObjectID; +import net.runelite.api.QuestState; +import net.runelite.api.Skill; +import net.runelite.api.annotations.Varbit; +import net.runelite.api.coords.WorldPoint; + +/** + * The quest guide for the "The Curse of Arrav" OSRS quest + *

+ * The OSRS wiki guide and Slayermusiq1's Quest Guide was referenced for this guide + */ +@SuppressWarnings("FieldCanBeLocal") +public class TheCurseOfArrav extends BasicQuestHelper +{ + static final @Varbit int VARBIT_SOUTH_LEVER_STATE = 11482; + static final @Varbit int VARBIT_NORTH_LEVER_STATE = 11481; + + // Required items + private ItemRequirement dwellberries3; + private ItemRequirement ringOfLife; + public ItemRequirement anyPickaxe; + private ItemRequirement anyGrappleableCrossbow; + private ItemRequirement mithrilGrapple; + private ItemRequirement insulatedBoots; + + // Recommended items + private TeleportItemRequirement fairyRingDLQ; + private TeleportItemRequirement trollheimTeleport; + private TeleportItemRequirement lumberyardTeleport; + private ItemRequirement golemCombatGear; + private ItemRequirement arravCombatGear; + private ItemRequirement food; + private ItemRequirement staminaPotion; + private ItemRequirement prayerPotion; + private ItemRequirement antiVenom; + private FreeInventorySlotRequirement twoFreeInventorySlots; + + // Mid-quest item requirements + private ItemRequirement firstMastabaKey; + private ItemRequirement secondMastabaKey; + private ItemRequirement canopicJarFullForHeist; + private ItemRequirement basePlans; + private ItemRequirement baseKey; + + // Zones & their requirements + /// Top floor of the tomb (Uzer Mastaba) + private ZoneRequirement insideTomb; + /// Top floor of the tomb (Uzer Mastaba) by the south lever + ZoneRequirement bySouthLever; + /// Top floor of the tomb (Uzer Mastaba) by the north lever + ZoneRequirement byNorthLever; + /// Top floor of the tomb (Uzer Mastaba) in the room with the Golem guard + ZoneRequirement insideGolemArena; + /// Second floor of the tomb (Uzer Mastaba) + private ZoneRequirement insideTombSecondFloor; + ZoneRequirement inTrollheimCave; + ZoneRequirement onTrollweissMountain; + ZoneRequirement inTrollweissCave; + ZoneRequirement inArravHouseFirstRoom; + ZoneRequirement inArravHouseSecondRoom; + private ZoneRequirement inZemouregalsBaseSection1; + private ZoneRequirement inZemouregalsBaseSection2; + private ZoneRequirement inZemouregalsBaseSection3; + private ZoneRequirement inZemouregalsBaseSection4; + private ZoneRequirement inZemouregalsBaseKitchen; + private ZoneRequirement inZemouregalsBaseSewer; + /// After exiting the sewer of Zemouregal's base + ZoneRequirement inZemouregalsBaseSecondPart; + /// In the storage room after exiting the sewer of Zemouregal's base + ZoneRequirement inStorageRoom; + /// Past the Metal door in Zemouregal's base + ZoneRequirement inGrapplePuzzleRoom; + /// Past the Grapple puzzle in Zemouregal's base + ZoneRequirement pastGrapplePuzzleRoom; + + // Steps + /// 0 + 2 + NpcStep startQuest; + + /// 4 + ObjectStep enterTomb; + + /// 6 + 8 + ConditionalStep unlockImposingDoors; + ObjectStep getFirstKey; + ObjectStep getSecondKey; + ObjectStep pullSouthLever; + ObjectStep pullNorthLever; + + /// 10 + ConditionalStep fightGolemCond; + ObjectStep enterGolemArena; + NpcStep fightGolemGuard; + + /// 12 + 14 + private ConditionalStep finishTilePuzzleAndGetCanopicJar; + private ObjectStep enterTombBasement; + private PuzzleWrapperStep solveTilePuzzle; + private ObjectStep searchShelvesForUrn; + private ObjectStep inspectMurals; + private ConditionalStep fillCanopicJar; + private ConditionalStep showCanopicJarToElias; + private ConditionalStep goThroughTrollweissCave1; + private ConditionalStep goThroughTrollweissCave2; + private ConditionalStep goThroughTrollweissCave3; + private ConditionalStep goThroughTrollweissCave4; + private ConditionalStep climbUpToZemouregalsFort; + private ConditionalStep confrontArravInZemouregalsFort; + private ConditionalStep stealBaseKeyForHeist; + private ConditionalStep unlockDoorInZemouregalsBase; + private ConditionalStep getToBackroomsOfZemouregalsBase; + private ConditionalStep unlockMetalDoor; + private ConditionalStep attemptToStealHeart; + private ConditionalStep actuallyConfrontArrav; + private ConditionalStep watchYourVictoryDialog; + private NpcStep finishQuest; + + private PuzzleWrapperStep rubbleMiner1; + private PuzzleWrapperStep rubbleMiner2; + private PuzzleWrapperStep rubbleMiner3; + private PuzzleWrapperStep rubbleMiner4; + private PuzzleWrapperStep metalDoorSolver; + private NpcStep returnToEliasWithBaseItems; + private NpcStep headToZemouregalsBaseAndTalkToElias; + private ObjectStep enterZemouregalsBase; + private DetailedQuestStep getToBackOfZemouregalsBase; + private QuestRequirement haveKilledGolem; + private VarbitRequirement finishedTilePuzzle; + private QuestRequirement haveMadeCanopicJar; + private QuestRequirement haveMinedAFullPath; + private QuestRequirement haveUsedPlans; + private QuestRequirement haveUsedKey; + private QuestRequirement haveMetArrav; + private ObjectStep searchTableForDecoderStrips; + private ObjectStep enterStorageRoom; + private ObjectStep openChestForCodeKey; + private NpcStep interpretPlansWithElias; + private NpcStep fightArrav; + private ObjectStep enterBossRoom; + private ObjectStep grappleAcross; + private ObjectStep openMetalDoors; + ObjectStep getToSouthLever; + ObjectStep leaveSouthLever; + ObjectStep getToNorthLever; + ObjectStep leaveNorthLever; + ItemStep combineJarWithDwellberries; + ItemStep combineJarWithRingOfLife; + NpcStep returnToElias; + ObjectStep headToTrollheim; + ObjectStep continueThroughTrollheimCave; + ObjectStep enterTrollweissCave; + ItemRequirement canopicJarFull; + ObjectStep climbUpstairsAndTalkToArrav; + NpcStep talkToArrav; + ObjectStep goToNextRoom; + ObjectStep searchTapestry; + VarbitRequirement haveUsedKeyOnSouthLever; + VarbitRequirement haveFlippedSouthLever; + VarbitRequirement haveUsedKeyOnNorthLever; + VarbitRequirement haveFlippedNorthLever; + + + @Override + public Map loadSteps() + { + initializeRequirements(); + setupSteps(); + + var steps = new HashMap(); + + steps.put(0, startQuest); + steps.put(2, startQuest); + steps.put(4, enterTomb); + steps.put(6, unlockImposingDoors); + steps.put(8, unlockImposingDoors); + steps.put(10, fightGolemCond); + steps.put(12, finishTilePuzzleAndGetCanopicJar); + steps.put(14, finishTilePuzzleAndGetCanopicJar); + steps.put(16, fillCanopicJar); + steps.put(18, showCanopicJarToElias); + steps.put(20, goThroughTrollweissCave1); + steps.put(22, goThroughTrollweissCave1); + steps.put(24, goThroughTrollweissCave2); + steps.put(26, goThroughTrollweissCave3); + steps.put(28, goThroughTrollweissCave4); + steps.put(30, climbUpToZemouregalsFort); + steps.put(32, confrontArravInZemouregalsFort); + steps.put(34, stealBaseKeyForHeist); + steps.put(36, returnToEliasWithBaseItems); + steps.put(38, interpretPlansWithElias); + steps.put(40, interpretPlansWithElias); + steps.put(42, headToZemouregalsBaseAndTalkToElias); + steps.put(44, unlockDoorInZemouregalsBase); + steps.put(46, getToBackroomsOfZemouregalsBase); + steps.put(48, unlockMetalDoor); + steps.put(50, unlockMetalDoor); + steps.put(52, attemptToStealHeart); + steps.put(54, actuallyConfrontArrav); + steps.put(56, watchYourVictoryDialog); + steps.put(58, finishQuest); + + return steps; + } + + @Override + protected void setupZones() + { + insideTomb = new ZoneRequirement(new Zone(new WorldPoint(3842, 4547, 0), new WorldPoint(3900, 4603, 0))); + bySouthLever = new ZoneRequirement(new Zone(new WorldPoint(3893, 4554, 0), new WorldPoint(3894, 4552, 0))); + byNorthLever = new ZoneRequirement(new Zone(new WorldPoint(3894, 4597, 0), new WorldPoint(3893, 4599, 0))); + insideGolemArena = new ZoneRequirement(new Zone(new WorldPoint(3856, 4592, 0), new WorldPoint(3884, 4599, 0))); + insideTombSecondFloor = new ZoneRequirement( + new Zone(new WorldPoint(3719, 4674, 0), new WorldPoint(3770, 4732, 0)), + new Zone(new WorldPoint(3845, 4674, 0), new WorldPoint(3900, 4732, 0)) + ); + + inTrollheimCave = new ZoneRequirement(new Zone(11167)); + onTrollweissMountain = new ZoneRequirement(new Zone(11068)); + inTrollweissCave = new ZoneRequirement(new Zone(11168)); + inArravHouseFirstRoom = new ZoneRequirement(new Zone(new WorldPoint(2848, 3868, 0), new WorldPoint(2858, 3873, 0))); + inArravHouseSecondRoom = new ZoneRequirement(new Zone(new WorldPoint(2863, 3865, 0), new WorldPoint(2859, 3873, 0))); + + // Right as you head into the base + inZemouregalsBaseSection1 = new ZoneRequirement(new Zone(new WorldPoint(3536, 4577, 0), new WorldPoint(3564, 4547, 0))); + // After you've passed the first door + inZemouregalsBaseSection2 = new ZoneRequirement( + new Zone(new WorldPoint(3535, 4562, 0), new WorldPoint(3523, 4602, 0)), + new Zone(new WorldPoint(3523, 4602, 0), new WorldPoint(3539, 4594, 0)) + ); + // After you've passed the second door + inZemouregalsBaseSection3 = new ZoneRequirement(new Zone(new WorldPoint(3576, 4610, 0), new WorldPoint(3540, 4588, 0))); + // After you've passed the third door (the one requiring the base key) + inZemouregalsBaseSection4 = new ZoneRequirement(new Zone(new WorldPoint(3577, 4615, 0), new WorldPoint(3605, 4592, 0))); + inZemouregalsBaseKitchen = new ZoneRequirement(new Zone(new WorldPoint(3613, 4604, 0), new WorldPoint(3606, 4598, 0))); + inZemouregalsBaseSewer = new ZoneRequirement(new Zone(14919)); + + inZemouregalsBaseSecondPart = new ZoneRequirement(new Zone(new WorldPoint(3590, 4538, 0), new WorldPoint(3622, 4597, 0))); + inStorageRoom = new ZoneRequirement(new Zone(new WorldPoint(3614, 4571, 0), new WorldPoint(3605, 4563, 0))); + inGrapplePuzzleRoom = new ZoneRequirement(new Zone(new WorldPoint(3613, 4587, 0), new WorldPoint(3625, 4579, 0))); + pastGrapplePuzzleRoom = new ZoneRequirement(new Zone(new WorldPoint(3621, 4589, 0), new WorldPoint(3645, 4578, 0))); + } + + @Override + protected void setupRequirements() + { + haveUsedKeyOnSouthLever = new VarbitRequirement(VARBIT_SOUTH_LEVER_STATE, 1, Operation.GREATER_EQUAL); + haveFlippedSouthLever = new VarbitRequirement(VARBIT_SOUTH_LEVER_STATE, 2); + haveUsedKeyOnNorthLever = new VarbitRequirement(VARBIT_NORTH_LEVER_STATE, 1, Operation.GREATER_EQUAL); + haveFlippedNorthLever = new VarbitRequirement(VARBIT_NORTH_LEVER_STATE, 2); + haveKilledGolem = new QuestRequirement(QuestHelperQuest.THE_CURSE_OF_ARRAV, 12); + finishedTilePuzzle = new VarbitRequirement(11483, 1); + haveMadeCanopicJar = new QuestRequirement(QuestHelperQuest.THE_CURSE_OF_ARRAV, 18); + haveMinedAFullPath = new QuestRequirement(QuestHelperQuest.THE_CURSE_OF_ARRAV, 30); + haveUsedPlans = new QuestRequirement(QuestHelperQuest.THE_CURSE_OF_ARRAV, 38); + haveUsedKey = new QuestRequirement(QuestHelperQuest.THE_CURSE_OF_ARRAV, 46); + haveMetArrav = new QuestRequirement(QuestHelperQuest.THE_CURSE_OF_ARRAV, 54); + + // Required items + dwellberries3 = new ItemRequirement("Dwellberries", ItemID.DWELLBERRIES, 3); + dwellberries3.setConditionToHide(haveMadeCanopicJar); + ringOfLife = new ItemRequirement("Ring of life", ItemID.RING_OF_LIFE); + ringOfLife.setConditionToHide(haveMadeCanopicJar); + anyPickaxe = new ItemRequirement("Any pickaxe", ItemCollections.PICKAXES).isNotConsumed(); + anyPickaxe.setConditionToHide(haveMinedAFullPath); + anyGrappleableCrossbow = new ItemRequirement("Any crossbow", ItemCollections.CROSSBOWS).isNotConsumed(); + anyGrappleableCrossbow.setConditionToHide(haveMetArrav); + mithrilGrapple = new ItemRequirement("Mith grapple", ItemID.MITH_GRAPPLE_9419).isNotConsumed(); + mithrilGrapple.setConditionToHide(haveMetArrav); + insulatedBoots = new ItemRequirement("Insulated boots", ItemID.INSULATED_BOOTS).isNotConsumed(); + + // Recommended items + fairyRingDLQ = new TeleportItemRequirement("Fairy Ring [DLQ]", ItemCollections.FAIRY_STAFF); + trollheimTeleport = new TeleportItemRequirement("Trollheim Teleport", ItemID.TROLLHEIM_TELEPORT); + trollheimTeleport.addAlternates(ItemCollections.GHOMMALS_HILT); + lumberyardTeleport = new TeleportItemRequirement("Lumberyard teleport", ItemID.LUMBERYARD_TELEPORT); + staminaPotion = new ItemRequirement("Stamina potion", ItemCollections.STAMINA_POTIONS, 1); + prayerPotion = new ItemRequirement("Prayer potion", ItemCollections.PRAYER_POTIONS, 1); + antiVenom = new ItemRequirement("Anti-venom", ItemCollections.ANTIVENOMS, 1); + golemCombatGear = new ItemRequirement("Crush or ranged combat gear to fight the Golem guard", -1, -1); + golemCombatGear.setDisplayItemId(BankSlotIcons.getCombatGear()); + golemCombatGear.setConditionToHide(haveKilledGolem); + arravCombatGear = new ItemRequirement("Ranged or melee combat gear for killing Arrav", -1, -1); + arravCombatGear.setTooltip("If you bring Melee gear, it's advisable to bring some ranged weapon swap like darts for killing the Zombies as they spawn"); + arravCombatGear.setDisplayItemId(BankSlotIcons.getRangedCombatGear()); + food = new ItemRequirement("Food", ItemCollections.GOOD_EATING_FOOD, -1); + twoFreeInventorySlots = new FreeInventorySlotRequirement(2); + + // Mid-quest item requirements + firstMastabaKey = new ItemRequirement("Mastaba Key", ItemID.MASTABA_KEY); + secondMastabaKey = new ItemRequirement("Mastaba Key", ItemID.MASTABA_KEY_30309); + canopicJarFull = new ItemRequirement("Canopic jar (full)", ItemID.CANOPIC_JAR_FULL); + canopicJarFullForHeist = new ItemRequirement("Canopic jar (full)", ItemID.CANOPIC_JAR_FULL); + canopicJarFullForHeist.setTooltip("You can get a new one from Elias at the entrance of Zemouregal's base if you've lost it."); + } + + public void setupSteps() + { + var unreachableState = new DetailedQuestStep(this, "This state should not be reachable, please make a report with a screenshot in the Quest Helper discord."); + + startQuest = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Talk to Elias south of Ruins of Uzer to start the quest."); + startQuest.addTeleport(fairyRingDLQ); + startQuest.addDialogStep("Yes."); + + enterTomb = new ObjectStep(this, ObjectID.ENTRY_50201, new WorldPoint(3486, 3023, 0), "Enter the tomb south-west of Elias.", dwellberries3, ringOfLife, twoFreeInventorySlots, golemCombatGear); + + getFirstKey = new ObjectStep(this, ObjectID.SKELETON_50350, new WorldPoint(3875, 4554, 0), "Get the first Mastaba key from the skeleton in the cave south of the entrance."); + getSecondKey = new ObjectStep(this, ObjectID.SKELETON_50353, new WorldPoint(3880, 4585, 0), "Get the second Mastaba key from the skeleton east of the entrance."); + getToSouthLever = new ObjectStep(this, ObjectID.ODD_MARKINGS_50207, new WorldPoint(3891, 4554, 0), "Search the Odd markings to the south to get to the south lever. Search the markings again if you fail."); + pullSouthLever = new ObjectStep(this, ObjectID.LEVER_50205, new WorldPoint(3894, 4553, 0), "Pull the lever to the south-east.", + secondMastabaKey.hideConditioned(haveUsedKeyOnSouthLever)); + pullSouthLever.addDialogStep("Yes."); + leaveSouthLever = new ObjectStep(this, ObjectID.ODD_MARKINGS_50208, new WorldPoint(3892, 4554, 0), "Search the Odd markings next to you to get out."); + pullSouthLever.addSubSteps(getToSouthLever, leaveSouthLever); + + getToNorthLever = new ObjectStep(this, ObjectID.ODD_MARKINGS_50208, new WorldPoint(3891, 4597, 0), "Search the Odd markings to the north to get to the north lever. Search the markings again if you fail."); + pullNorthLever = new ObjectStep(this, ObjectID.LEVER_50205, new WorldPoint(3894, 4598, 0), "Pull the lever to the north-east.", firstMastabaKey.hideConditioned(haveUsedKeyOnNorthLever)); + pullNorthLever.addDialogStep("Yes."); + leaveNorthLever = new ObjectStep(this, ObjectID.ODD_MARKINGS_50207, new WorldPoint(3892, 4597, 0), "Search the Odd markings next to you to get out."); + pullNorthLever.addSubSteps(getToNorthLever, leaveNorthLever); + + var haveOrUsedFirstKey = or(firstMastabaKey, haveUsedKeyOnNorthLever); + var haveOrUsedSecondKey = or(secondMastabaKey, haveUsedKeyOnSouthLever); + var haveOrUsedBothKeys = and(haveOrUsedFirstKey, haveOrUsedSecondKey); + + var stepsNearNorthLever = new ConditionalStep(this, leaveNorthLever); + stepsNearNorthLever.addStep(and(haveOrUsedFirstKey, not(haveFlippedNorthLever)), pullNorthLever); + + var stepsNearSouthLever = new ConditionalStep(this, leaveSouthLever); + stepsNearSouthLever.addStep(and(haveOrUsedSecondKey, not(haveFlippedSouthLever)), pullSouthLever); + + unlockImposingDoors = new ConditionalStep(this, enterTomb); + unlockImposingDoors.addStep(not(insideTomb), enterTomb); + unlockImposingDoors.addStep(bySouthLever, stepsNearSouthLever); + unlockImposingDoors.addStep(byNorthLever, stepsNearNorthLever); + unlockImposingDoors.addStep(and(haveOrUsedBothKeys, not(haveFlippedSouthLever)), getToSouthLever); + unlockImposingDoors.addStep(and(haveOrUsedBothKeys, not(haveFlippedNorthLever)), getToNorthLever); + unlockImposingDoors.addStep(not(haveOrUsedFirstKey), getFirstKey); + unlockImposingDoors.addStep(not(haveOrUsedSecondKey), getSecondKey); + + // Once the north lever is pulled, quest varbit changed from 6 to 8, then 8 to 10 at the same tick + // This might have to do with which order you pulled the levers in + + enterGolemArena = new ObjectStep(this, ObjectID.IMPOSING_DOORS_50211, new WorldPoint(3885, 4597, 0), "Open the imposing doors, ready to fight the Golem guard."); + fightGolemGuard = new NpcStep(this, NpcID.GOLEM_GUARD, new WorldPoint(3860, 4595, 0), "Fight the Golem guard. It is weak to crush style weapons. Use Protect from Melee to avoid damage from his attacks. When the screen shakes, step away from him to avoid taking damage."); + fightGolemCond = new ConditionalStep(this, enterGolemArena); + // Get inside the tomb if you're not already inside. In case the user has teleported out or died to golem? + fightGolemCond.addStep(not(insideTomb), enterTomb); + fightGolemCond.addStep(byNorthLever, leaveNorthLever); + fightGolemCond.addStep(bySouthLever, leaveSouthLever); + fightGolemCond.addStep(insideGolemArena, fightGolemGuard); + + var enterGolemArenaWithoutFight = new ObjectStep(this, ObjectID.IMPOSING_DOORS_50211, new WorldPoint(3885, 4597, 0), "Open the imposing doors to the north-east of the tomb."); + enterTombBasement = new ObjectStep(this, ObjectID.STAIRS_55785, new WorldPoint(3860, 4596, 0), "Climb the stairs down the tomb basement."); + enterTombBasement.addSubSteps(enterGolemArenaWithoutFight); + + solveTilePuzzle = new TilePuzzleSolver(this).puzzleWrapStep("Move across the floor tile puzzle."); + + searchShelvesForUrn = new ObjectStep(this, ObjectID.SHELVES_55796, new WorldPoint(3854, 4722, 0), "Search the shelves to the west for an oil-filled canopic jar."); + var oilFilledCanopicJar = new ItemRequirement("Oil-filled canopic jar", ItemID.CANOPIC_JAR_OIL); + + inspectMurals = new ObjectStep(this, ObjectID.MURAL_55790, new WorldPoint(3852, 4687, 0), "Inspect the murals in the room to the south.", oilFilledCanopicJar); + + finishTilePuzzleAndGetCanopicJar = new ConditionalStep(this, enterTomb); + finishTilePuzzleAndGetCanopicJar.addStep(and(insideTombSecondFloor, finishedTilePuzzle, oilFilledCanopicJar), inspectMurals); + finishTilePuzzleAndGetCanopicJar.addStep(and(insideTombSecondFloor, finishedTilePuzzle), searchShelvesForUrn); + finishTilePuzzleAndGetCanopicJar.addStep(insideTombSecondFloor, solveTilePuzzle); + finishTilePuzzleAndGetCanopicJar.addStep(and(insideTomb, insideGolemArena), enterTombBasement); + finishTilePuzzleAndGetCanopicJar.addStep(and(insideTomb), enterGolemArenaWithoutFight); + + var oilAndBerryFilledCanopicJar = new ItemRequirement("Canopic jar (oil and berries)", ItemID.CANOPIC_JAR_OIL_AND_BERRIES); + + combineJarWithDwellberries = new ItemStep(this, "Put the Dwellberries in the Canopic jar.", oilFilledCanopicJar.highlighted(), dwellberries3.highlighted(), ringOfLife); + combineJarWithRingOfLife = new ItemStep(this, "Put the Ring of life in the Canopic jar.", oilAndBerryFilledCanopicJar.highlighted(), ringOfLife.highlighted()); + + fillCanopicJar = new ConditionalStep(this, unreachableState); + fillCanopicJar.addStep(oilAndBerryFilledCanopicJar, combineJarWithRingOfLife); + fillCanopicJar.addStep(oilFilledCanopicJar, combineJarWithDwellberries); + fillCanopicJar.addStep(and(insideTombSecondFloor, finishedTilePuzzle), searchShelvesForUrn); + fillCanopicJar.addStep(not(insideTomb), enterTomb); + fillCanopicJar.addStep(and(insideTomb, insideGolemArena), enterTombBasement); + fillCanopicJar.addStep(and(insideTomb), enterGolemArenaWithoutFight); + + + returnToElias = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Return to Elias south of Ruins of Uzer, either by walking out of the tomb or using the fairy ring.", canopicJarFull); + returnToElias.addTeleport(fairyRingDLQ); + var returnToEliasByWalking = new ObjectStep(this, ObjectID.STAIRS_55786, new WorldPoint(3894, 4714, 0), "Return to Elias south of Ruins of Uzer, either by walking out of the tomb or using the fairy ring."); + returnToEliasByWalking.addTeleport(fairyRingDLQ); + var returnToEliasByWalkingMidway = new ObjectStep(this, ObjectID.STAIRS_50202, new WorldPoint(3848, 4577, 0), "Return to Elias south of Ruins of Uzer, either by walking out of the tomb or using the fairy ring."); + returnToEliasByWalkingMidway.addTeleport(fairyRingDLQ); + var returnToEliasByWalkingMidwayGolem = new ObjectStep(this, ObjectID.IMPOSING_DOORS_50211, new WorldPoint(3885, 4597, 0), "Return to Elias south of Ruins of Uzer, either by walking out of the tomb or using the fairy ring."); + returnToEliasByWalkingMidwayGolem.addTeleport(fairyRingDLQ); + + returnToElias.addSubSteps(returnToEliasByWalking, returnToEliasByWalkingMidway, returnToEliasByWalkingMidwayGolem); + + showCanopicJarToElias = new ConditionalStep(this, returnToElias); + showCanopicJarToElias.addStep(insideTombSecondFloor, returnToEliasByWalking); + showCanopicJarToElias.addStep(insideGolemArena, returnToEliasByWalkingMidwayGolem); + showCanopicJarToElias.addStep(insideTomb, returnToEliasByWalkingMidway); + // ardy cloak + fairy ring takes 50s, walking takes 1m12s + + headToTrollheim = new ObjectStep(this, ObjectID.CAVE_ENTRANCE_5007, new WorldPoint(2821, 3744, 0), "Enter the cave next to Trollheim. You can use a Trollheim teleport tablet or the GWD Ghommal's Hilt teleport to get close.", anyPickaxe); + headToTrollheim.addTeleport(trollheimTeleport); + + continueThroughTrollheimCave = new ObjectStep(this, ObjectID.CREVASSE, new WorldPoint(2772, 10233, 0), "Continue through the Trollheim cave, exiting at the Crevasse to the north-west. Use Protect from Melee to avoid taking damage from the Ice Trolls.", anyPickaxe); + + enterTrollweissCave = new ObjectStep(this, ObjectID.CAVE_55779, new WorldPoint(2809, 3861, 0), "Enter the Trollweiss cave to the east.", anyPickaxe); + + var rubbleMiner1Real = new RubbleSolverOne(this); + var rubbleMiner2Real = new RubbleSolverTwo(this); + var rubbleMiner3Real = new RubbleSolverThree(this); + var rubbleMiner4Real = new RubbleSolverFour(this); + + rubbleMiner1 = rubbleMiner1Real.puzzleWrapStep("Mine the rubble and make your way through the cave."); + rubbleMiner2 = rubbleMiner2Real.puzzleWrapStep("Mine the rubble and make your way through the cave."); + rubbleMiner3 = rubbleMiner3Real.puzzleWrapStep("Mine the rubble and make your way through the cave."); + rubbleMiner4 = rubbleMiner4Real.puzzleWrapStep("Mine the rubble and make your way through the cave."); + + rubbleMiner1Real.addSubSteps(rubbleMiner2, rubbleMiner3, rubbleMiner4); + + + goThroughTrollweissCave1 = new ConditionalStep(this, headToTrollheim); + goThroughTrollweissCave1.addStep(inTrollweissCave, rubbleMiner1); + goThroughTrollweissCave1.addStep(onTrollweissMountain, enterTrollweissCave); + goThroughTrollweissCave1.addStep(inTrollheimCave, continueThroughTrollheimCave); + + goThroughTrollweissCave2 = new ConditionalStep(this, headToTrollheim); + goThroughTrollweissCave2.addStep(inTrollweissCave, rubbleMiner2); + goThroughTrollweissCave2.addStep(onTrollweissMountain, enterTrollweissCave); + goThroughTrollweissCave2.addStep(inTrollheimCave, continueThroughTrollheimCave); + + goThroughTrollweissCave3 = new ConditionalStep(this, headToTrollheim); + goThroughTrollweissCave3.addStep(inTrollweissCave, rubbleMiner3); + goThroughTrollweissCave3.addStep(onTrollweissMountain, enterTrollweissCave); + goThroughTrollweissCave3.addStep(inTrollheimCave, continueThroughTrollheimCave); + + goThroughTrollweissCave4 = new ConditionalStep(this, headToTrollheim); + goThroughTrollweissCave4.addStep(inTrollweissCave, rubbleMiner4); + goThroughTrollweissCave4.addStep(onTrollweissMountain, enterTrollweissCave); + goThroughTrollweissCave4.addStep(inTrollheimCave, continueThroughTrollheimCave); + + climbUpstairsAndTalkToArrav = new ObjectStep(this, ObjectID.STAIRS_50508, new WorldPoint(2811, 10267, 0), "Climb up the stairs in the room with the red tile floor and talk to Arrav."); + climbUpToZemouregalsFort = new ConditionalStep(this, headToTrollheim); + climbUpToZemouregalsFort.addStep(inTrollweissCave, climbUpstairsAndTalkToArrav); + climbUpToZemouregalsFort.addStep(onTrollweissMountain, enterTrollweissCave); + climbUpToZemouregalsFort.addStep(inTrollheimCave, continueThroughTrollheimCave); + + talkToArrav = new NpcStep(this, NpcID.ARRAV_14129, new WorldPoint(2856, 3871, 0), "Talk to Arrav."); + + confrontArravInZemouregalsFort = new ConditionalStep(this, headToTrollheim); + confrontArravInZemouregalsFort.addStep(inArravHouseFirstRoom, talkToArrav); + confrontArravInZemouregalsFort.addStep(inTrollweissCave, climbUpstairsAndTalkToArrav); + confrontArravInZemouregalsFort.addStep(onTrollweissMountain, enterTrollweissCave); + confrontArravInZemouregalsFort.addStep(inTrollheimCave, continueThroughTrollheimCave); + + goToNextRoom = new ObjectStep(this, ObjectID.DOOR_50514, new WorldPoint(2859, 3870, 0), "Enter the room to your east and search the tapestry for something to help you with your heist."); + searchTapestry = new ObjectStep(this, ObjectID.TAPESTRY_50516, new WorldPoint(2861, 3865, 0), "Search the tapestry in the south of the room."); + stealBaseKeyForHeist = new ConditionalStep(this, headToTrollheim); + stealBaseKeyForHeist.addStep(inArravHouseSecondRoom, searchTapestry); + stealBaseKeyForHeist.addStep(inArravHouseFirstRoom, goToNextRoom); + stealBaseKeyForHeist.addStep(inTrollweissCave, climbUpstairsAndTalkToArrav); + stealBaseKeyForHeist.addStep(onTrollweissMountain, enterTrollweissCave); + stealBaseKeyForHeist.addStep(inTrollheimCave, continueThroughTrollheimCave); + + var tapestryFindText = "Can be acquired by heading back to Zemouregal's Fort past the Trollweiss mining dungeon and searching the tapestry."; + basePlans = new ItemRequirement("Base plans", ItemID.BASE_PLANS); + basePlans.setConditionToHide(haveUsedPlans); + basePlans.setTooltip(tapestryFindText); // only needed until varbit 38 + baseKey = new ItemRequirement("Base key", ItemID.BASE_KEY); + baseKey.setConditionToHide(haveUsedKey); + baseKey.setTooltip(tapestryFindText); // no longer needed when varbit hits 46 + + returnToEliasWithBaseItems = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Return to Elias south of Ruins of Uzer and ask him for help interpreting the plans.", basePlans, baseKey); + returnToEliasWithBaseItems.addTeleport(fairyRingDLQ); + + interpretPlansWithElias = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Return to Elias south of Ruins of Uzer and ask him for help interpreting the plans.", baseKey); + interpretPlansWithElias.addTeleport(fairyRingDLQ); + + returnToEliasWithBaseItems.addSubSteps(interpretPlansWithElias); + + // 40 -> 42 + // 9658: 5 -> 6 + + headToZemouregalsBaseAndTalkToElias = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3341, 3516, 0), "Head to Zemouregal's base east of Varrock's sawmill and talk to Elias.", anyGrappleableCrossbow, mithrilGrapple, arravCombatGear, insulatedBoots, canopicJarFullForHeist, baseKey); + headToZemouregalsBaseAndTalkToElias.addDialogStep("Ready when you are."); + headToZemouregalsBaseAndTalkToElias.addTeleport(lumberyardTeleport); + enterZemouregalsBase = new ObjectStep(this, NullObjectID.NULL_50689, new WorldPoint(3343, 3515, 0), "Enter Zemouregal's base east of Varrock's sawmill.", anyGrappleableCrossbow, mithrilGrapple, arravCombatGear, insulatedBoots, canopicJarFullForHeist, baseKey); + enterZemouregalsBase.addTeleport(lumberyardTeleport); + + + getToBackOfZemouregalsBase = new DetailedQuestStep(this, "Make your way to the back of Zemouregal's base. Protect from Melee against the zombies to avoid most damage."); + var passZemouregalsBaseDoor1 = new ObjectStep(this, ObjectID.GATE_50149, new WorldPoint(3536, 4571, 0), "Open the gate and make your way to the back of Zemouregal's base. Protect from Melee against the zombies to avoid most damage.", baseKey, canopicJarFullForHeist, insulatedBoots); + var passZemouregalsBaseDoor2 = new ObjectStep(this, ObjectID.GATE_50150, new WorldPoint(3540, 4597, 0), "Open the gate and make your way to the back of Zemouregal's base. Protect from Melee against the zombies to avoid most damage.", baseKey, canopicJarFullForHeist, insulatedBoots); + var passZemouregalsBaseDoor3 = new ObjectStep(this, ObjectID.DOOR_50152, new WorldPoint(3576, 4604, 0), "Open the door and make your way to the back of Zemouregal's base. Protect from Melee against the zombies to avoid most damage.", baseKey, canopicJarFullForHeist, insulatedBoots); + var passZemouregalsBaseDoor4 = new ObjectStep(this, ObjectID.GATE_50537, new WorldPoint(3605, 4603, 0), "Open the gate and make your way to the back of Zemouregal's base.", canopicJarFullForHeist, insulatedBoots); + + unlockDoorInZemouregalsBase = new ConditionalStep(this, enterZemouregalsBase); + unlockDoorInZemouregalsBase.addStep(inZemouregalsBaseSection3, passZemouregalsBaseDoor3); + unlockDoorInZemouregalsBase.addStep(inZemouregalsBaseSection2, passZemouregalsBaseDoor2); + unlockDoorInZemouregalsBase.addStep(inZemouregalsBaseSection1, passZemouregalsBaseDoor1); + + var enterZemouregalsBaseSewer = new ObjectStep(this, ObjectID.PIPE_50523, new WorldPoint(3609, 4598, 0), "Enter the sewers and make your way to the back of Zemouregal's base.", canopicJarFullForHeist, insulatedBoots.highlighted().equipped()); + + var exitZemouregalsBaseSewer = new ObjectStep(this, ObjectID.PIPE_50525, new WorldPoint(3741, 4573, 0), "Head south to exit the sewers and make your way to the back of Zemouregal's base.", canopicJarFullForHeist, insulatedBoots.highlighted().equipped()); + + getToBackOfZemouregalsBase.addSubSteps(passZemouregalsBaseDoor1, passZemouregalsBaseDoor2, passZemouregalsBaseDoor3, passZemouregalsBaseDoor4, enterZemouregalsBaseSewer, exitZemouregalsBaseSewer); + + // 44 -> 46 when opening door, consuming the base key + + getToBackroomsOfZemouregalsBase = new ConditionalStep(this, enterZemouregalsBase); + getToBackroomsOfZemouregalsBase.addStep(inZemouregalsBaseSewer, exitZemouregalsBaseSewer); + getToBackroomsOfZemouregalsBase.addStep(inZemouregalsBaseKitchen, enterZemouregalsBaseSewer); + getToBackroomsOfZemouregalsBase.addStep(inZemouregalsBaseSection4, passZemouregalsBaseDoor4); + getToBackroomsOfZemouregalsBase.addStep(inZemouregalsBaseSection3, passZemouregalsBaseDoor3); + getToBackroomsOfZemouregalsBase.addStep(inZemouregalsBaseSection2, passZemouregalsBaseDoor2); + getToBackroomsOfZemouregalsBase.addStep(inZemouregalsBaseSection1, passZemouregalsBaseDoor1); + + searchTableForDecoderStrips = new ObjectStep(this, ObjectID.TABLE_50533, "Search the table for some decoder strips."); + enterStorageRoom = new ObjectStep(this, ObjectID.GATE_50537, new WorldPoint(3609, 4572, 0), "Enter the storage room to the south-east."); + var exitStorageRoom = new ObjectStep(this, ObjectID.GATE_50537, new WorldPoint(3609, 4572, 0), "Exit the storage room."); + var codeKey = new ItemRequirement("Code key", ItemID.CODE_KEY); + + openChestForCodeKey = new ObjectStep(this, ObjectID.CHEST_50530, new WorldPoint(3609, 4565, 0), "Open the chest for the code key."); + + var metalDoorSolverReal = new MetalDoorSolver(this); + metalDoorSolverReal.addSubSteps(exitStorageRoom); + + this.metalDoorSolver = metalDoorSolverReal.puzzleWrapStep(); + + + // also used for 50 + unlockMetalDoor = new ConditionalStep(this, enterZemouregalsBase); + unlockMetalDoor.addStep(and(inZemouregalsBaseSecondPart, not(inStorageRoom), codeKey), metalDoorSolver); + unlockMetalDoor.addStep(and(inStorageRoom, codeKey), exitStorageRoom); + unlockMetalDoor.addStep(and(inStorageRoom), openChestForCodeKey); + unlockMetalDoor.addStep(and(inZemouregalsBaseSecondPart, not(codeKey)), enterStorageRoom); + unlockMetalDoor.addStep(inZemouregalsBaseSewer, exitZemouregalsBaseSewer); + unlockMetalDoor.addStep(inZemouregalsBaseKitchen, enterZemouregalsBaseSewer); + unlockMetalDoor.addStep(inZemouregalsBaseSection4, passZemouregalsBaseDoor4); + unlockMetalDoor.addStep(inZemouregalsBaseSection3, passZemouregalsBaseDoor3); + unlockMetalDoor.addStep(inZemouregalsBaseSection2, passZemouregalsBaseDoor2); + unlockMetalDoor.addStep(inZemouregalsBaseSection1, passZemouregalsBaseDoor1); + + openMetalDoors = new ObjectStep(this, ObjectID.METAL_DOORS, new WorldPoint(3612, 4582, 0), "Step through through the metal doors.", canopicJarFullForHeist, anyGrappleableCrossbow, mithrilGrapple); + + grappleAcross = new ObjectStep(this, ObjectID.PIPE_50542, new WorldPoint(3615, 4582, 0), "Grapple across the pipe", canopicJarFullForHeist, anyGrappleableCrossbow.highlighted().equipped(), mithrilGrapple.highlighted().equipped()); + + + enterBossRoom = new ObjectStep(this, ObjectID.PEDESTAL_50539, new WorldPoint(3638, 4582, 0), "Attempt to take Arrav's heart from the pedestal, ready for a fight with Arrav. Kill zombies as they appear (ranged weapons are handy here). Avoid the venom pools they spawn. Use Protect from Melee to avoid some of the incoming damage. When Arrav throws an axe towards you, step to the side or behind him.", canopicJarFullForHeist, arravCombatGear); + enterBossRoom.setOverlayText("Attempt to take Arrav's heart from the pedestal, ready for a fight with Arrav. Some hints are available in the sidebar."); + + attemptToStealHeart = new ConditionalStep(this, enterZemouregalsBase); + attemptToStealHeart.addStep(pastGrapplePuzzleRoom, enterBossRoom); + attemptToStealHeart.addStep(inGrapplePuzzleRoom, grappleAcross); + attemptToStealHeart.addStep(inZemouregalsBaseSecondPart, openMetalDoors); + attemptToStealHeart.addStep(inZemouregalsBaseSewer, exitZemouregalsBaseSewer); + attemptToStealHeart.addStep(inZemouregalsBaseKitchen, enterZemouregalsBaseSewer); + attemptToStealHeart.addStep(inZemouregalsBaseSection4, passZemouregalsBaseDoor4); + attemptToStealHeart.addStep(inZemouregalsBaseSection3, passZemouregalsBaseDoor3); + attemptToStealHeart.addStep(inZemouregalsBaseSection2, passZemouregalsBaseDoor2); + attemptToStealHeart.addStep(inZemouregalsBaseSection1, passZemouregalsBaseDoor1); + + // User has engaged Arrav + + fightArrav = new NpcStep(this, NpcID.ARRAV_14132, new WorldPoint(3635, 4582, 0), "Fight Arrav. Kill zombies as they appear (ranged weapons are handy here). Avoid the venom pools they spawn. Use Protect from Melee to avoid some of the incoming damage. When Arrav throws an axe towards you, step to the side or behind him.", canopicJarFullForHeist); + fightArrav.setOverlayText("Fight Arrav. Some hints are available in the sidebar."); + actuallyConfrontArrav = new ConditionalStep(this, enterZemouregalsBase); + actuallyConfrontArrav.addStep(or(pastGrapplePuzzleRoom, inGrapplePuzzleRoom), fightArrav); + actuallyConfrontArrav.addStep(inZemouregalsBaseSecondPart, openMetalDoors); + actuallyConfrontArrav.addStep(inZemouregalsBaseSewer, exitZemouregalsBaseSewer); + actuallyConfrontArrav.addStep(inZemouregalsBaseKitchen, enterZemouregalsBaseSewer); + actuallyConfrontArrav.addStep(inZemouregalsBaseSection4, passZemouregalsBaseDoor4); + actuallyConfrontArrav.addStep(inZemouregalsBaseSection3, passZemouregalsBaseDoor3); + actuallyConfrontArrav.addStep(inZemouregalsBaseSection2, passZemouregalsBaseDoor2); + actuallyConfrontArrav.addStep(inZemouregalsBaseSection1, passZemouregalsBaseDoor1); + + + // 54 -> 56 when beating arrav + + var watchTheDialog = new DetailedQuestStep(this, "Watch the dialog."); + fightArrav.addSubSteps(watchTheDialog); + watchYourVictoryDialog = new ConditionalStep(this, enterZemouregalsBase); + watchYourVictoryDialog.addStep(or(pastGrapplePuzzleRoom, inGrapplePuzzleRoom), watchTheDialog); + watchYourVictoryDialog.addStep(inZemouregalsBaseSecondPart, openMetalDoors); + watchYourVictoryDialog.addStep(inZemouregalsBaseSewer, exitZemouregalsBaseSewer); + watchYourVictoryDialog.addStep(inZemouregalsBaseKitchen, enterZemouregalsBaseSewer); + watchYourVictoryDialog.addStep(inZemouregalsBaseSection4, passZemouregalsBaseDoor4); + watchYourVictoryDialog.addStep(inZemouregalsBaseSection3, passZemouregalsBaseDoor3); + watchYourVictoryDialog.addStep(inZemouregalsBaseSection2, passZemouregalsBaseDoor2); + watchYourVictoryDialog.addStep(inZemouregalsBaseSection1, passZemouregalsBaseDoor1); + + finishQuest = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Talk to Elias to finish the quest."); + } + + @Override + public List getItemRequirements() + { + return List.of( + dwellberries3, + ringOfLife, + anyPickaxe, + anyGrappleableCrossbow, + mithrilGrapple, + insulatedBoots + ); + } + + @Override + public List getItemRecommended() + { + return List.of( + staminaPotion, + prayerPotion, + antiVenom, + golemCombatGear, + arravCombatGear, + food, + fairyRingDLQ, + trollheimTeleport, + lumberyardTeleport + ); + } + + @Override + public List getGeneralRecommended() + { + return List.of( + twoFreeInventorySlots, + new CombatLevelRequirement(85), + new SkillRequirement(Skill.PRAYER, 43, false, "43 Prayer to use Protect from Melee") + ); + } + + @Override + public List getGeneralRequirements() + { + return List.of( + new QuestRequirement(QuestHelperQuest.DEFENDER_OF_VARROCK, QuestState.FINISHED), + new QuestRequirement(QuestHelperQuest.TROLL_ROMANCE, QuestState.FINISHED), + new SkillRequirement(Skill.MINING, 64), + new SkillRequirement(Skill.RANGED, 62), + new SkillRequirement(Skill.THIEVING, 62), + new SkillRequirement(Skill.AGILITY, 61), + new SkillRequirement(Skill.STRENGTH, 58), + new SkillRequirement(Skill.SLAYER, 37) + ); + } + + @Override + public List getCombatRequirements() + { + return List.of( + "Golem guard (lvl 141)", + "Arrav (lvl 339)" + ); + } + + @Override + public QuestPointReward getQuestPointReward() + { + return new QuestPointReward(2); + } + + @Override + public List getExperienceRewards() + { + return List.of( + new ExperienceReward(Skill.MINING, 40_000), + new ExperienceReward(Skill.THIEVING, 40_000), + new ExperienceReward(Skill.AGILITY, 40_000) + ); + } + + @Override + public List getUnlockRewards() + { + return List.of( + new UnlockReward("Access to Zemouregal's Fort") + ); + } + + @Override + public List getPanels() + { + var panels = new ArrayList(); + + panels.add(new PanelDetails("Tomb Raiding", List.of( + startQuest, + enterTomb, + getFirstKey, + getSecondKey, + pullSouthLever, + pullNorthLever, + enterGolemArena, + fightGolemGuard, + enterTombBasement, + solveTilePuzzle, + searchShelvesForUrn, + inspectMurals, + combineJarWithDwellberries, + combineJarWithRingOfLife, + returnToElias + ), List.of( + dwellberries3, + ringOfLife, + golemCombatGear + // Requirements + ), List.of( + // Recommended + twoFreeInventorySlots, + fairyRingDLQ, + staminaPotion, + prayerPotion, + food + ))); + panels.add(new PanelDetails("Fort Invasion", List.of( + headToTrollheim, + continueThroughTrollheimCave, + enterTrollweissCave, + rubbleMiner1, + climbUpstairsAndTalkToArrav, + talkToArrav, + goToNextRoom, + searchTapestry, + returnToEliasWithBaseItems + ), List.of( + // Requirements + anyPickaxe + ), List.of( + // Recommended + fairyRingDLQ, + trollheimTeleport, + staminaPotion + ))); + panels.add(new PanelDetails("Hearty Heist", List.of( + // Steps + headToZemouregalsBaseAndTalkToElias, + enterZemouregalsBase, + getToBackOfZemouregalsBase, + enterStorageRoom, + searchTableForDecoderStrips, + openChestForCodeKey, + metalDoorSolver, + openMetalDoors, + grappleAcross, + enterBossRoom, + fightArrav, + finishQuest + ), List.of( + // Requirements + basePlans, + baseKey, + anyGrappleableCrossbow, + mithrilGrapple, + arravCombatGear, + insulatedBoots, + canopicJarFullForHeist + ), List.of( + // Recommended + lumberyardTeleport, + staminaPotion, + prayerPotion, + antiVenom, + food + ))); + + return panels; + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TilePuzzleSolver.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TilePuzzleSolver.java new file mode 100644 index 0000000000..58705801d1 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TilePuzzleSolver.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav; + +import com.questhelper.steps.DetailedOwnerStep; +import com.questhelper.steps.DetailedQuestStep; +import com.questhelper.steps.ObjectStep; +import com.questhelper.steps.QuestStep; +import com.questhelper.steps.tools.QuestPerspective; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.NullObjectID; +import net.runelite.api.ObjectID; +import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.eventbus.Subscribe; +import org.apache.commons.lang3.tuple.Pair; + +@Slf4j +public class TilePuzzleSolver extends DetailedOwnerStep +{ + /** + * Width & height of the tile puzzle + */ + private static final int SIZE = 12; + private static final int GREEN_TILE = NullObjectID.NULL_50296; + private static final int BLUE_TILE = NullObjectID.NULL_50294; + private static final int RED_TILE = NullObjectID.NULL_50295; + private static final int YELLOW_TILE = NullObjectID.NULL_50297; + private static final Set VALID_TILES = Set.of(GREEN_TILE, BLUE_TILE, RED_TILE, YELLOW_TILE); + + /** + * a 2-dimensional array of the tiles. [x][y] + * The value is the object ID (i.e. the color of the tile) + */ + private final int[][] tiles = new int[SIZE][SIZE]; + + @Inject + Client client; + + /** + * State of the tiles array. + * False if tiles have not had their object IDs filled in. + * True if they have had their object IDs filled in. + */ + private boolean tilesConfigured = false; + + List shortestPath = null; + private DetailedQuestStep fallbackStep; + private QuestStep finishPuzzleStep; + private DetailedQuestStep pathStep; + + public TilePuzzleSolver(TheCurseOfArrav theCurseOfArrav) + { + super(theCurseOfArrav, "Solve the floor tile puzzle. Follow the instructions in the overlay."); + } + + /** + * @param startY local Y coordinate of the tiles array for where to start searching + * @return A list of local X/Y coordinates if a path to the end was found + */ + private @Nullable List findPath(int startY) + { + int startX = 11; + + assert (startY >= 0 && startY < SIZE); + assert (this.tilesConfigured); + + int startObjectId = this.tiles[startX][startY]; + var visited = new boolean[SIZE][SIZE]; + var queue = new LinkedList(); + queue.add(new int[]{startX, startY}); + visited[startX][startY] = true; + + var parent = new int[SIZE][SIZE][2]; + parent[startX][startY] = new int[]{-1, -1}; + + int[][] directions = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; + + while (!queue.isEmpty()) + { + var current = queue.poll(); + var x = current[0]; + var y = current[1]; + + if (x == 0) + { + var path = new ArrayList(); + var retracedStep = new int[]{x, y}; + while (retracedStep[0] != -1 && retracedStep[1] != -1) + { + path.add(0, retracedStep); + retracedStep = parent[retracedStep[0]][retracedStep[1]]; + } + return path; + } + + for (var direction : directions) + { + int newX = x + direction[0]; + int newY = y + direction[1]; + + if (newX >= 0 && newX < SIZE && newY >= 0 && newY < SIZE && !visited[newX][newY] && areObjectIdsCompatible(this.tiles[newX][newY], startObjectId)) + { + queue.add(new int[]{newX, newY}); + visited[newX][newY] = true; + parent[newX][newY] = new int[]{x, y}; + } + } + } + + return null; + } + + private boolean areObjectIdsCompatible(int a, int b) + { + assert (VALID_TILES.contains(a)); + assert (VALID_TILES.contains(b)); + + if (a == GREEN_TILE || a == BLUE_TILE) + { + return b == GREEN_TILE || b == BLUE_TILE; + } + + if (a == RED_TILE || a == YELLOW_TILE) + { + return b == RED_TILE || b == YELLOW_TILE; + } + + return false; + } + + private Pair findPuzzleStart(Tile[][] wvTiles) + { + // stupidly search for first tile with a green, red, blue, or yellow tile + for (int x = 0; x < wvTiles.length; x++) + { + for (int y = 0; y < wvTiles[x].length; y++) + { + var tile = wvTiles[x][y]; + var groundObject = tile.getGroundObject(); + if (groundObject != null) + { + if (VALID_TILES.contains(groundObject.getId())) + { + return Pair.of(x, y); + } + } + } + } + + return null; + } + + /** + * Look through the world view and attempt to fill up the tiles array + */ + private void tryFillTiles() + { + var localPlayer = client.getLocalPlayer(); + if (localPlayer == null) + { + return; + } + + var worldView = localPlayer.getWorldView(); + var squareOfTiles = worldView.getScene().getTiles()[worldView.getPlane()]; + + var puzzleStart = this.findPuzzleStart(squareOfTiles); + if (puzzleStart == null) + { + return; + } + + var firstPuzzleX = puzzleStart.getLeft(); + var firstPuzzleY = puzzleStart.getRight(); + + assert (firstPuzzleX != null); + assert (firstPuzzleY != null); + + log.debug("Found first puzzle tile at {}/{}", firstPuzzleX, firstPuzzleY); + + for (int x = 0; x < 12; x++) + { + var offsetX = x + firstPuzzleX; + if (offsetX >= squareOfTiles.length) + { + log.debug("X({} + {}) out of bounds when mapping puzzle tiles", x, firstPuzzleX); + return; + } + + for (int y = 0; y < 12; y++) + { + var offsetY = y + firstPuzzleY; + if (offsetY >= squareOfTiles[offsetX].length) + { + log.debug("Y({} + {}) out of bounds when mapping puzzle tiles", y, firstPuzzleY); + return; + } + + var tile = squareOfTiles[offsetX][offsetY]; + var groundObject = tile.getGroundObject(); + if (groundObject == null) + { + log.debug("X({} + {}) Y({} + {}) had no ground object", x, firstPuzzleX, y, firstPuzzleY); + return; + } + + if (!VALID_TILES.contains(groundObject.getId())) + { + log.debug("X({} + {}) Y({} + {}) had an invalid ground object ({})", x, firstPuzzleX, y, firstPuzzleY, groundObject.getId()); + return; + } + + this.tiles[x][y] = groundObject.getId(); + } + } + + this.tilesConfigured = true; + } + + @Subscribe + public void onGameTick(GameTick event) + { + if (!this.tilesConfigured) + { + this.tryFillTiles(); + } + + if (this.tilesConfigured && this.shortestPath == null) + { + // Figure out the shortest path + var possiblePaths = new ArrayList>(); + for (int y = 0; y < 12; y++) + { + var path = this.findPath(y); + if (path != null) + { + log.debug("Found possible path starting at {}/{}. Length {}", 11, y, path.size()); + possiblePaths.add(path); + } + } + + var baseX = 3737; + var baseY = 4709; + + for (var possiblePath : possiblePaths) + { + if (this.shortestPath == null || possiblePath.size() < this.shortestPath.size()) + { + List line = new ArrayList<>(); + for (int[] tileXY : possiblePath) + { + var wp = new WorldPoint(baseX + tileXY[0], baseY + tileXY[1], 0); + line.add(wp); + } + this.shortestPath = line; + this.pathStep.setLinePoints(this.shortestPath); + } + } + } + + updateSteps(); + } + + @Override + public void startUp() + { + updateSteps(); + } + + @Override + protected void setupSteps() + { + pathStep = new DetailedQuestStep(getQuestHelper(), new WorldPoint(3734, 4714, 0), "Click the tiles to pass through the puzzle."); + + fallbackStep = new DetailedQuestStep(getQuestHelper(), new WorldPoint(3734, 4714, 0), "Unable to figure out a path, click your way across lol"); // TODO + + finishPuzzleStep = new ObjectStep(getQuestHelper(), ObjectID.LEVER_50205, new WorldPoint(3735, 4719, 0), "Finish the puzzle by clicking the lever."); + } + + protected void updateSteps() + { + if (this.shortestPath == null) { + startUpStep(fallbackStep); + return; + } + + var localPlayer = client.getLocalPlayer(); + if (localPlayer == null) { + startUpStep(fallbackStep); + return; + } + + var playerWp = localPlayer.getWorldLocation(); + var localPoint = QuestPerspective.getRealWorldPointFromLocal(client, localPlayer.getWorldLocation()); + if (localPoint == null) { + startUpStep(fallbackStep); + return; + } + + + var baseX = 3737; + var baseY = 4709; + + var xInPuzzle = localPoint.getX() - baseX; + var yInPuzzle = localPoint.getY() - baseY; + + if (xInPuzzle > 0 && xInPuzzle < SIZE && yInPuzzle >= 0 && yInPuzzle < SIZE) { + log.debug("Player is in the puzzle, at {}/{}", xInPuzzle, yInPuzzle); + startUpStep(pathStep); + } else { + log.debug("player is outside of puzzle: {} / {} / {}/{}", playerWp, localPoint, xInPuzzle, yInPuzzle); + var userIsPastPuzzle = localPoint.getX() <= 3730 || (localPoint.getX() <= baseX && localPoint.getY() >= 4701); + if (userIsPastPuzzle) { + // highlight lever + startUpStep(finishPuzzleStep); + } else { + // highlight puzzle start + startUpStep(pathStep); + } + } + } + + @Override + public List getSteps() + { + var steps = new ArrayList(); + steps.add(fallbackStep); + steps.add(pathStep); + steps.add(finishPuzzleStep); + + return steps; + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolver.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolver.java new file mode 100644 index 0000000000..0174e2399d --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolver.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav.rubblesolvers; + +import com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav; +import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.conditional.Conditions; +import com.questhelper.requirements.conditional.ObjectCondition; +import com.questhelper.requirements.util.LogicType; +import com.questhelper.steps.*; + +import java.util.*; +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.Direction; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.eventbus.Subscribe; + +@Slf4j +public abstract class RubbleSolver extends DetailedOwnerStep { + @Inject + protected Client client; + + private List mineSteps; + private List conditions; + private List inverseConditions; + private ConditionalStep conditionalStep; + + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private int stepCounter; + + public RubbleSolver(TheCurseOfArrav theCurseOfArrav, @SuppressWarnings("unused") String number) { + super(theCurseOfArrav, "Make your way through the Trollweiss cave, mining rubble with your pickaxe from the direction indicated. Rubble can only be mined from the same direction once."); + } + + protected void addMineRubbleStep(int x, int y, RubbleType rubbleType, Direction direction) { + var validObjectIDs = rubbleType.getObjectIDs(); + assert !validObjectIDs.isEmpty(); + + var validIDSet = new HashSet<>(validObjectIDs); + + var wp = new WorldPoint(x, y, 0); + // Useful for debugging + // var stepCounter = this.stepCounter++; + var text = String.format("Mine the rubble from the %s side", direction.toString().toLowerCase()); + var mainObjectID = validObjectIDs.get(0); + var step = new ObjectStep(getQuestHelper(), mainObjectID, wp, text, ((TheCurseOfArrav) getQuestHelper()).anyPickaxe); + var offsetX = x; + var offsetY = y; + switch (direction) { + case NORTH: + offsetY += 1; + break; + case SOUTH: + offsetY -= 1; + break; + case WEST: + offsetX -= 1; + break; + case EAST: + offsetX += 1; + break; + } + var posWp = new WorldPoint(offsetX, offsetY, 0); + step.addTileMarker(posWp, SpriteID.SKILL_MINING); + for (var alternateIDs : validObjectIDs) { + // todo this adds the first object again xd + step.addAlternateObjects(alternateIDs); + } + + var conditionText = String.format("Rubble mined from the %s side", direction.toString().toLowerCase()); + var inverseConditionText = String.format("Rubble needs to be mined from the %s side", direction.toString().toLowerCase()); + var conditionThatRubbleIsStillThere = new ObjectCondition(validIDSet, wp); + var conditionThatRubbleHasBeenMined = new Conditions(true, LogicType.NAND, conditionThatRubbleIsStillThere); + conditionThatRubbleIsStillThere.setText(inverseConditionText); + conditionThatRubbleHasBeenMined.setText(conditionText); + + this.mineSteps.add(step); + this.conditions.add(conditionThatRubbleHasBeenMined); + this.inverseConditions.add(conditionThatRubbleIsStillThere); + } + + @Subscribe + public void onGameTick(GameTick event) { + updateSteps(); + } + + @Override + public void startUp() { + updateSteps(); + } + + protected abstract void setupRubbleSteps(); + + @Override + protected void setupSteps() { + this.stepCounter = 1; + this.mineSteps = new ArrayList<>(); + this.conditions = new ArrayList<>(); + this.inverseConditions = new ArrayList<>(); + + var todo = new DetailedQuestStep(getQuestHelper(), "todo"); + + this.setupRubbleSteps(); + + conditionalStep = new ConditionalStep(getQuestHelper(), todo); + + assert this.mineSteps.size() == this.conditions.size(); + assert this.mineSteps.size() == this.inverseConditions.size(); + + for (var i = 0; i < mineSteps.size(); i++) { + var mineStep = mineSteps.get(i); + var inverseCondition = this.inverseConditions.get(i); + + // Useful for debugging + // mineStep.addRequirement(inverseCondition); + + conditionalStep.addStep(inverseCondition, mineStep); + } + } + + protected void updateSteps() { + startUpStep(this.conditionalStep); + } + + @Override + public List getSteps() { + var steps = new ArrayList(); + + steps.add(this.conditionalStep); + steps.addAll(this.mineSteps); + + return steps; + } + +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverFour.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverFour.java new file mode 100644 index 0000000000..bb51d450e1 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverFour.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav.rubblesolvers; + +import com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.Direction; + +/** + * This class describes the rubble mining steps required for Roadblock 4 (when quest state varbit is 28) + */ +@Slf4j +public class RubbleSolverFour extends RubbleSolver +{ + public RubbleSolverFour(TheCurseOfArrav theCurseOfArrav) { + super(theCurseOfArrav, "4"); + } + + @Override + protected void setupRubbleSteps() { + this.addMineRubbleStep(2787, 10267, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2787, 10266, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2787, 10267, RubbleType.Two, Direction.SOUTH); + this.addMineRubbleStep(2788, 10267, RubbleType.Two, Direction.NORTH); + this.addMineRubbleStep(2787, 10267, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2788, 10267, RubbleType.One, Direction.WEST); + + // Last part from south + this.addMineRubbleStep(2803, 10264, RubbleType.Three, Direction.SOUTH); + this.addMineRubbleStep(2803, 10265, RubbleType.Two, Direction.SOUTH); + + // Last part from north + this.addMineRubbleStep(2803, 10267, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2803, 10266, RubbleType.Three, Direction.NORTH); + this.addMineRubbleStep(2804, 10266, RubbleType.Two, Direction.NORTH); + + // Last part from west + this.addMineRubbleStep(2802, 10266, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2801, 10265, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2802, 10265, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2803, 10265, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2802, 10266, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2804, 10265, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2803, 10266, RubbleType.Two, Direction.SOUTH); + this.addMineRubbleStep(2803, 10266, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2804, 10266, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2804, 10265, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2805, 10265, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2806, 10265, RubbleType.One, Direction.WEST); + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverOne.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverOne.java new file mode 100644 index 0000000000..74dfd7e873 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverOne.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav.rubblesolvers; + +import com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.Direction; + +/** + * This class describes the rubble mining steps required for Roadblock 1 (when quest state varbit is 22) + */ +@Slf4j +public class RubbleSolverOne extends RubbleSolver +{ + public RubbleSolverOne(TheCurseOfArrav theCurseOfArrav) { + super(theCurseOfArrav, "1"); + } + + @Override + protected void setupRubbleSteps() { + this.addMineRubbleStep(2764, 10266, RubbleType.Two, Direction.SOUTH); // 1 + this.addMineRubbleStep(2775, 10258, RubbleType.One, Direction.SOUTH); // 2 + this.addMineRubbleStep(2764, 10266, RubbleType.One, Direction.EAST); // 3 + this.addMineRubbleStep(2764, 10267, RubbleType.One, Direction.SOUTH); // 4 + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverThree.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverThree.java new file mode 100644 index 0000000000..719c4cfd46 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverThree.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav.rubblesolvers; + +import com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.Direction; + +/** + * This class describes the rubble mining steps required for Roadblock 3 (when quest state varbit is 26) + */ +@Slf4j +public class RubbleSolverThree extends RubbleSolver +{ + public RubbleSolverThree(TheCurseOfArrav theCurseOfArrav) { + super(theCurseOfArrav, "3"); + } + + @Override + protected void setupRubbleSteps() { + // These 3 steps are technically part of the next solver, but it's better to get these done asap + this.addMineRubbleStep(2787, 10267, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2787, 10266, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2787, 10267, RubbleType.Two, Direction.SOUTH); + + this.addMineRubbleStep(2789, 10286, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2789, 10285, RubbleType.Three, Direction.NORTH); + this.addMineRubbleStep(2789, 10285, RubbleType.Two, Direction.WEST); + + this.addMineRubbleStep(2789, 10283, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2789, 10284, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2789, 10285, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2790, 10285, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2791, 10285, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2789, 10283, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2790, 10283, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2791, 10283, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2790, 10282, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2791, 10282, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2791, 10283, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2791, 10285, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2792, 10285, RubbleType.Two, Direction.SOUTH); + this.addMineRubbleStep(2792, 10285, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2793, 10285, RubbleType.One, Direction.WEST); + + this.addMineRubbleStep(2787, 10267, RubbleType.One, Direction.NORTH); + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverTwo.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverTwo.java new file mode 100644 index 0000000000..0aa0ae2f44 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleSolverTwo.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav.rubblesolvers; + +import com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.Direction; + +/** + * This class describes the rubble mining steps required for Roadblock 2 (when quest state varbit is 24) + */ +@Slf4j +public class RubbleSolverTwo extends RubbleSolver +{ + public RubbleSolverTwo(TheCurseOfArrav theCurseOfArrav) { + super(theCurseOfArrav, "2"); + } + + @Override + protected void setupRubbleSteps() { + this.addMineRubbleStep(2766, 10279, RubbleType.Three, Direction.WEST); + this.addMineRubbleStep(2766, 10280, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2767, 10281, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2766, 10279, RubbleType.Two, Direction.NORTH); + this.addMineRubbleStep(2766, 10278, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2766, 10278, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2766, 10279, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2767, 10278, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2767, 10279, RubbleType.Two, Direction.SOUTH); + this.addMineRubbleStep(2767, 10279, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2768, 10279, RubbleType.One, Direction.WEST); + this.addMineRubbleStep(2768, 10280, RubbleType.Three, Direction.SOUTH); + this.addMineRubbleStep(2768, 10281, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2769, 10281, RubbleType.Two, Direction.WEST); + this.addMineRubbleStep(2767, 10281, RubbleType.One, Direction.EAST); + this.addMineRubbleStep(2767, 10282, RubbleType.One, Direction.SOUTH); + this.addMineRubbleStep(2769, 10281, RubbleType.One, Direction.NORTH); + this.addMineRubbleStep(2770, 10281, RubbleType.One, Direction.WEST); + } +} diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleType.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleType.java new file mode 100644 index 0000000000..75b276cad5 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/rubblesolvers/RubbleType.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav.rubblesolvers; + +import lombok.Getter; +import net.runelite.api.ObjectID; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Getter +public enum RubbleType +{ + Three(ObjectID.RUBBLE_50603, ObjectID.RUBBLE_50604), + Two(ObjectID.RUBBLE_50598, ObjectID.RUBBLE_50602, ObjectID.RUBBLE_50601, ObjectID.RUBBLE_50599), + One(ObjectID.RUBBLE_50587, ObjectID.RUBBLE_50589, ObjectID.RUBBLE_50590, ObjectID.RUBBLE_50594, ObjectID.RUBBLE_50597, ObjectID.RUBBLE_50588, ObjectID.RUBBLE_50593); + + private final List objectIDs; + + RubbleType(Integer... possibleObjectIDs) { + this.objectIDs = new ArrayList<>(); + Collections.addAll(this.objectIDs, possibleObjectIDs); + } +} diff --git a/src/main/java/com/questhelper/panel/questorders/OptimalQuestGuide.java b/src/main/java/com/questhelper/panel/questorders/OptimalQuestGuide.java index d7dc2baa68..1d741e90fc 100644 --- a/src/main/java/com/questhelper/panel/questorders/OptimalQuestGuide.java +++ b/src/main/java/com/questhelper/panel/questorders/OptimalQuestGuide.java @@ -250,6 +250,7 @@ public class OptimalQuestGuide //QuestHelperQuest.INTO_THE_TOMBS, - Placeholder for future addition. QuestHelperQuest.A_NIGHT_AT_THE_THEATRE, QuestHelperQuest.DRAGON_SLAYER_II, + QuestHelperQuest.THE_CURSE_OF_ARRAV, QuestHelperQuest.MAKING_FRIENDS_WITH_MY_ARM, QuestHelperQuest.SECRETS_OF_THE_NORTH, QuestHelperQuest.WHILE_GUTHIX_SLEEPS, diff --git a/src/main/java/com/questhelper/panel/questorders/QuestOrders.java b/src/main/java/com/questhelper/panel/questorders/QuestOrders.java index 8c111ca844..9106358f80 100644 --- a/src/main/java/com/questhelper/panel/questorders/QuestOrders.java +++ b/src/main/java/com/questhelper/panel/questorders/QuestOrders.java @@ -249,6 +249,7 @@ public class QuestOrders QuestHelperQuest.SECRETS_OF_THE_NORTH, QuestHelperQuest.SONG_OF_THE_ELVES, QuestHelperQuest.DESERT_TREASURE_II, + QuestHelperQuest.THE_CURSE_OF_ARRAV, // Not from wiki QuestHelperQuest.WHILE_GUTHIX_SLEEPS, // Remaining section is unordered as not part of list on https://oldschool.runescape.wiki/w/Optimal_quest_guide/Ironman diff --git a/src/main/java/com/questhelper/panel/questorders/ReleaseDate.java b/src/main/java/com/questhelper/panel/questorders/ReleaseDate.java index 13b1afae50..0df0ea19df 100644 --- a/src/main/java/com/questhelper/panel/questorders/ReleaseDate.java +++ b/src/main/java/com/questhelper/panel/questorders/ReleaseDate.java @@ -217,6 +217,7 @@ public class ReleaseDate QuestHelperQuest.DEATH_ON_THE_ISLE, QuestHelperQuest.MEAT_AND_GREET, QuestHelperQuest.ETHICALLY_ACQUIRED_ANTIQUITIES, + QuestHelperQuest.THE_CURSE_OF_ARRAV, // Miniquests QuestHelperQuest.ALFRED_GRIMHANDS_BARCRAWL, QuestHelperQuest.THE_MAGE_ARENA, diff --git a/src/main/java/com/questhelper/questhelpers/BasicQuestHelper.java b/src/main/java/com/questhelper/questhelpers/BasicQuestHelper.java index 879ff9c4ba..b5e8c703a9 100644 --- a/src/main/java/com/questhelper/questhelpers/BasicQuestHelper.java +++ b/src/main/java/com/questhelper/questhelpers/BasicQuestHelper.java @@ -25,9 +25,6 @@ package com.questhelper.questhelpers; import com.questhelper.QuestHelperConfig; -import com.questhelper.requirements.Requirement; -import com.questhelper.requirements.conditional.Conditions; -import com.questhelper.requirements.util.LogicType; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -39,8 +36,14 @@ public abstract class BasicQuestHelper extends QuestHelper protected Map steps; protected int var; - @Override - public void init() + public Map getStepList() { + return this.steps; + } + + /** + * Attempt to load steps from the quest if steps have not yet been loaded + */ + private void tryLoadSteps() { if (steps == null) { @@ -48,9 +51,18 @@ public void init() } } + @Override + public void init() + { + this.tryLoadSteps(); + } + @Override public void startUp(QuestHelperConfig config) { + // this.tryLoadSteps(); + // TODO: This should use `tryLoadSteps` but when it is being more careful to be initialized, it doesn't handle + // highlighting in sidebars properly steps = loadSteps(); this.config = config; instantiateSteps(steps.values()); diff --git a/src/main/java/com/questhelper/questinfo/ExternalQuestResources.java b/src/main/java/com/questhelper/questinfo/ExternalQuestResources.java index 2487e22566..a8251c179d 100644 --- a/src/main/java/com/questhelper/questinfo/ExternalQuestResources.java +++ b/src/main/java/com/questhelper/questinfo/ExternalQuestResources.java @@ -203,6 +203,7 @@ public enum ExternalQuestResources IN_SEARCH_OF_KNOWLEDGE("https://oldschool.runescape.wiki/w/In_Search_of_Knowledge"), DADDYS_HOME("https://oldschool.runescape.wiki/w/Daddy%27s_Home"), HOPESPEARS_WILL("https://oldschool.runescape.wiki/w/Hopespear%27s_Will"), + THE_CURSE_OF_ARRAV("https://oldschool.runescape.wiki/w/The_Curse_of_Arrav"), // Fake miniquests KNIGHT_WAVES_TRAINING_GROUNDS("https://oldschool.runescape.wiki/w/Camelot_training_room"), diff --git a/src/main/java/com/questhelper/questinfo/QuestHelperQuest.java b/src/main/java/com/questhelper/questinfo/QuestHelperQuest.java index ac68fba933..6b681feb94 100644 --- a/src/main/java/com/questhelper/questinfo/QuestHelperQuest.java +++ b/src/main/java/com/questhelper/questinfo/QuestHelperQuest.java @@ -234,6 +234,7 @@ import com.questhelper.helpers.quests.templeoftheeye.TempleOfTheEye; import com.questhelper.helpers.quests.theascentofarceuus.TheAscentOfArceuus; import com.questhelper.helpers.quests.thecorsaircurse.TheCorsairCurse; +import com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav; import com.questhelper.helpers.quests.thedepthsofdespair.TheDepthsOfDespair; import com.questhelper.helpers.quests.thedigsite.TheDigSite; import com.questhelper.helpers.quests.theeyesofglouphrie.TheEyesOfGlouphrie; @@ -485,6 +486,7 @@ public enum QuestHelperQuest DEATH_ON_THE_ISLE(new DeathOnTheIsle(), Quest.DEATH_ON_THE_ISLE, QuestVarbits.QUEST_DEATH_ON_THE_ISLE, QuestDetails.Type.P2P, QuestDetails.Difficulty.INTERMEDIATE), MEAT_AND_GREET(new MeatAndGreet(), Quest.MEAT_AND_GREET, QuestVarbits.QUEST_MEAT_AND_GREET, QuestDetails.Type.P2P, QuestDetails.Difficulty.EXPERIENCED), THE_HEART_OF_DARKNESS(new TheHeartOfDarkness(), Quest.THE_HEART_OF_DARKNESS, QuestVarbits.QUEST_THE_HEART_OF_DARKNESS, QuestDetails.Type.P2P, QuestDetails.Difficulty.EXPERIENCED), + THE_CURSE_OF_ARRAV(new TheCurseOfArrav(), Quest.THE_CURSE_OF_ARRAV, QuestVarbits.QUEST_THE_CURSE_OF_ARRAV, QuestDetails.Type.P2P, QuestDetails.Difficulty.MASTER), //Miniquests ENTER_THE_ABYSS(new EnterTheAbyss(), Quest.ENTER_THE_ABYSS, QuestVarPlayer.QUEST_ENTER_THE_ABYSS, QuestDetails.Type.MINIQUEST, QuestDetails.Difficulty.MINIQUEST), diff --git a/src/main/java/com/questhelper/questinfo/QuestVarbits.java b/src/main/java/com/questhelper/questinfo/QuestVarbits.java index fdd2338259..ec653f8ea8 100644 --- a/src/main/java/com/questhelper/questinfo/QuestVarbits.java +++ b/src/main/java/com/questhelper/questinfo/QuestVarbits.java @@ -119,6 +119,7 @@ public enum QuestVarbits QUEST_DEATH_ON_THE_ISLE(11210), QUEST_MEAT_AND_GREET(11182), QUEST_THE_HEART_OF_DARKNESS(11117), + QUEST_THE_CURSE_OF_ARRAV(11479), /** * mini-quest varbits, these don't hold the completion value. */ diff --git a/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java b/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java index dc4d0316dd..5d5359c031 100644 --- a/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java +++ b/src/main/java/com/questhelper/requirements/conditional/ConditionForStep.java @@ -63,10 +63,13 @@ public void updateHandler() .forEach(req -> ((InitializableRequirement) req).updateHandler()); } + @Setter + private String text = ""; + @Nonnull @Override - public String getDisplayText() // conditions don't need display text (yet?) + public String getDisplayText() { - return ""; + return this.text; } } diff --git a/src/main/java/com/questhelper/requirements/conditional/ObjectCondition.java b/src/main/java/com/questhelper/requirements/conditional/ObjectCondition.java index 072dbdb720..ee780486be 100644 --- a/src/main/java/com/questhelper/requirements/conditional/ObjectCondition.java +++ b/src/main/java/com/questhelper/requirements/conditional/ObjectCondition.java @@ -32,10 +32,12 @@ import net.runelite.api.Tile; import net.runelite.api.TileObject; import net.runelite.api.coords.WorldPoint; +import java.util.Objects; +import java.util.Set; public class ObjectCondition extends ConditionForStep { - private final int objectID; + private final Set objectIDs; private final Zone zone; @Setter @@ -46,7 +48,7 @@ public class ObjectCondition extends ConditionForStep public ObjectCondition(int objectID) { - this.objectID = objectID; + this.objectIDs = Set.of(objectID); this.zone = null; } @@ -54,7 +56,7 @@ public ObjectCondition(int objectID, WorldPoint worldPoint) { assert(worldPoint != null); - this.objectID = objectID; + this.objectIDs = Set.of(objectID); this.zone = new Zone(worldPoint); } @@ -62,10 +64,21 @@ public ObjectCondition(int objectID, Zone zone) { assert(zone != null); - this.objectID = objectID; + this.objectIDs = Set.of(objectID); this.zone = zone; } + public ObjectCondition(Set objectIDs, WorldPoint worldPoint) + { + assert(worldPoint != null); + assert(objectIDs != null); + assert(objectIDs.stream().noneMatch(Objects::isNull)); + + this.objectIDs = objectIDs; + this.zone = new Zone(worldPoint); + } + + @Override public boolean check(Client client) { Tile[][] tiles; @@ -118,7 +131,16 @@ private boolean checkTile(Tile tile, Client client) private boolean checkForObjects(TileObject object) { - return object != null && (object.getId() == objectID || objectID == -1); + if (object == null) { + return false; + } + + // SPECIAL CASE FROM BEFORE: do we really need this? + if (this.objectIDs.contains(-1)) { + return true; + } + + return this.objectIDs.contains(object.getId()); } @Override diff --git a/src/main/java/com/questhelper/requirements/widget/WidgetModelRequirement.java b/src/main/java/com/questhelper/requirements/widget/WidgetModelRequirement.java index a7c9fbf77b..ffc8b10981 100644 --- a/src/main/java/com/questhelper/requirements/widget/WidgetModelRequirement.java +++ b/src/main/java/com/questhelper/requirements/widget/WidgetModelRequirement.java @@ -26,12 +26,14 @@ */ package com.questhelper.requirements.widget; +import lombok.Setter; import net.runelite.api.Client; import net.runelite.api.widgets.Widget; public class WidgetModelRequirement extends WidgetPresenceRequirement { - private final int id; + @Setter + private int id; public WidgetModelRequirement(int groupId, int childId, int childChildId, int id) { diff --git a/src/main/java/com/questhelper/requirements/widget/WidgetTextRequirement.java b/src/main/java/com/questhelper/requirements/widget/WidgetTextRequirement.java index 2f1cd9b7c8..677914b4b2 100644 --- a/src/main/java/com/questhelper/requirements/widget/WidgetTextRequirement.java +++ b/src/main/java/com/questhelper/requirements/widget/WidgetTextRequirement.java @@ -47,7 +47,9 @@ public class WidgetTextRequirement extends SimpleRequirement private final int groupId; private final int childId; - private final List text; + + private List text; + private int childChildId = -1; private boolean checkChildren; @@ -58,6 +60,30 @@ public class WidgetTextRequirement extends SimpleRequirement @Setter private String displayText = ""; + /** + * Use this if you need to update a widget requirement dynamically. + *

+ * Updating the text will reset the `hasPassed` variable. + * + * @param text list of valid strings for this widget check + */ + public void setText(@Nonnull List text) { + this.setHasPassed(false); + + this.text = text; + } + + /** + * Use this if you need to update a widget requirement dynamically. + *

+ * Updating the text will reset the `hasPassed` variable. + * + * @param text valid string for this widget check + */ + public void setText(@Nonnull String text) { + this.setText(List.of(text)); + } + public WidgetTextRequirement(@Component int componentId, String... text) { var pair = Utils.unpackWidget(componentId); diff --git a/src/main/java/com/questhelper/steps/WidgetStep.java b/src/main/java/com/questhelper/steps/WidgetStep.java index 83398da076..203bfbc957 100644 --- a/src/main/java/com/questhelper/steps/WidgetStep.java +++ b/src/main/java/com/questhelper/steps/WidgetStep.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; import lombok.Setter; import net.runelite.api.widgets.Widget; import com.questhelper.QuestHelperPlugin; @@ -40,6 +41,8 @@ public class WidgetStep extends DetailedQuestStep @Setter protected List widgetDetails = new ArrayList<>(); + protected List> extraWidgetOverlayHintFunctions = new ArrayList<>(); + public WidgetStep(QuestHelper questHelper, String text, int groupID, int childID) { super(questHelper, text); @@ -52,6 +55,10 @@ public WidgetStep(QuestHelper questHelper, String text, WidgetDetails... widgetD this.widgetDetails.addAll(Arrays.asList(widgetDetails)); } + public void addExtraWidgetOverlayHintFunction(BiConsumer function) { + this.extraWidgetOverlayHintFunctions.add(function); + } + @Override public void makeWidgetOverlayHint(Graphics2D graphics, QuestHelperPlugin plugin) { @@ -79,5 +86,9 @@ public void makeWidgetOverlayHint(Graphics2D graphics, QuestHelperPlugin plugin) graphics.setColor(questHelper.getConfig().targetOverlayColor()); graphics.draw(widget.getBounds()); } + + for (var extraWidgetOverlayHintFunction : extraWidgetOverlayHintFunctions) { + extraWidgetOverlayHintFunction.accept(graphics, plugin); + } } } diff --git a/src/main/java/com/questhelper/steps/widget/WidgetDetails.java b/src/main/java/com/questhelper/steps/widget/WidgetDetails.java index 9070198924..42a4bdfdb4 100644 --- a/src/main/java/com/questhelper/steps/widget/WidgetDetails.java +++ b/src/main/java/com/questhelper/steps/widget/WidgetDetails.java @@ -37,6 +37,13 @@ public class WidgetDetails public int childID; public int childChildID; + public WidgetDetails(int groupID, int childID) + { + this.groupID = groupID; + this.childID = childID; + this.childChildID = -1; + } + public WidgetDetails(@Component int componentId) { var pair = Utils.unpackWidget(componentId); diff --git a/src/test/java/com/questhelper/MockedTest.java b/src/test/java/com/questhelper/MockedTest.java index 048f8461de..501da5301c 100644 --- a/src/test/java/com/questhelper/MockedTest.java +++ b/src/test/java/com/questhelper/MockedTest.java @@ -32,17 +32,21 @@ import com.questhelper.statemanagement.AchievementDiaryStepManager; import com.questhelper.statemanagement.PlayerStateManager; import net.runelite.api.Client; +import net.runelite.api.SpriteID; import net.runelite.client.callback.ClientThread; import net.runelite.client.callback.Hooks; import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; import net.runelite.client.game.ItemManager; +import net.runelite.client.game.SpriteManager; import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.overlay.OverlayManager; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mockito; import javax.inject.Named; +import java.awt.image.BufferedImage; import java.util.concurrent.ScheduledExecutorService; import static org.mockito.Mockito.when; @@ -69,9 +73,15 @@ public abstract class MockedTest extends MockedTestBase @Bind protected QuestHelperConfig questHelperConfig = Mockito.mock(QuestHelperConfig.class); + @Bind + protected RuneLiteConfig runeLiteConfig = Mockito.mock(RuneLiteConfig.class); + @Bind protected QuestOverlayManager questOverlayManager = Mockito.mock(QuestOverlayManager.class); + @Bind + protected SpriteManager spriteManager = Mockito.mock(SpriteManager.class); + @Bind protected RuneliteObjectManager runeliteObjectManager = Mockito.mock(RuneliteObjectManager.class); @@ -100,6 +110,7 @@ public abstract class MockedTest extends MockedTestBase @Named("developerMode") private boolean developerMode; + @Override @BeforeEach protected void setUp() @@ -108,8 +119,9 @@ protected void setUp() when(questHelperPlugin.getPlayerStateManager()).thenReturn(playerStateManager); when(playerStateManager.getAccountType()).thenReturn(AccountType.NORMAL); - when(client.getIntStack()).thenReturn(new int[] { 1, 1, 1, 1 }); + when(client.getIntStack()).thenReturn(new int[] {1, 1, 1, 1}); when(questHelperConfig.solvePuzzles()).thenReturn(true); + when(spriteManager.getSprite(SpriteID.TAB_QUESTS, 0)).thenReturn(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)); AchievementDiaryStepManager.setup(configManager); } diff --git a/src/test/java/com/questhelper/helpers/quests/thecurseofarrav/KeysAndLeversTest.java b/src/test/java/com/questhelper/helpers/quests/thecurseofarrav/KeysAndLeversTest.java new file mode 100644 index 0000000000..86986ebe6b --- /dev/null +++ b/src/test/java/com/questhelper/helpers/quests/thecurseofarrav/KeysAndLeversTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav; + +import com.questhelper.MockedTest; +import com.questhelper.domain.AccountType; +import com.questhelper.questinfo.QuestHelperQuest; +import com.questhelper.steps.ConditionalStep; +import com.questhelper.steps.tools.QuestPerspective; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemContainer; +import net.runelite.api.ItemID; +import net.runelite.api.Player; +import net.runelite.api.Scene; +import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import static com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav.VARBIT_NORTH_LEVER_STATE; +import static com.questhelper.helpers.quests.thecurseofarrav.TheCurseOfArrav.VARBIT_SOUTH_LEVER_STATE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class KeysAndLeversTest extends MockedTest +{ + private MockedStatic questPerspectiveMockedStatic; + private MockedStatic worldPointMockedStatic; + private TheCurseOfArrav helper; + + @BeforeEach + protected void preTest() + { + when(playerStateManager.getAccountType()).thenReturn(AccountType.NORMAL); + + var mockedPlayer = Mockito.mock(Player.class); + // when(mockedPlayer.getLocalLocation()).thenReturn(new LocalPoint(1, 1, 1)); + when(client.getLocalPlayer()).thenReturn(mockedPlayer); + + questPerspectiveMockedStatic = Mockito.mockStatic(QuestPerspective.class); + + worldPointMockedStatic = Mockito.mockStatic(WorldPoint.class); + + questPerspectiveMockedStatic.when(() -> QuestPerspective.getInstanceLocalPointFromReal(any(), any())) + .thenReturn(null); + + helper = new TheCurseOfArrav(); + + } + + @AfterEach + protected void postTest() + { + questPerspectiveMockedStatic.close(); + worldPointMockedStatic.close(); + } + + private ConditionalStep init(WorldPoint playerLocation) + { + return this.init(playerLocation, null); + } + + private ConditionalStep init(WorldPoint playerLocation, Item[] mockedItems) + { + worldPointMockedStatic.when(() -> WorldPoint.fromLocalInstance(any(), any())) + .thenReturn(playerLocation); + + var mockedItemContainer = Mockito.mock(ItemContainer.class); + if (mockedItems != null) + { + when(mockedItemContainer.getItems()).thenReturn(mockedItems); + when(client.getItemContainer(InventoryID.INVENTORY)).thenReturn(mockedItemContainer); + } + + when(client.getPlane()).thenReturn(0); + + var mockedScene = Mockito.mock(Scene.class); + when(mockedScene.getTiles()).thenReturn(new Tile[][][]{ + {} + }); + when(client.getScene()).thenReturn(mockedScene); + + this.injector.injectMembers(helper); + helper.setInjector(injector); + helper.setQuest(QuestHelperQuest.THE_CURSE_OF_ARRAV); + helper.setQuestHelperPlugin(questHelperPlugin); + helper.setConfig(questHelperConfig); + helper.init(); + + helper.startUp(questHelperConfig); + var conditionalStep = helper.unlockImposingDoors; + conditionalStep.startUp(); + return conditionalStep; + } + + @Test + void ensureOutsideTomb() + { + var conditionalStep = this.init(new WorldPoint(3305, 3037, 0)); + + assertEquals(this.helper.enterTomb, conditionalStep.getActiveStep()); + } + + @Test + void getFirstKey() + { + var conditionalStep = this.init(new WorldPoint(3845, 4547, 0)); + + assertEquals(this.helper.getFirstKey, conditionalStep.getActiveStep()); + } + + @Test + void getSecondKey() + { + var mockedItems = new Item[]{new Item(ItemID.MASTABA_KEY, 1)}; + var conditionalStep = this.init(new WorldPoint(3845, 4547, 0), mockedItems); + + assertEquals(this.helper.getSecondKey, conditionalStep.getActiveStep()); + } + + @Test + void getToSouthLever() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + new Item(ItemID.MASTABA_KEY_30309, 1), + }; + var conditionalStep = this.init(new WorldPoint(3845, 4547, 0), mockedItems); + + assertEquals(this.helper.getToSouthLever, conditionalStep.getActiveStep()); + } + + @Test + void insertKeyIntoSouthLever() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + new Item(ItemID.MASTABA_KEY_30309, 1), + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(0); + var conditionalStep = this.init(new WorldPoint(3893, 4552, 0), mockedItems); + + assertEquals(this.helper.pullSouthLever, conditionalStep.getActiveStep()); + } + + @Test + void getToSouthLeverAfterInsertingKey1() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(1); + var conditionalStep = this.init(new WorldPoint(3845, 4547, 0), mockedItems); + + assertEquals(this.helper.getToSouthLever, conditionalStep.getActiveStep()); + } + + @Test + void pullSouthLeverAfterInsertingKey1() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(1); + var conditionalStep = this.init(new WorldPoint(3893, 4552, 0), mockedItems); + + assertEquals(this.helper.pullSouthLever, conditionalStep.getActiveStep()); + } + + @Test + void leaveSouthLeverAfterInsertingKey1() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(2); + var conditionalStep = this.init(new WorldPoint(3893, 4552, 0), mockedItems); + + assertEquals(this.helper.leaveSouthLever, conditionalStep.getActiveStep()); + } + + @Test + void goToNorthLeverAfterPullingSouthLeverKey1() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(2); + var conditionalStep = this.init(new WorldPoint(3845, 4547, 0), mockedItems); + + assertEquals(this.helper.getToNorthLever, conditionalStep.getActiveStep()); + } + + @Test + void insertKeyIntoNorthLeverAfterPullingSouthLeverKey1() + { + var mockedItems = new Item[]{ + new Item(ItemID.MASTABA_KEY, 1), + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(2); + var conditionalStep = this.init(new WorldPoint(3894, 4597, 0), mockedItems); + + assertEquals(this.helper.pullNorthLever, conditionalStep.getActiveStep()); + } + + @Test + void getToSouthLeverAfterInsertingKey() + { + var mockedItems = new Item[]{ + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(2); + when(client.getVarbitValue(VARBIT_NORTH_LEVER_STATE)).thenReturn(1); + var conditionalStep = this.init(new WorldPoint(3845, 4547, 0), mockedItems); + + assertEquals(this.helper.getToNorthLever, conditionalStep.getActiveStep()); + } + + @Test + void pullNorthLeverAfterPullingSouthLeverKey1() + { + var mockedItems = new Item[]{ + }; + when(client.getVarbitValue(VARBIT_SOUTH_LEVER_STATE)).thenReturn(2); + when(client.getVarbitValue(VARBIT_NORTH_LEVER_STATE)).thenReturn(1); + var conditionalStep = this.init(new WorldPoint(3894, 4597, 0), mockedItems); + + assertEquals(this.helper.pullNorthLever, conditionalStep.getActiveStep()); + } +} diff --git a/src/test/java/com/questhelper/helpers/quests/thecurseofarrav/MetalDoorSolverTest.java b/src/test/java/com/questhelper/helpers/quests/thecurseofarrav/MetalDoorSolverTest.java new file mode 100644 index 0000000000..d392301a71 --- /dev/null +++ b/src/test/java/com/questhelper/helpers/quests/thecurseofarrav/MetalDoorSolverTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2024, pajlada + * 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.helpers.quests.thecurseofarrav; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class MetalDoorSolverTest +{ + @Test + public void testValidCodes() + { + // from pajdonk + assertArrayEquals(new int[]{0, 2, 3, 5}, MetalDoorSolver.calculate("IFCB")); + + // from Gupinic + assertArrayEquals(new int[]{1, 3, 7, 2}, MetalDoorSolver.calculate("FBDG")); + + // from Avsynthe + assertArrayEquals(new int[]{1, 8, 4, 5}, MetalDoorSolver.calculate("FCEB")); + + // from Zoinkwiz + assertArrayEquals(new int[]{6, 3, 6, 4}, MetalDoorSolver.calculate("AEGH")); + + // from pajdank + assertArrayEquals(new int[]{1, 3, 4, 2}, MetalDoorSolver.calculate("BIAF")); + } + + @Test + public void testLowercaseValidCodes() + { + // from pajdonk + assertArrayEquals(new int[]{0, 2, 3, 5}, MetalDoorSolver.calculate("ifcb")); + + // from Gupinic + assertArrayEquals(new int[]{1, 3, 7, 2}, MetalDoorSolver.calculate("fbdg")); + } + + @Test + public void testInvalidCodes() + { + // Some character in the code is not valid + assertNull(MetalDoorSolver.calculate("JAAA")); + assertNull(MetalDoorSolver.calculate("AJAA")); + assertNull(MetalDoorSolver.calculate("AAJA")); + assertNull(MetalDoorSolver.calculate("AAAJ")); + } + + @Test + public void testCodeTooLong() + { + assertNull(MetalDoorSolver.calculate("AAAAA")); + assertNull(MetalDoorSolver.calculate("AAAAAAAAA")); + assertNull(MetalDoorSolver.calculate("AAAA ")); + } + + @Test + public void testCodeTooShort() + { + assertNull(MetalDoorSolver.calculate("AAA")); + assertNull(MetalDoorSolver.calculate("AA")); + assertNull(MetalDoorSolver.calculate("A")); + assertNull(MetalDoorSolver.calculate("")); + } + + @Test + public void testCodeIsNull() + { + assertNull(MetalDoorSolver.calculate(null)); + } + + @Test + public void testDistanceUp() + { + assertEquals(0, MetalDoorSolver.calculateDistanceUp(0, 0)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(1, 1)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(2, 2)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(3, 3)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(4, 4)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(5, 5)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(6, 6)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(7, 7)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(8, 8)); + assertEquals(0, MetalDoorSolver.calculateDistanceUp(9, 9)); + + assertEquals(1, MetalDoorSolver.calculateDistanceUp(0, 1)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(1, 2)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(2, 3)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(3, 4)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(4, 5)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(5, 6)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(6, 7)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(7, 8)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(8, 9)); + assertEquals(1, MetalDoorSolver.calculateDistanceUp(9, 0)); + + assertEquals(9, MetalDoorSolver.calculateDistanceUp(0, 9)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(1, 0)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(2, 1)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(3, 2)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(4, 3)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(5, 4)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(6, 5)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(7, 6)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(8, 7)); + assertEquals(9, MetalDoorSolver.calculateDistanceUp(9, 8)); + } + + @Test + public void testDistanceDown() + { + assertEquals(0, MetalDoorSolver.calculateDistanceDown(0, 0)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(1, 1)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(2, 2)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(3, 3)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(4, 4)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(5, 5)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(6, 6)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(7, 7)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(8, 8)); + assertEquals(0, MetalDoorSolver.calculateDistanceDown(9, 9)); + + assertEquals(1, MetalDoorSolver.calculateDistanceDown(0, 9)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(1, 0)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(2, 1)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(3, 2)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(4, 3)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(5, 4)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(6, 5)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(7, 6)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(8, 7)); + assertEquals(1, MetalDoorSolver.calculateDistanceDown(9, 8)); + + assertEquals(9, MetalDoorSolver.calculateDistanceDown(0, 1)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(1, 2)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(2, 3)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(3, 4)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(4, 5)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(5, 6)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(6, 7)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(7, 8)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(8, 9)); + assertEquals(9, MetalDoorSolver.calculateDistanceDown(9, 0)); + } +} diff --git a/src/test/java/com/questhelper/questhelpers/QuestHelperTest.java b/src/test/java/com/questhelper/questhelpers/QuestHelperTest.java index 7cbc628dd0..0d7a260193 100644 --- a/src/test/java/com/questhelper/questhelpers/QuestHelperTest.java +++ b/src/test/java/com/questhelper/questhelpers/QuestHelperTest.java @@ -2,15 +2,19 @@ import com.questhelper.MockedTest; import com.questhelper.domain.AccountType; +import com.questhelper.panel.PanelDetails; import com.questhelper.questinfo.QuestHelperQuest; import com.questhelper.requirements.Requirement; import com.questhelper.requirements.item.ItemRequirement; import com.questhelper.requirements.zone.ZoneRequirement; import com.questhelper.statemanagement.AchievementDiaryStepManager; import java.lang.reflect.Field; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertFalse; +import com.questhelper.steps.ConditionalStep; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -169,4 +173,58 @@ void ensureAllVariablesCorrectlySet() } } } + + // @Test + // void ensureAllStepsHaveSidebarLink() + // { + // when(questHelperConfig.solvePuzzles()).thenReturn(true); + + // AchievementDiaryStepManager.setup(configManager); + + // for (var quest : QuestHelperQuest.values()) + // { + // var helper = quest.getQuestHelper(); + // helper.setQuest(quest); + // if (quest.getPlayerQuests() != null) + // { + // continue; + // } + + // this.injector.injectMembers(helper); + // helper.setQuestHelperPlugin(questHelperPlugin); + // helper.setConfig(questHelperConfig); + // helper.init(); + + // if (quest != QuestHelperQuest.THE_CURSE_OF_ARRAV) { + // continue; + // } + + // if (helper instanceof BasicQuestHelper) { + // var basicHelper = (BasicQuestHelper) helper; + // var panels = helper.getPanels(); + // var panelSteps = panels.stream().flatMap(panelDetails -> panelDetails.getSteps().stream()).collect(Collectors.toList()); + // var steps = basicHelper.getStepList().values(); + // for (var step : steps) { + // assertNotNull(step); + // var rawText = step.getText(); + // var text = rawText == null ? "" : String.join("\n", step.getText()); + // if (step instanceof ConditionalStep) { + // // + // } else { + // var isInPanelSteps = panelSteps.contains(step); + // /* TODO + // var isSubstepOf = steps.stream().filter(questStep -> { + // if (questStep instanceof BasicQuest) { + // return questStep.getSubSteps(); + // } + // return null; + // }); + // */ + // var isInAnyStepSubStepsThatIsInPanelSteps = true; // todo + // assertTrue(isInPanelSteps); + // } + // } + // } + // } + // } }