diff --git a/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/RubbleMiner.java b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/RubbleMiner.java new file mode 100644 index 0000000000..339f0b8518 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/RubbleMiner.java @@ -0,0 +1,194 @@ +package com.questhelper.helpers.quests.thecurseofarrav; + +import com.questhelper.helpers.quests.thepathofglouphrie.DiscInsertionStep; +import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.conditional.Conditions; +import com.questhelper.requirements.conditional.ObjectCondition; +import com.questhelper.requirements.item.ItemRequirement; +import com.questhelper.requirements.util.LogicType; +import com.questhelper.requirements.widget.WidgetPresenceRequirement; +import com.questhelper.steps.*; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.inject.Inject; + +import com.questhelper.steps.tools.QuestPerspective; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.Direction; +import net.runelite.api.coords.LocalPoint; +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; + +import static com.questhelper.requirements.util.LogicHelper.and; +import static com.questhelper.requirements.util.LogicHelper.nor; + +@Slf4j +public class RubbleMiner extends DetailedOwnerStep { + @Inject + Client client; + + private List mineSteps; + private List conditions; + private ConditionalStep conditionalStep; + + private int operation; + + public RubbleMiner(TheCurseOfArrav theCurseOfArrav) { + super(theCurseOfArrav, "Make your way through the Trollweiss cave, mining rubble with your pickaxe."); + } + + + private void addMineRubbleStep(int x, int y, RubbleLevel rubbleLevel, Direction direction) { + var validObjectIDs = rubbleLevel.getObjectIDs(); + assert !validObjectIDs.isEmpty(); + + var validIDSet = new HashSet<>(validObjectIDs); + + var wp = new WorldPoint(x, y, 0); + var operation = this.operation++; + var text = String.format("[%d] Mine the rubble from the %s side", operation, direction.toString().toLowerCase()); + log.info("setting up mine rubble step: {}: {}", operation, text); + var mainObjectID = validObjectIDs.get(0); + var step = new ObjectStep(getQuestHelper(), mainObjectID, wp, text); + 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("[%d] Rubble mined from the %s side", operation, direction.toString().toLowerCase()); + var conditionThatThisStepHasBeenDone = new ObjectCondition(validIDSet, wp); + conditionThatThisStepHasBeenDone.setText(text); + + this.mineSteps.add(step); + this.conditions.add(conditionThatThisStepHasBeenDone); + } + + @Subscribe + public void onGameTick(GameTick event) { + updateSteps(); + } + + @Override + public void startUp() { + updateSteps(); + } + + @Override + protected void setupSteps() { + this.operation = 1; + this.mineSteps = new ArrayList<>(); + this.conditions = new ArrayList<>(); + + var todo = new DetailedQuestStep(getQuestHelper(), "todo"); + + this.addMineRubbleStep(2764, 10266, RubbleLevel.Two, Direction.SOUTH); + this.addMineRubbleStep(2775, 10258, RubbleLevel.One, Direction.SOUTH); + this.addMineRubbleStep(2764, 10266, RubbleLevel.One, Direction.EAST); + this.addMineRubbleStep(2764, 10267, RubbleLevel.One, Direction.SOUTH); + + // after reversing + // mineStep 0: mine C + // mineStep 1: mine B + // mineStep 2: mine A + + // condition 0: A is not at the given state + // condition 1: B is not at the given state + // condition 2: C is not at the given state + + // i = 0: Mine C, if B and A are mined + // i = 1: Mine B, if A is mined + // i = 2: Mine A, with no condition + + conditionalStep = new ConditionalStep(getQuestHelper(), todo); + Collections.reverse(mineSteps); + + assert this.mineSteps.size() == this.conditions.size(); + + { + var allDone = new DetailedQuestStep(getQuestHelper(), "you are all done lol"); + var conditionList = new ArrayList(); + for (var condition : this.conditions) { + var xd2 = new Conditions(LogicType.NAND, condition); + xd2.setText(condition.getDisplayText()); + allDone.addRequirement(xd2); + } + var xd = new Conditions(LogicType.NAND, conditionList); + conditionalStep.addStep(xd, allDone); + } + for (var i = 0; i < mineSteps.size(); i++) { + var mineStep = mineSteps.get(i); + var conditionList = new ArrayList(); + StringBuilder text = new StringBuilder(); + for (var j = 0; j < this.conditions.size() - i - 1; j++) { + conditionList.add(this.conditions.get(j)); + text.append(this.conditions.get(j).getDisplayText()); + var xd2 = new Conditions(LogicType.NAND, this.conditions.get(j)); + xd2.setText(this.conditions.get(j).getDisplayText()); + mineStep.addRequirement(xd2); + } + + var xd = new Conditions(LogicType.NOR, conditionList); + xd.setText(text.toString()); + + conditionalStep.addStep(xd, mineStep); + } + } + + protected void updateSteps() { + startUpStep(this.conditionalStep); + } + + @Override + public List getSteps() { + var steps = new ArrayList(); + + steps.add(this.conditionalStep); + for (var step : this.mineSteps) { + steps.add(step); + } + + return steps; + } + + @Getter + enum RubbleLevel { + Three(123), + Two(ObjectID.RUBBLE_50598), + One(ObjectID.RUBBLE_50587, ObjectID.RUBBLE_50589); + + private final List objectIDs; + + RubbleLevel(Integer... possibleObjectIDs) { + this.objectIDs = new ArrayList(); + for (var xd : possibleObjectIDs) { + this.objectIDs.add(xd); + } + // Collections.addAll(this.objectIDs, possibleObjectIDs); + } + } +} 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..468bf6f3ed --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TheCurseOfArrav.java @@ -0,0 +1,548 @@ +/* + * 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.panel.PanelDetails; +import com.questhelper.questhelpers.BasicQuestHelper; +import com.questhelper.questinfo.QuestHelperQuest; +import com.questhelper.requirements.Requirement; +import com.questhelper.requirements.conditional.ObjectCondition; +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 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.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.ObjectID; +import net.runelite.api.QuestState; +import net.runelite.api.Skill; +import net.runelite.api.SpriteID; +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 + */ +public class TheCurseOfArrav extends BasicQuestHelper +{ + /// Required items + private ItemRequirement dwellberries3; + private ItemRequirement ringOfLife; + private ItemRequirement anyPickaxe; + private ItemRequirement anyGrappleableCrossbow; + private ItemRequirement mithrilGrapple; + private ItemRequirement insulatedBoots; + + /// Recommended items + // teleport to fairy ring + // fairy ring staff + // trollheim teleport / ghommal's hilt + // antivenom + // lumberyard teleport + // melee (crush) combat gear for golem + // ranged combat gear for arrav + private ItemRequirement crushCombatGear; + private ItemRequirement rangedCombatGear; + private ItemRequirement food; + private ItemRequirement staminaPotion; + private ItemRequirement prayerPotion; + // 2 inv slots + + /// Mid-quest item requirements + private ItemRequirement experimentalKebab; + private ItemRequirement goodTestKebab; + private ItemRequirement goodTestKebabs; + + /// Zones & their requirements + private Zone wolfDen; + private ZoneRequirement inWolfDen; + private Zone fortisColosseum; + private ZoneRequirement inFortisColosseum; + + /// Steps + private QuestStep startQuest; + private QuestStep enterTomb; + private ObjectStep getFirstKey; + private ObjectStep getSecondKey; + private ObjectStep pullSouthLever; + private ObjectStep pullNorthLever; + private ObjectStep enterGolemArena; + private NpcStep fightGolemGuard; + private ObjectStep enterTombBasement; + private QuestStep unsortedStep6; + private ConditionalStep unsortedStep10; + private PuzzleWrapperStep solveTilePuzzle; + private QuestStep unsortedStep12; + private QuestStep unsortedStep14; + private QuestStep unsortedStep16; + private QuestStep unsortedStep18; + private QuestStep unsortedStep20; + private QuestStep unsortedStep22; + private QuestStep unsortedStep24; + private QuestStep unsortedStep26; + private QuestStep unsortedStep28; + private QuestStep unsortedStep30; + private QuestStep unsortedStep32; + private QuestStep unsortedStep34; + private QuestStep unsortedStep36; + private QuestStep unsortedStep38; + private QuestStep unsortedStep40; + private QuestStep unsortedStep42; + private QuestStep unsortedStep44; + private QuestStep unsortedStep46; + private QuestStep unsortedStep48; + private QuestStep unsortedStep50; + private QuestStep unsortedStep52; + private QuestStep unsortedStep54; + private QuestStep unsortedStep56; + private QuestStep unsortedStep58; + private PuzzleWrapperStep rubbleMiner; + + + @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, unsortedStep6); + steps.put(8, unsortedStep6); + steps.put(10, unsortedStep10); + steps.put(12, unsortedStep12); + steps.put(14, unsortedStep12); + steps.put(16, unsortedStep16); + steps.put(18, unsortedStep18); + steps.put(20, unsortedStep20); + steps.put(22, unsortedStep20); + steps.put(24, unsortedStep24); + steps.put(26, unsortedStep26); + steps.put(28, unsortedStep28); + steps.put(30, unsortedStep30); + steps.put(32, unsortedStep32); + steps.put(34, unsortedStep34); + steps.put(36, unsortedStep36); + steps.put(38, unsortedStep38); + steps.put(40, unsortedStep40); + steps.put(42, unsortedStep42); + steps.put(44, unsortedStep44); + steps.put(46, unsortedStep46); + steps.put(48, unsortedStep48); + steps.put(50, unsortedStep50); + steps.put(52, unsortedStep52); + steps.put(54, unsortedStep54); + steps.put(56, unsortedStep56); + steps.put(58, unsortedStep58); + + return steps; + } + + @Override + protected void setupZones() + { + } + + @Override + protected void setupRequirements() + { + dwellberries3 = new ItemRequirement("Dwellberries", ItemID.DWELLBERRIES, 3); + ringOfLife = new ItemRequirement("Ring of life", ItemID.RING_OF_LIFE); + anyPickaxe = new ItemRequirement("Any pickaxe", ItemCollections.PICKAXES).isNotConsumed(); + anyGrappleableCrossbow = new ItemRequirement("Any crossbow", ItemCollections.CROSSBOWS).isNotConsumed(); + mithrilGrapple = new ItemRequirement("Mith grapple", ItemID.MITH_GRAPPLE_9419).isNotConsumed(); + // TODO: Check if the other insulated boots can be used + insulatedBoots = new ItemRequirement("Insulated boots", ItemID.INSULATED_BOOTS).isNotConsumed(); + + staminaPotion = new ItemRequirement("Stamina potion", ItemCollections.STAMINA_POTIONS, 1); + prayerPotion = new ItemRequirement("Prayer potion", ItemCollections.PRAYER_POTIONS, 1); + crushCombatGear = new ItemRequirement("Melee combat gear (crush preferred)", -1, -1); + crushCombatGear.setDisplayItemId(BankSlotIcons.getCombatGear()); + food = new ItemRequirement("Food", ItemCollections.GOOD_EATING_FOOD, -1); + } + + public void setupSteps() + { + var todo = new NpcStep(this, 5, "TODO XD"); + + startQuest = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Talk to Elias south of Ruins of Uzer (DLQ FAIRY RING)."); + startQuest.addDialogStep("Yes."); + + enterTomb = new ObjectStep(this, ObjectID.ENTRY_50201, new WorldPoint(3486, 3023, 0), "Enter the tomb south-west of Elias."); + // TODO: Ensure player can get hint to return + + var insideTomb = new Zone(new WorldPoint(3842, 4603, 0), new WorldPoint(3900, 4547, 0)); + var insideTombReq = new ZoneRequirement(insideTomb); + var hasFirstKey = new ItemRequirement("Mastaba Key", ItemID.MASTABA_KEY); + getFirstKey = new ObjectStep(this, ObjectID.SKELETON_50350, new WorldPoint(3875, 4554, 0), "Get the first Mastaba key from the skeleton to the south of the entrance."); + var hasSecondKey = new ItemRequirement("Mastaba Key", ItemID.MASTABA_KEY_30309); + getSecondKey = new ObjectStep(this, ObjectID.SKELETON_50353, new WorldPoint(3880, 4585, 0), "Get the second Mastaba key from the skeleton east of the entrance."); + var bySouthLever = new Zone(new WorldPoint(3893, 4554, 0), new WorldPoint(3894, 4552, 0)); + var bySouthLeverReq = new ZoneRequirement(bySouthLever); + pullSouthLever = new ObjectStep(this, ObjectID.LEVER_50205, new WorldPoint(3894, 4553, 0), "Pull the lever to the south-east.", hasSecondKey); + pullSouthLever.addDialogStep("Yes."); + var 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."); + var needToInsertKeyInSouthLever = new VarbitRequirement(11482, 0); + var needToFlipSouthLever = new VarbitRequirement(11482, 1); + var haveFlippedSouthLever = new VarbitRequirement(11482, 2); + var 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); + + var byNorthLever = new Zone(new WorldPoint(3894, 4597, 0), new WorldPoint(3893, 4599, 0)); + var byNorthLeverReq = new ZoneRequirement(byNorthLever); + var 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.", hasFirstKey); + pullNorthLever.addDialogStep("Yes."); + var needToInsertKeyInNorthLever = new VarbitRequirement(11481, 0); + var needToFlipNorthLever = new VarbitRequirement(11481, 1); + var haveFlippedNorthLever = new VarbitRequirement(11481, 2); + var 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); + + unsortedStep6 = new ConditionalStep(this, enterTomb); + + // Get inside the tomb if you're not already inside. In case the user has teleported out or died to golem? + ((ConditionalStep) unsortedStep6).addStep(not(insideTombReq), enterTomb); + + // If the user has flipped the south lever & needs to get out of the little room + ((ConditionalStep) unsortedStep6).addStep(and(haveFlippedSouthLever, bySouthLeverReq), leaveSouthLever); + // If the user has flipped the north lever & needs to get out of the little room + ((ConditionalStep) unsortedStep6).addStep(and(haveFlippedNorthLever, byNorthLeverReq), leaveNorthLever); + + // If the user has not already put the key in the south lever, and does not have the key + ((ConditionalStep) unsortedStep6).addStep(and(needToInsertKeyInSouthLever, not(hasSecondKey)), getSecondKey); + // If the user has not already put the key in the north lever, and does not have the key + ((ConditionalStep) unsortedStep6).addStep(and(needToInsertKeyInNorthLever, not(hasFirstKey)), getFirstKey); + + // If the user has the key & stands by the south lever + ((ConditionalStep) unsortedStep6).addStep(and(needToFlipSouthLever, bySouthLeverReq), pullSouthLever); + // If the user needs to flip the south lever, but is not inside the little room, get to the little room + ((ConditionalStep) unsortedStep6).addStep(not(haveFlippedSouthLever), getToSouthLever); + + // If the user has the key & stands by the north lever + ((ConditionalStep) unsortedStep6).addStep(and(needToFlipNorthLever, byNorthLeverReq), pullNorthLever); + // If the user needs to flip the north lever, but is not inside the little room, get to the little room + ((ConditionalStep) unsortedStep6).addStep(needToFlipNorthLever, getToNorthLever); + + // Once last lever was 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 + + var golemArenaZone = new Zone(new WorldPoint(3856, 4592, 0), new WorldPoint(3884, 4599, 0)); + var insideGolenArena = new ZoneRequirement(golemArenaZone); + 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. He 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."); + unsortedStep10 = 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? + unsortedStep10.addStep(not(insideTombReq), enterTomb); + unsortedStep10.addStep(byNorthLeverReq, leaveNorthLever); + unsortedStep10.addStep(bySouthLeverReq, leaveSouthLever); + unsortedStep10.addStep(insideGolenArena, 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."); + + var insideTombSecondFloor = new Zone(new WorldPoint(3719, 4674, 0), new WorldPoint(3770, 4732, 0)); + var insideTombSecondFloorAfterFinishingPuzzle = new Zone(new WorldPoint(3845, 4674, 0), new WorldPoint(3900, 4732, 0)); + var insideTombSecondFloorReq = new ZoneRequirement(insideTombSecondFloor, insideTombSecondFloorAfterFinishingPuzzle); + + var 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); + + var inspectMurals = new ObjectStep(this, ObjectID.MURAL_55790, new WorldPoint(3852, 4687, 0), "Inspect the murals in the room to the south.", oilFilledCanopicJar); + + var finishedTilePuzzle = new VarbitRequirement(11483, 1); + unsortedStep12 = new ConditionalStep(this, todo); + ((ConditionalStep) unsortedStep12).addStep(and(insideTombSecondFloorReq, finishedTilePuzzle, oilFilledCanopicJar), inspectMurals); + ((ConditionalStep) unsortedStep12).addStep(and(insideTombSecondFloorReq, finishedTilePuzzle, oilFilledCanopicJar), todo); + ((ConditionalStep) unsortedStep12).addStep(and(insideTombSecondFloorReq, finishedTilePuzzle), searchShelvesForUrn); + ((ConditionalStep) unsortedStep12).addStep(insideTombSecondFloorReq, solveTilePuzzle); + ((ConditionalStep) unsortedStep12).addStep(not(insideTombReq), enterTomb); + ((ConditionalStep) unsortedStep12).addStep(and(insideTombReq, insideGolenArena), enterTombBasement); + ((ConditionalStep) unsortedStep12).addStep(and(insideTombReq), enterGolemArenaWithoutFight); + + var oilAndBerryFilledCanopicJar = new ItemRequirement("Canopic jar (oil and berries)", ItemID.CANOPIC_JAR_OIL_AND_BERRIES); + + var combineJarWithDwellberries = new ItemStep(this, "Combine the Dwellberries with the Canopic jar.", oilFilledCanopicJar.highlighted(), dwellberries3.highlighted(), ringOfLife); + var combineJarWithRingOfLife = new ItemStep(this, "Combine the Dwellberries with the Ring of life.", oilAndBerryFilledCanopicJar.highlighted(), ringOfLife.highlighted()); + + unsortedStep16 = new ConditionalStep(this, todo); + ((ConditionalStep) unsortedStep16).addStep(oilAndBerryFilledCanopicJar, combineJarWithRingOfLife); + ((ConditionalStep) unsortedStep16).addStep(oilFilledCanopicJar, combineJarWithDwellberries); + ((ConditionalStep) unsortedStep16).addStep(and(insideTombSecondFloorReq, finishedTilePuzzle), searchShelvesForUrn); + ((ConditionalStep) unsortedStep16).addStep(not(insideTombReq), enterTomb); + ((ConditionalStep) unsortedStep16).addStep(and(insideTombReq, insideGolenArena), enterTombBasement); + ((ConditionalStep) unsortedStep16).addStep(and(insideTombReq), enterGolemArenaWithoutFight); + + var fairyRingDLQ = new TeleportItemRequirement("Fairy Ring [DLQ]", ItemCollections.FAIRY_STAFF); + + var returnToElias = new NpcStep(this, NpcID.ELIAS_WHITE, new WorldPoint(3505, 3037, 0), "Return to to Elias south of Ruins of Uzer, either by walking out of the tomb or using the fairy ring."); + returnToElias.addTeleport(fairyRingDLQ); + var returnToEliasByWalking = new ObjectStep(this, ObjectID.STAIRS_55786, new WorldPoint(3894, 4714, 0), "Return to 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 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 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); + + unsortedStep18 = new ConditionalStep(this, returnToElias); + ((ConditionalStep)unsortedStep18).addStep(insideTombSecondFloorReq, returnToEliasByWalking); + ((ConditionalStep)unsortedStep18).addStep(insideGolenArena, returnToEliasByWalkingMidwayGolem); + ((ConditionalStep)unsortedStep18).addStep(insideTombReq, returnToEliasByWalkingMidway); + // ardy cloak + fairy ring takes 50s, walking takes 1m12s + + var trollheimTeleport = new TeleportItemRequirement("Trollheim Teleport", ItemID.TROLLHEIM_TELEPORT); + trollheimTeleport.addAlternates(ItemCollections.GHOMMALS_HILT); + var 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."); + headToTrollheim.addTeleport(trollheimTeleport); + + var trollheimCave = new Zone(11167); + var inTrollheimCave = new ZoneRequirement(trollheimCave); + var 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."); + + var trollweissMountain = new Zone(11068); + var onTrollweissMountain = new ZoneRequirement(trollweissMountain); + var enterTrollweissCave = new ObjectStep(this, ObjectID.CAVE_55779, new WorldPoint(2809, 3861, 0), "Enter the Trollweiss cave to the east."); + + var trollweissCave1 = new Zone(11168); + var inTrollweissCave = new ZoneRequirement(trollweissCave1); + + var whereToStandSprite = SpriteID.COMBAT_STYLE_PICKAXE_SMASH; + + // Rubble 50598 = 2 hits + // Rubble 50587/50589 = 1 hit + + // var mineRubble1FromSouth = new ObjectStep(this, ObjectID.RUBBLE_50598, new WorldPoint(2764, 10266, 0), "Mine the rubble from the south."); + // mineRubble1FromSouth.addTileMarker(new WorldPoint(2764, 10265, 0), whereToStandSprite); + + // var rubble1MinedOnce = new ObjectCondition(ObjectID.RUBBLE_50589, new WorldPoint(2764, 10266, 0)); + // var rubble2Mined = not(new ObjectCondition(ObjectID.RUBBLE_50589, new WorldPoint(2775, 10258, 0))); + + // var mineRubble2FromSouth = new ObjectStep(this, ObjectID.RUBBLE_50587, new WorldPoint(2775, 10258, 0), "Mine the rubble from the south."); + // mineRubble2FromSouth.addTileMarker(new WorldPoint(2775, 10257, 0), whereToStandSprite); + // mineRubble2FromSouth.setLinePoints(List.of(new WorldPoint(2763, 10264, 0), new WorldPoint(2769, 10254, 0), new WorldPoint(2775, 10255, 0))); + + rubbleMiner = new RubbleMiner(this).puzzleWrapStep("make your way through the mine"); + + unsortedStep20 = new ConditionalStep(this, headToTrollheim); + ((ConditionalStep) unsortedStep20).addStep(inTrollweissCave, rubbleMiner); + ((ConditionalStep) unsortedStep20).addStep(onTrollweissMountain, enterTrollweissCave); + ((ConditionalStep) unsortedStep20).addStep(inTrollheimCave, continueThroughTrollheimCave); + + unsortedStep22 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 22"); + unsortedStep24 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 24"); + unsortedStep26 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 26"); + unsortedStep28 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 28"); + unsortedStep30 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 30"); + unsortedStep32 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 32"); + unsortedStep34 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 34"); + unsortedStep36 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 36"); + unsortedStep38 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 38"); + unsortedStep40 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 40"); + unsortedStep42 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 42"); + unsortedStep44 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 44"); + unsortedStep46 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 46"); + unsortedStep48 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 48"); + unsortedStep50 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 50"); + unsortedStep52 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 52"); + unsortedStep54 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 54"); + unsortedStep56 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 56"); + unsortedStep58 = new NpcStep(this, NpcID.YELLOW_FORTUNE_SECRETARY, "Step 58"); + + } + + @Override + public List getItemRequirements() + { + return List.of( + dwellberries3, + ringOfLife, + anyPickaxe, + anyGrappleableCrossbow, + mithrilGrapple, + insulatedBoots + ); + } + + @Override + public List getItemRecommended() + { + return List.of( + staminaPotion, + prayerPotion, + crushCombatGear, + food + ); + } + + @Override + public List getGeneralRecommended() + { + return List.of( + new CombatLevelRequirement(85), + new SkillRequirement(Skill.PRAYER, 43, false, "43+ Prayer to use protection prayers") + ); + } + + @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, + unsortedStep16, + unsortedStep18 + ), List.of( + new FreeInventorySlotRequirement(2) + // TODO + ))); + panels.add(new PanelDetails("Fort Invasion", List.of( + unsortedStep20, + rubbleMiner, + unsortedStep22, + unsortedStep24, + unsortedStep26, + unsortedStep28, + unsortedStep30, + unsortedStep32, + unsortedStep34, + unsortedStep36, + unsortedStep38, + unsortedStep40, + unsortedStep42, + unsortedStep44, + unsortedStep46, + unsortedStep48, + unsortedStep50, + unsortedStep52, + unsortedStep54, + unsortedStep56, + unsortedStep58 + // TODO + ), List.of(staminaPotion, prayerPotion, crushCombatGear, food))); + panels.add(new PanelDetails("Hearty Heist", List.of( + // TODO + ), List.of( + // TODO + ))); + + panels.add(new PanelDetails("The Hero of Avarrocka", List.of( + // TODO + ), List.of( + // TODO + ))); + + 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..07c0a56328 --- /dev/null +++ b/src/main/java/com/questhelper/helpers/quests/thecurseofarrav/TilePuzzleSolver.java @@ -0,0 +1,396 @@ +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; + private ObjectStep firstObjectStepXD; + private ObjectStep[][] objectSteps; + + List shortestPath = null; + private DetailedQuestStep fallbackStep; + private QuestStep finishPuzzleStep; + private ObjectStep mostRecentStep = null; + + public TilePuzzleSolver(TheCurseOfArrav theCurseOfArrav) + { + super(theCurseOfArrav, "Solve the floor tile puzzle. Follow the instructions in the overlay."); + } + + /** + * @param startX local X coordinate of the tiles array for where to start searching + * @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 startX, int startY) + { + assert (startX >= 0 && startX < SIZE); + 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(11, y); + if (path != null) + { + log.debug("Found possible path starting at {}/{}. Length {}", 11, y, path.size()); + possiblePaths.add(path); + } + } + + for (var possiblePath : possiblePaths) + { + if (this.shortestPath == null || possiblePath.size() < this.shortestPath.size()) + { + this.shortestPath = possiblePath; + } + } + + if (this.shortestPath == null) + { + // TODO: tell user to just find a way to solve it lol + } + else + { + // TODO: figure out how to highlight this? make object steps? + // How do I get the correct world points? maybe check objectstep to see if we offset it somewhere with instance points or something + } + } + + updateSteps(); + } + + @Override + public void startUp() + { + updateSteps(); + } + + @Override + protected void setupSteps() + { + objectSteps = new ObjectStep[SIZE][SIZE]; + var baseX = 3737; + var baseY = 4709; + var plane = 0; + for (int x = 0; x < SIZE; ++x) + { + var objectText = "Click the tile to pass through the puzzle."; + if (x == 11) { + objectText = "Click the tile to start the puzzle."; + } + for (int y = 0; y < SIZE; ++y) + { + var wp = new WorldPoint(baseX + x, baseY + y, plane); + var objectStep = new ObjectStep(getQuestHelper(), GREEN_TILE, wp, objectText); + objectStep.addAlternateObjects(BLUE_TILE, RED_TILE, YELLOW_TILE); + if (firstObjectStepXD == null) { + firstObjectStepXD = objectStep; + } + objectSteps[x][y] = objectStep; + } + } + + 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; + + var puzzleStartX = this.shortestPath.get(0)[0]; + var puzzleStartY = this.shortestPath.get(0)[1]; + + if (xInPuzzle >= 0 && xInPuzzle < SIZE && yInPuzzle >= 0 && yInPuzzle < SIZE) { + log.info("Player is in the puzzle, at {}/{}", xInPuzzle, yInPuzzle); + boolean nextIsOurCoolStep = false; + for (var pathPos : this.shortestPath) { + if (nextIsOurCoolStep) { + startUpStep(objectSteps[pathPos[0]][pathPos[1]]); + return; + } + if (pathPos[0] == xInPuzzle && pathPos[1] == yInPuzzle) { + nextIsOurCoolStep = true; + this.mostRecentStep = objectSteps[pathPos[0]][pathPos[1]]; + } + } + if (nextIsOurCoolStep) { + log.info("user is at end"); + startUpStep(finishPuzzleStep); + return; + } + + if (this.mostRecentStep != null) + { + log.debug("user stepped off the path, lead them back"); + startUpStep(this.mostRecentStep); + return; + } + } else { + log.debug("player is outside of puzzle: {} / {} / {}/{}", playerWp, localPoint, xInPuzzle, yInPuzzle); + var userIsPastPuzzle = localPoint.getX() <= baseX; // TODO: If the user walks to the cave to the south, we might tell the user to just click the lever. I need to figure out the correct zone here. polish pass! + if (userIsPastPuzzle) { + // highlight lever + startUpStep(finishPuzzleStep); + } else { + // highlight puzzle start + startUpStep(objectSteps[puzzleStartX][puzzleStartY]); + } + } + } + + @Override + public List getSteps() + { + var steps = new ArrayList(); + steps.add(fallbackStep); + steps.add(firstObjectStepXD); + + for (int x = 0; x < SIZE; ++x) + { + for (int y = 0; y < SIZE; ++y) + { + steps.add(objectSteps[x][y]); + } + } + + steps.add(finishPuzzleStep); + + return steps; + } +} 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..3183b16ec0 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?) { - 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..f579d84859 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.List; +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,18 @@ 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); + + this.objectIDs = objectIDs; + this.zone = new Zone(worldPoint); + } + public boolean check(Client client) { Tile[][] tiles; @@ -118,7 +128,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