From a007416e54b9ad37316224dd513e6442b7cfdbe9 Mon Sep 17 00:00:00 2001 From: Chillibloke <87402686+Chillibloke@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:28:28 +1030 Subject: [PATCH] Add Slayer plugin v1.0.0 with full automation loop (#308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated slayer plugin handling the complete task cycle: getting assignments from any of 8 slayer masters, banking for supplies, traveling to task locations, fighting monsters with prayer/potion management, looting, cannon support, and returning for new tasks. Key features: - Per-task JSON profile system (gear, style, prayer, location, potions) - 40+ predefined cannon spots, 80+ location keys - Prayer flicking (lazy, perfect lazy, mixed), AOE dodging - Superior monster prioritization, world hop on crash - Auto skip/block tasks with point safety reserves - POH pool restoration with spellbook swap - Death safety (auto-logout to pause gravestone) - Break handler integration - User documentation (docs/README.md) Co-authored-by: chsami Co-authored-by: eemil Co-authored-by: Chris Gemmell Co-authored-by: Jesús Hernández <62583125+jesufh@users.noreply.github.com> Co-authored-by: Netoxic <35343567+Netoxique@users.noreply.github.com> Co-authored-by: Gage307 Co-authored-by: davidja92 Co-authored-by: FunkyMonkeyCloud Co-authored-by: FunkyMonkeyCloud Co-authored-by: Igor <74077743+Bolado@users.noreply.github.com> Co-authored-by: Jamdrizzle Co-authored-by: Jam Co-authored-by: eqp48 Co-authored-by: VIPDO1 <74063365+VIPDO1@users.noreply.github.com> Co-authored-by: Syntax <79747812+Syntax2022@users.noreply.github.com> Co-authored-by: Syntax Co-authored-by: Pixelated Co-authored-by: Claude Opus 4.6 --- .../plugins/microbot/PluginConstants.java | 1 + .../plugins/microbot/slayer/CannonSpot.java | 154 + .../plugins/microbot/slayer/LootStyle.java | 23 + .../microbot/slayer/PohTeleportMethod.java | 22 + .../microbot/slayer/PrayerFlickStyle.java | 26 + .../microbot/slayer/SlayerCombatStyle.java | 65 + .../plugins/microbot/slayer/SlayerConfig.java | 672 +++ .../microbot/slayer/SlayerLocation.java | 183 + .../plugins/microbot/slayer/SlayerMaster.java | 29 + .../microbot/slayer/SlayerOverlay.java | 100 + .../plugins/microbot/slayer/SlayerPlugin.java | 237 + .../plugins/microbot/slayer/SlayerPrayer.java | 83 + .../plugins/microbot/slayer/SlayerScript.java | 4066 +++++++++++++++++ .../plugins/microbot/slayer/SlayerState.java | 25 + .../microbot/slayer/combat/AttackStyle.java | 11 + .../slayer/combat/SlayerDodgeScript.java | 121 + .../slayer/combat/SlayerFlickerScript.java | 249 + .../slayer/combat/SlayerHighAlchScript.java | 221 + .../microbot/slayer/combat/SlayerMonster.java | 36 + .../slayer/profile/SlayerProfileManager.java | 548 +++ .../slayer/profile/SlayerTaskProfileJson.java | 192 + .../plugins/microbot/slayer/docs/README.md | 295 ++ src/main/resources/slayer_icon.png | Bin 0 -> 503 bytes .../java/net/runelite/client/Microbot.java | 8 +- 24 files changed, 7361 insertions(+), 6 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/CannonSpot.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/LootStyle.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/PohTeleportMethod.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/PrayerFlickStyle.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerCombatStyle.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerConfig.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerLocation.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerMaster.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerOverlay.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPrayer.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerState.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/combat/AttackStyle.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerDodgeScript.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerFlickerScript.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerHighAlchScript.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerMonster.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerProfileManager.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerTaskProfileJson.java create mode 100644 src/main/resources/net/runelite/client/plugins/microbot/slayer/docs/README.md create mode 100644 src/main/resources/slayer_icon.png diff --git a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java index d2fba60532..3c28f7f994 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java +++ b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java @@ -33,6 +33,7 @@ private PluginConstants() public static final String VIP = "[VIP] "; public static final String NATE = "[N] "; public static final String SYN = "[Syn] "; + public static final String BIGL = "[BL] "; public static final boolean DEFAULT_ENABLED = false; public static final boolean IS_EXTERNAL = true; //test diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/CannonSpot.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/CannonSpot.java new file mode 100644 index 0000000000..1e73fbd773 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/CannonSpot.java @@ -0,0 +1,154 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +import java.util.Arrays; +import java.util.List; + +/** + * Predefined cannon placement spots for slayer tasks. + * Data sourced from RuneLite's CannonSpots enum. + */ +@Getter +@RequiredArgsConstructor +public enum CannonSpot { + ABERRANT_SPECTRES("Aberrant spectre", new WorldPoint(2456, 9791, 0)), + ANKOU("Ankou", new WorldPoint(3177, 10193, 0), new WorldPoint(3360, 10077, 0)), + BANDIT("Bandit", new WorldPoint(3037, 3700, 0)), + BEAR("Bear", new WorldPoint(3113, 3672, 0)), + BLACK_DEMONS("Black demon", + new WorldPoint(2859, 9778, 0), + new WorldPoint(2841, 9791, 0), + new WorldPoint(1421, 10089, 1), + new WorldPoint(3174, 10154, 0), + new WorldPoint(3089, 9960, 0)), + BLACK_DRAGON("Black dragon", new WorldPoint(3239, 10206, 0), new WorldPoint(3362, 10156, 0)), + BLACK_KNIGHTS("Black knight", new WorldPoint(2906, 9685, 0), new WorldPoint(3053, 3852, 0)), + BLOODVELDS("Bloodveld", + new WorldPoint(2439, 9821, 0), + new WorldPoint(2448, 9821, 0), + new WorldPoint(2472, 9832, 0), + new WorldPoint(2453, 9817, 0), + new WorldPoint(3596, 9743, 0)), + BLUE_DRAGON("Blue dragon", new WorldPoint(1933, 8973, 1)), + BRINE_RAT("Brine rat", new WorldPoint(2707, 10132, 0)), + CAVE_HORROR("Cave horror", new WorldPoint(3785, 9460, 0)), + DAGANNOTH("Dagannoth", + new WorldPoint(2524, 10020, 0), + new WorldPoint(2478, 10443, 0), + new WorldPoint(2420, 10425, 0)), + DARK_BEAST("Dark beast", new WorldPoint(1992, 4655, 0)), + DARK_WARRIOR("Dark warrior", new WorldPoint(3030, 3632, 0)), + DUST_DEVIL("Dust devil", new WorldPoint(3218, 9366, 0)), + EARTH_WARRIOR("Earth warrior", new WorldPoint(3120, 9987, 0)), + ELDER_CHAOS_DRUID("Elder Chaos druid", new WorldPoint(3237, 3622, 0)), + ELVES("Elf", new WorldPoint(3278, 6098, 0)), + FIRE_GIANTS("Fire giant", + new WorldPoint(2393, 9782, 0), + new WorldPoint(2412, 9776, 0), + new WorldPoint(2401, 9780, 0), + new WorldPoint(3047, 10340, 0)), + GREATER_DEMONS("Greater demon", + new WorldPoint(1435, 10086, 2), + new WorldPoint(3224, 10132, 0), + new WorldPoint(3427, 10149, 0)), + GREEN_DRAGON("Green dragon", new WorldPoint(3225, 10068, 0), new WorldPoint(3399, 10122, 0)), + HELLHOUNDS("Hellhound", + new WorldPoint(2431, 9776, 0), + new WorldPoint(2413, 9786, 0), + new WorldPoint(2783, 9686, 0), + new WorldPoint(3198, 10071, 0)), + HILL_GIANT("Hill giant", new WorldPoint(3044, 10318, 0)), + ICE_GIANT("Ice giant", new WorldPoint(3207, 10164, 0), new WorldPoint(3339, 10056, 0)), + ICE_WARRIOR("Ice warrior", new WorldPoint(2955, 3876, 0)), + KALPHITE("Kalphite", new WorldPoint(3307, 9528, 0)), + LESSER_DEMON("Lesser demon", + new WorldPoint(2838, 9559, 0), + new WorldPoint(3163, 10114, 0), + new WorldPoint(3338, 10134, 0)), + LIZARDMEN("Lizardman", new WorldPoint(1507, 3705, 0)), + LIZARDMEN_SHAMAN("Lizardman shaman", new WorldPoint(1423, 3715, 0)), + MAGIC_AXE("Magic axe", new WorldPoint(3190, 3960, 0)), + MAMMOTH("Mammoth", new WorldPoint(3168, 3595, 0)), + MINIONS_OF_SCARABAS("Scarab", new WorldPoint(3297, 9252, 0)), + MOSS_GIANT("Moss giant", new WorldPoint(3159, 9903, 0)), + ROGUE("Rogue", new WorldPoint(3285, 3930, 0)), + SCORPION("Scorpion", new WorldPoint(3233, 10335, 0)), + SKELETON("Skeleton", new WorldPoint(3017, 3589, 0)), + SMOKE_DEVIL("Smoke devil", new WorldPoint(2398, 9444, 0)), + SPIDER("Spider", new WorldPoint(3169, 3886, 0)), + SUQAHS("Suqah", new WorldPoint(2114, 3943, 0)), + TROLLS("Troll", new WorldPoint(2401, 3856, 0), new WorldPoint(1242, 3517, 0)), + WARPED_CREATURES("Warped creature", new WorldPoint(1490, 4263, 1)), + WYRMS("Wyrm", new WorldPoint(1368, 9695, 0)), + ZOMBIE("Zombie", new WorldPoint(3172, 3677, 0)); + + private final String taskName; + private final List spots; + + CannonSpot(String taskName, WorldPoint... spots) { + this.taskName = taskName; + this.spots = Arrays.asList(spots); + } + + /** + * Gets the first (primary) cannon spot for this task + */ + public WorldPoint getPrimarySpot() { + return spots.isEmpty() ? null : spots.get(0); + } + + /** + * Finds a cannon spot for the given slayer task name + * @param taskName The slayer task name to search for + * @return The matching CannonSpot, or null if not found + */ + public static CannonSpot forTask(String taskName) { + if (taskName == null || taskName.isEmpty()) { + return null; + } + String taskLower = taskName.toLowerCase(); + for (CannonSpot spot : values()) { + if (taskLower.contains(spot.getTaskName().toLowerCase()) || + spot.getTaskName().toLowerCase().contains(taskLower)) { + return spot; + } + } + return null; + } + + /** + * Finds the closest cannon spot to the given location + * @param taskName The slayer task name + * @param playerLocation Current player location + * @return The closest WorldPoint for cannon placement, or null if no spots for this task + */ + public static WorldPoint getClosestSpot(String taskName, WorldPoint playerLocation) { + CannonSpot cannonSpot = forTask(taskName); + if (cannonSpot == null || playerLocation == null) { + return null; + } + + WorldPoint closest = null; + int closestDistance = Integer.MAX_VALUE; + + for (WorldPoint spot : cannonSpot.getSpots()) { + int distance = playerLocation.distanceTo(spot); + if (distance < closestDistance) { + closestDistance = distance; + closest = spot; + } + } + + return closest; + } + + /** + * Checks if a cannon spot exists for the given task + */ + public static boolean hasSpotForTask(String taskName) { + return forTask(taskName) != null; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/LootStyle.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/LootStyle.java new file mode 100644 index 0000000000..d9bb19a31a --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/LootStyle.java @@ -0,0 +1,23 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Looting style options for the slayer plugin. + */ +@Getter +@RequiredArgsConstructor +public enum LootStyle { + MIXED("Mixed", "Loot by both item list and GE price"), + ITEM_LIST("Item List", "Only loot items from the custom list"), + GE_PRICE_RANGE("GE Price Range", "Only loot items within price range"); + + private final String name; + private final String description; + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/PohTeleportMethod.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/PohTeleportMethod.java new file mode 100644 index 0000000000..1db33d0118 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/PohTeleportMethod.java @@ -0,0 +1,22 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PohTeleportMethod { + HOUSE_TAB("House Tab", "Teleport to house", "Break"), + SPELL("Teleport Spell", null, null), + CONSTRUCTION_CAPE("Construction Cape", "Construct. cape", "Tele to POH"), + MAX_CAPE("Max Cape", "Max cape", "Tele to POH"); + + private final String displayName; + private final String itemName; + private final String action; + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/PrayerFlickStyle.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/PrayerFlickStyle.java new file mode 100644 index 0000000000..fe332a4d6f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/PrayerFlickStyle.java @@ -0,0 +1,26 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Prayer flicking styles for slayer combat. + * Based on AIOFighter's prayer styles. + */ +@Getter +@RequiredArgsConstructor +public enum PrayerFlickStyle { + OFF("Off", "Prayer management disabled"), + ALWAYS_ON("Always On", "Prayer stays on during combat"), + LAZY_FLICK("Lazy Flick", "Flicks prayer tick before enemy hit"), + PERFECT_LAZY_FLICK("Perfect Lazy Flick", "Flicks prayer on enemy hit tick"), + MIXED_LAZY_FLICK("Mixed Lazy Flick", "Randomly flicks on hit or tick before"); + + private final String name; + private final String description; + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerCombatStyle.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerCombatStyle.java new file mode 100644 index 0000000000..021193b640 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerCombatStyle.java @@ -0,0 +1,65 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Combat styles for slayer tasks. + * Determines how the bot approaches combat for a given task. + */ +@Getter +@RequiredArgsConstructor +public enum SlayerCombatStyle { + MELEE("Melee", "Standard melee combat"), + RANGED("Ranged", "Standard ranged combat"), + MAGIC("Magic", "Standard magic combat (single target)"), + BURST("Burst", "Multi-target burst spells with goading"), + BARRAGE("Barrage", "Multi-target barrage spells with goading"); + + private final String displayName; + private final String description; + + /** + * Parses a string to a combat style. + * Supports various aliases for convenience. + */ + public static SlayerCombatStyle fromString(String style) { + if (style == null || style.isEmpty()) { + return MELEE; // Default + } + + String normalized = style.toUpperCase().trim(); + + switch (normalized) { + case "MELEE": + case "M": + return MELEE; + case "RANGED": + case "RANGE": + case "R": + return RANGED; + case "MAGIC": + case "MAGE": + return MAGIC; + case "BURST": + case "B": + case "ICE BURST": + case "ICEBURST": + return BURST; + case "BARRAGE": + case "BB": + case "ICE BARRAGE": + case "ICEBARRAGE": + return BARRAGE; + default: + return MELEE; + } + } + + /** + * Checks if this style is a multi-target AoE style (burst or barrage) + */ + public boolean isAoeStyle() { + return this == BURST || this == BARRAGE; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerConfig.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerConfig.java new file mode 100644 index 0000000000..f609e8b255 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerConfig.java @@ -0,0 +1,672 @@ +package net.runelite.client.plugins.microbot.slayer; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigInformation; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; + +@ConfigGroup("microbot-slayer") +@ConfigInformation("Automated slayer task completion with banking and travel support") +public interface SlayerConfig extends Config { + + // ===================== + // General Section + // ===================== + @ConfigSection( + name = "General", + description = "General plugin settings", + position = 0, + closedByDefault = false + ) + String generalSection = "General"; + + @ConfigItem( + keyName = "enablePlugin", + name = "Enable Plugin", + description = "Toggle the slayer plugin on/off", + position = 0, + section = generalSection + ) + default boolean enablePlugin() { + return true; + } + + @ConfigItem( + keyName = "enableAutoTravel", + name = "Auto Travel", + description = "Automatically travel to slayer task location", + position = 1, + section = generalSection + ) + default boolean enableAutoTravel() { + return true; + } + + @ConfigItem( + keyName = "slayerMaster", + name = "Slayer Master", + description = "Which slayer master to get tasks from", + position = 2, + section = generalSection + ) + default SlayerMaster slayerMaster() { + return SlayerMaster.DURADEL; + } + + @ConfigItem( + keyName = "getNewTask", + name = "Get New Task", + description = "Automatically get a new task when current task is complete", + position = 3, + section = generalSection + ) + default boolean getNewTask() { + return true; + } + + // ===================== + // Task Management Section + // ===================== + @ConfigSection( + name = "Task Management", + description = "Task skip and block settings", + position = 5, + closedByDefault = false + ) + String taskSection = "Task Management"; + + @ConfigItem( + keyName = "enableAutoSkip", + name = "Auto Skip Tasks", + description = "Automatically skip tasks on the skip list (costs 30 points)", + position = 0, + section = taskSection + ) + default boolean enableAutoSkip() { + return false; + } + + @ConfigItem( + keyName = "skipTaskList", + name = "Skip Task List", + description = "Comma-separated list of task names to skip (e.g., 'Black demons, Hellhounds, Greater demons')", + position = 1, + section = taskSection + ) + default String skipTaskList() { + return ""; + } + + @ConfigItem( + keyName = "minPointsToSkip", + name = "Min Points to Skip", + description = "Minimum slayer points required before skipping (keeps a reserve)", + position = 2, + section = taskSection + ) + default int minPointsToSkip() { + return 100; + } + + @ConfigItem( + keyName = "enableAutoBlock", + name = "Auto Block Tasks", + description = "Automatically block tasks on the block list (costs 100 points, permanent)", + position = 3, + section = taskSection + ) + default boolean enableAutoBlock() { + return false; + } + + @ConfigItem( + keyName = "blockTaskList", + name = "Block Task List", + description = "Comma-separated list of task names to permanently block (e.g., 'Spiritual creatures, Drakes')", + position = 4, + section = taskSection + ) + default String blockTaskList() { + return ""; + } + + @ConfigItem( + keyName = "minPointsToBlock", + name = "Min Points to Block", + description = "Minimum slayer points required before blocking (keeps a reserve)", + position = 5, + section = taskSection + ) + default int minPointsToBlock() { + return 150; + } + + // ===================== + // Banking Section + // ===================== + @ConfigSection( + name = "Banking", + description = "Banking and inventory settings", + position = 10, + closedByDefault = false + ) + String bankingSection = "Banking"; + + @ConfigItem( + keyName = "enableAutoBanking", + name = "Auto Banking", + description = "Automatically bank when supplies are low", + position = 0, + section = bankingSection + ) + default boolean enableAutoBanking() { + return true; + } + + @ConfigItem( + keyName = "foodThreshold", + name = "Food Threshold", + description = "Bank when food count drops below this amount (0 to disable)", + position = 1, + section = bankingSection + ) + default int foodThreshold() { + return 3; + } + + @ConfigItem( + keyName = "potionThreshold", + name = "Prayer Potion Threshold", + description = "Bank when prayer potion/super restore doses drop below this amount (0 to disable)", + position = 2, + section = bankingSection + ) + default int potionThreshold() { + return 4; + } + + // ===================== + // POH Section + // ===================== + @ConfigSection( + name = "POH (House)", + description = "Player Owned House settings for restoration", + position = 15, + closedByDefault = false + ) + String pohSection = "POH (House)"; + + @ConfigItem( + keyName = "usePohPool", + name = "Use POH Pool", + description = "Use POH rejuvenation pool to restore HP/Prayer/Run energy", + position = 0, + section = pohSection + ) + default boolean usePohPool() { + return false; + } + + @ConfigItem( + keyName = "pohTeleportMethod", + name = "Teleport Method", + description = "How to teleport to your house", + position = 1, + section = pohSection + ) + default PohTeleportMethod pohTeleportMethod() { + return PohTeleportMethod.HOUSE_TAB; + } + + @ConfigItem( + keyName = "usePohBeforeBanking", + name = "Use Before Banking", + description = "Use POH pool to restore before going to bank (saves supplies)", + position = 2, + section = pohSection + ) + default boolean usePohBeforeBanking() { + return true; + } + + @ConfigItem( + keyName = "usePohAfterTask", + name = "Use After Task", + description = "Use POH pool after completing a task (before getting new task)", + position = 3, + section = pohSection + ) + default boolean usePohAfterTask() { + return true; + } + + @ConfigItem( + keyName = "pohRestoreThreshold", + name = "Restore Below HP %", + description = "Use POH when HP drops below this percentage (0 to only restore at task end)", + position = 4, + section = pohSection + ) + default int pohRestoreThreshold() { + return 50; + } + + // ===================== + // Combat Section + // ===================== + @ConfigSection( + name = "Combat", + description = "Combat settings", + position = 20, + closedByDefault = false + ) + String combatSection = "Combat"; + + @ConfigItem( + keyName = "enableAutoCombat", + name = "Auto Combat", + description = "Automatically attack slayer task monsters", + position = 0, + section = combatSection + ) + default boolean enableAutoCombat() { + return true; + } + + @ConfigItem( + keyName = "attackRadius", + name = "Attack Radius", + description = "Maximum distance to search for monsters", + position = 1, + section = combatSection + ) + default int attackRadius() { + return 10; + } + + @ConfigItem( + keyName = "prioritizeSuperiors", + name = "Prioritize Superiors", + description = "Always attack superior slayer monsters first when they spawn", + position = 2, + section = combatSection + ) + default boolean prioritizeSuperiors() { + return true; + } + + @ConfigItem( + keyName = "eatAtHealthPercent", + name = "Eat at HP %", + description = "Eat food when health drops below this percentage", + position = 3, + section = combatSection + ) + default int eatAtHealthPercent() { + return 50; + } + + @ConfigItem( + keyName = "prayerFlickStyle", + name = "Prayer Style", + description = "How to manage prayer during combat", + position = 4, + section = combatSection + ) + default PrayerFlickStyle prayerFlickStyle() { + return PrayerFlickStyle.OFF; + } + + @ConfigItem( + keyName = "drinkPrayerAt", + name = "Drink Prayer At", + description = "Drink prayer potion when prayer points drop below this amount", + position = 5, + section = combatSection + ) + default int drinkPrayerAt() { + return 20; + } + + @ConfigItem( + keyName = "useCombatPotions", + name = "Use Combat Potions", + description = "Drink combat potions (attack, strength, defence, ranging, magic)", + position = 6, + section = combatSection + ) + default boolean useCombatPotions() { + return false; + } + + @ConfigItem( + keyName = "useOffensivePrayers", + name = "Use Offensive Prayers", + description = "Activate offensive prayers (Piety/Rigour/Augury) based on combat style", + position = 7, + section = combatSection + ) + default boolean useOffensivePrayers() { + return false; + } + + @ConfigItem( + keyName = "dodgeProjectiles", + name = "Dodge AOE Attacks", + description = "Automatically dodge AOE projectile attacks (e.g., dragon poison pools)", + position = 8, + section = combatSection + ) + default boolean dodgeProjectiles() { + return false; + } + + @ConfigItem( + keyName = "hopWhenCrashed", + name = "Hop When Crashed", + description = "Hop worlds if no attackable targets found for 20 seconds (someone else has aggro)", + position = 9, + section = combatSection + ) + default boolean hopWhenCrashed() { + return true; + } + + @ConfigItem( + keyName = "hopWorldList", + name = "Hop World List", + description = "Comma-separated list of worlds to hop to (e.g., 390,391,392). Leave empty for random members world.", + position = 10, + section = combatSection + ) + default String hopWorldList() { + return ""; + } + + // ===================== + // Cannon Section + // ===================== + @ConfigSection( + name = "Cannon", + description = "Dwarf multicannon settings", + position = 25, + closedByDefault = false + ) + String cannonSection = "Cannon"; + + @ConfigItem( + keyName = "enableCannon", + name = "Enable Cannon", + description = "Use dwarf multicannon for tasks with predefined cannon spots", + position = 0, + section = cannonSection + ) + default boolean enableCannon() { + return false; + } + + @ConfigItem( + keyName = "cannonballThreshold", + name = "Cannonball Threshold", + description = "Bank when cannonball count drops below this amount", + position = 1, + section = cannonSection + ) + default int cannonballThreshold() { + return 100; + } + + @ConfigItem( + keyName = "cannonTaskList", + name = "Cannon Tasks (Optional)", + description = "Only use cannon for these tasks (comma-separated). Leave empty to cannon all supported tasks.", + position = 2, + section = cannonSection + ) + default String cannonTaskList() { + return ""; + } + + // ===================== + // Loot Section + // ===================== + @ConfigSection( + name = "Loot", + description = "Looting settings", + position = 30, + closedByDefault = false + ) + String lootSection = "Loot"; + + @ConfigItem( + keyName = "enableLooting", + name = "Enable Looting", + description = "Pick up loot from killed monsters", + position = 0, + section = lootSection + ) + default boolean enableLooting() { + return true; + } + + @ConfigItem( + keyName = "lootStyle", + name = "Loot Style", + description = "How to determine what to loot", + position = 1, + section = lootSection + ) + default LootStyle lootStyle() { + return LootStyle.MIXED; + } + + @ConfigItem( + keyName = "lootItemList", + name = "Item List", + description = "Comma-separated list of items to always loot. Uses exact name matching. Use * for wildcards (e.g., 'totem piece, *clue scroll*, ancient shard')", + position = 2, + section = lootSection + ) + default String lootItemList() { + return ""; + } + + @ConfigItem( + keyName = "lootExcludeList", + name = "Exclude List", + description = "Comma-separated list of items to NEVER loot. Uses exact name matching. Use * for wildcards (e.g., 'vial, jug, *bones')", + position = 3, + section = lootSection + ) + default String lootExcludeList() { + return ""; + } + + @ConfigItem( + keyName = "minLootValue", + name = "Min Loot Value", + description = "Minimum GE value to loot (0 to disable price filtering)", + position = 3, + section = lootSection + ) + default int minLootValue() { + return 1000; + } + + @ConfigItem( + keyName = "maxLootValue", + name = "Max Loot Value", + description = "Maximum GE value to loot (0 for no limit)", + position = 4, + section = lootSection + ) + default int maxLootValue() { + return 0; + } + + @ConfigItem( + keyName = "lootCoins", + name = "Loot Coins", + description = "Pick up coin stacks", + position = 5, + section = lootSection + ) + default boolean lootCoins() { + return true; + } + + @ConfigItem( + keyName = "minCoinStack", + name = "Min Coin Stack", + description = "Only loot coins if stack is at least this amount (0 = loot all)", + position = 6, + section = lootSection + ) + default int minCoinStack() { + return 0; + } + + @ConfigItem( + keyName = "lootArrows", + name = "Loot Arrows", + description = "Pick up arrow stacks (10+ arrows)", + position = 7, + section = lootSection + ) + default boolean lootArrows() { + return false; + } + + @ConfigItem( + keyName = "lootRunes", + name = "Loot Runes", + description = "Pick up rune stacks (2+ runes)", + position = 8, + section = lootSection + ) + default boolean lootRunes() { + return false; + } + + @ConfigItem( + keyName = "lootUntradables", + name = "Loot Untradables", + description = "Pick up untradable items (clue scrolls, keys, etc.)", + position = 9, + section = lootSection + ) + default boolean lootUntradables() { + return true; + } + + @ConfigItem( + keyName = "lootBones", + name = "Loot Bones", + description = "Pick up bones from killed monsters", + position = 10, + section = lootSection + ) + default boolean lootBones() { + return false; + } + + @ConfigItem( + keyName = "buryBones", + name = "Bury Bones", + description = "Automatically bury bones after picking them up", + position = 11, + section = lootSection + ) + default boolean buryBones() { + return false; + } + + @ConfigItem( + keyName = "scatterAshes", + name = "Scatter Ashes", + description = "Pick up and scatter demonic/infernal ashes", + position = 12, + section = lootSection + ) + default boolean scatterAshes() { + return false; + } + + @ConfigItem( + keyName = "forceLoot", + name = "Force Loot", + description = "Loot items even while in combat", + position = 13, + section = lootSection + ) + default boolean forceLoot() { + return false; + } + + @ConfigItem( + keyName = "onlyLootMyItems", + name = "Only Loot My Items", + description = "Only loot items dropped by/for you", + position = 14, + section = lootSection + ) + default boolean onlyLootMyItems() { + return false; + } + + @ConfigItem( + keyName = "delayedLooting", + name = "Delayed Looting", + description = "Wait before looting (lets items pile up)", + position = 15, + section = lootSection + ) + default boolean delayedLooting() { + return false; + } + + @ConfigItem( + keyName = "eatForLootSpace", + name = "Eat For Loot Space", + description = "Eat food to make room for valuable loot", + position = 15, + section = lootSection + ) + default boolean eatForLootSpace() { + return false; + } + + @ConfigItem( + keyName = "enableHighAlch", + name = "Enable High Alch", + description = "High alch items from your inventory while fighting", + position = 20, + section = lootSection + ) + default boolean enableHighAlch() { + return false; + } + + @ConfigItem( + keyName = "highAlchItemList", + name = "High Alch Items", + description = "Comma-separated list of items to high alch. Uses exact name matching. Use * for wildcards (e.g., 'Rune platelegs, Rune full helm, *dragon*')", + position = 21, + section = lootSection + ) + default String highAlchItemList() { + return ""; + } + + @ConfigItem( + keyName = "highAlchExcludeList", + name = "High Alch Exclude", + description = "Comma-separated list of items to NEVER high alch. Uses exact name matching. Use * for wildcards (e.g., 'Dragon scimitar, *shield*')", + position = 22, + section = lootSection + ) + default String highAlchExcludeList() { + return ""; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerLocation.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerLocation.java new file mode 100644 index 0000000000..95e066e058 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerLocation.java @@ -0,0 +1,183 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +import java.util.Arrays; + +/** + * Registry of common slayer task locations with their WorldPoints. + * Used to translate profile location names to actual coordinates. + */ +@Getter +@RequiredArgsConstructor +public enum SlayerLocation { + // Catacombs of Kourend - great for multi-combat bursting + CATACOMBS("catacombs", "Catacombs of Kourend", new WorldPoint(1666, 10049, 0)), + CATACOMBS_DUST_DEVILS("catacombs dust devils", "Catacombs - Dust Devils", new WorldPoint(1716,10032,0)), + CATACOMBS_NECHRYAEL("catacombs nechryael", "Catacombs - Nechryael", new WorldPoint(1697, 10077, 0)), + CATACOMBS_ABYSSAL_DEMONS("catacombs abyssal demons", "Catacombs - Abyssal Demons", new WorldPoint(1678, 10086, 0)), + CATACOMBS_HELLHOUNDS("catacombs hellhounds", "Catacombs - Hellhounds", new WorldPoint(1642, 10054, 0)), + CATACOMBS_BLACK_DEMONS("catacombs black demons", "Catacombs - Black Demons", new WorldPoint(1720, 10055, 0)), + CATACOMBS_GREATER_DEMONS("catacombs greater demons", "Catacombs - Greater Demons", new WorldPoint(1685,10086,0)), + CATACOMBS_FIRE_GIANTS("catacombs fire giants", "Catacombs - Fire Giants", new WorldPoint(1629,10051,0)), + CATACOMBS_ANKOU("catacombs ankou", "Catacombs - Ankou", new WorldPoint(1650, 10084, 0)), + CATACOMBS_DAGANNOTH("catacombs dagannoth", "Catacombs - Dagannoth", new WorldPoint(1674,9997,0)), + CATACOMBS_MUTATED_BLOODVELDS("catacombs mutated bloodvelds", "Catacombs - Mutated Bloodvelds", new WorldPoint(1691, 10015, 0)), + CATACOMBS_BRUTAL_BLUE_DRAGONS("catacombs brutal blue dragons", "Catacombs - Brutal Blue Dragons", new WorldPoint(1634, 10075, 0)), + + // Slayer Tower + SLAYER_TOWER("slayer tower", "Slayer Tower", new WorldPoint(3429, 3534, 0)), + SLAYER_TOWER_BASEMENT("slayer tower basement", "Slayer Tower Basement", new WorldPoint(3417, 9932, 0)), + SLAYER_TOWER_GARGOYLES("slayer tower gargoyles", "Slayer Tower - Gargoyles", new WorldPoint(3442, 3543, 2)), + SLAYER_TOWER_NECHRYAEL("slayer tower nechryael", "Slayer Tower - Nechryael", new WorldPoint(3438, 3558, 2)), + SLAYER_TOWER_ABYSSAL_DEMONS("slayer tower abyssal demons", "Slayer Tower - Abyssal Demons", new WorldPoint(3420, 3568, 2)), + SLAYER_TOWER_BLOODVELDS("slayer tower bloodvelds", "Slayer Tower - Bloodvelds", new WorldPoint(3418, 3558, 1)), + SLAYER_TOWER_ABERRANT_SPECTRES("slayer tower aberrant spectres", "Slayer Tower - Aberrant Spectres", new WorldPoint(3438, 3549, 1)), + SLAYER_TOWER_CRAWLING_HANDS("slayer tower crawling hands", "Slayer Tower - Crawling Hands", new WorldPoint(3418, 3547, 0)), + SLAYER_TOWER_INFERNAL_MAGES("slayer tower infernal mages", "Slayer Tower - Infernal Mages", new WorldPoint(3442, 3550, 1)), + + // Stronghold Slayer Cave (Nieve's Cave) + STRONGHOLD_CAVE("stronghold cave", "Stronghold Slayer Cave", new WorldPoint(2431, 9806, 0)), + STRONGHOLD_CAVE_BLOODVELDS("stronghold bloodvelds", "Stronghold - Bloodvelds", new WorldPoint(2426, 9820, 0)), + STRONGHOLD_CAVE_HELLHOUNDS("stronghold hellhounds", "Stronghold - Hellhounds", new WorldPoint(2413, 9785, 0)), + STRONGHOLD_CAVE_FIRE_GIANTS("stronghold fire giants", "Stronghold - Fire Giants", new WorldPoint(2392, 9782, 0)), + STRONGHOLD_CAVE_ABERRANT_SPECTRES("stronghold aberrant spectres", "Stronghold - Aberrant Spectres", new WorldPoint(2453, 9793, 0)), + STRONGHOLD_CAVE_ABYSSAL_DEMONS("stronghold abyssal demons", "Stronghold - Abyssal Demons", new WorldPoint(2465, 9772, 0)), + STRONGHOLD_CAVE_GREATER_DEMONS("stronghold greater demons", "Stronghold - Greater Demons", new WorldPoint(2467, 9805, 0)), + STRONGHOLD_CAVE_BLACK_DEMONS("stronghold black demons", "Stronghold - Black Demons", new WorldPoint(2466, 9823, 0)), + STRONGHOLD_CAVE_DAGANNOTH("stronghold dagannoth", "Stronghold - Dagannoth", new WorldPoint(2446, 9829, 0)), + STRONGHOLD_CAVE_ANKOU("stronghold ankou", "Stronghold - Ankou", new WorldPoint(2393, 9806, 0)), + + // Karuulm Slayer Dungeon (Mount Karuulm) + KARUULM("karuulm", "Karuulm Slayer Dungeon", new WorldPoint(1310, 10188, 0)), + KARUULM_HYDRAS("karuulm hydras", "Karuulm - Hydras", new WorldPoint(1312, 10232, 0)), + KARUULM_DRAKES("karuulm drakes", "Karuulm - Drakes", new WorldPoint(1298, 10188, 0)), + KARUULM_WYRMS("karuulm wyrms", "Karuulm - Wyrms", new WorldPoint(1282, 10175, 0)), + KARUULM_SULPHUR_LIZARDS("karuulm sulphur lizards", "Karuulm - Sulphur Lizards", new WorldPoint(1300, 10158, 0)), + + // Smoke Dungeon + SMOKE_DUNGEON("smoke dungeon", "Smoke Dungeon", new WorldPoint(3206, 9379, 0)), + SMOKE_DUNGEON_DUST_DEVILS("smoke dungeon dust devils", "Smoke Dungeon - Dust Devils", new WorldPoint(3218, 9370, 0)), + SMOKE_DEVIL_DUNGEON("smoke devil dungeon", "Smoke Devil Dungeon", new WorldPoint(2398, 9444, 0)), + + // Chasm of Fire + CHASM_OF_FIRE("chasm of fire", "Chasm of Fire", new WorldPoint(1435, 10076, 0)), + CHASM_OF_FIRE_GREATER_DEMONS("chasm greater demons", "Chasm - Greater Demons", new WorldPoint(1451, 10083, 0)), + CHASM_OF_FIRE_BLACK_DEMONS("chasm black demons", "Chasm - Black Demons", new WorldPoint(1419, 10072, 0)), + CHASM_OF_FIRE_LESSER_DEMONS("chasm lesser demons", "Chasm - Lesser Demons", new WorldPoint(1419, 10095, 0)), + + // Dragons + LITHKREN_VAULT("lithkren vault", "Lithkren Vault", new WorldPoint(1556, 5074, 0)), + BRIMHAVEN_DUNGEON("brimhaven dungeon", "Brimhaven Dungeon", new WorldPoint(2713, 9564, 0)), + BRIMHAVEN_BLACK_DRAGONS("brimhaven black dragons", "Brimhaven - Black Dragons", new WorldPoint(2717, 9477, 0)), + BRIMHAVEN_RED_DRAGONS("brimhaven red dragons", "Brimhaven - Red Dragons", new WorldPoint(2683, 9523, 0)), + BRIMHAVEN_METAL_DRAGONS("brimhaven metal dragons", "Brimhaven - Metal Dragons", new WorldPoint(2690, 9466, 0)), + BRIMHAVEN_STEEL_DRAGONS("brimhaven steel dragons", "Brimhaven - Steel Dragons", new WorldPoint(2661, 9423, 0)), + TAVERLEY_DUNGEON("taverley dungeon", "Taverley Dungeon", new WorldPoint(2884, 9798, 0)), + TAVERLEY_BLACK_DRAGONS("taverley black dragons", "Taverley - Black Dragons", new WorldPoint(2861, 9822, 0)), + TAVERLEY_BABY_BLACK_DRAGONS("taverley baby black dragons", "Taverley - Baby Black Dragons (Slayer)", new WorldPoint(2820, 9819, 1)), + TAVERLEY_BABY_BLUE_DRAGONS("taverley baby blue dragons", "Taverley - Baby Blue Dragons", new WorldPoint(2916, 9804, 0)), + TAVERLEY_BLUE_DRAGONS("taverley blue dragons", "Taverley - Blue Dragons", new WorldPoint(2901, 9799, 0)), + TAVERLEY_HELLHOUNDS("taverley hellhounds", "Taverley - Hellhounds", new WorldPoint(2874, 9847, 0)), + ANCIENT_CAVERN("ancient cavern", "Ancient Cavern", new WorldPoint(1768, 5366, 1)), + ANCIENT_CAVERN_MITHRIL_DRAGONS("ancient cavern mithril dragons", "Ancient Cavern - Mithril Dragons", new WorldPoint(1743, 5329, 0)), + + // Fremennik Slayer Dungeon + FREMENNIK_CAVE("fremennik cave", "Fremennik Slayer Dungeon", new WorldPoint(2794, 10018, 0)), + FREMENNIK_CAVE_KURASK("fremennik kurask", "Fremennik - Kurask", new WorldPoint(2702, 10000, 0)), + FREMENNIK_CAVE_TUROTH("fremennik turoth", "Fremennik - Turoth", new WorldPoint(2717, 10016, 0)), + FREMENNIK_CAVE_COCKATRICE("fremennik cockatrice", "Fremennik - Cockatrice", new WorldPoint(2792, 10034, 0)), + FREMENNIK_CAVE_BASILISK("fremennik basilisk", "Fremennik - Basilisk", new WorldPoint(2740, 10008, 0)), + FREMENNIK_CAVE_JELLY("fremennik jelly", "Fremennik - Jelly", new WorldPoint(2704, 10030, 0)), + + // Kalphite areas + KALPHITE_LAIR("kalphite lair", "Kalphite Lair", new WorldPoint(3307,9528,0)), + KALPHITE_CAVE("kalphite cave", "Kalphite Cave", new WorldPoint(3315, 9499, 0)), + + // Fossil Island + FOSSIL_ISLAND_WYVERNS("fossil island wyverns", "Fossil Island - Wyverns", new WorldPoint(3609, 10278, 0)), + + // Wilderness + WILDERNESS_SLAYER_CAVE("wilderness slayer cave", "Wilderness Slayer Cave", new WorldPoint(3421, 10116, 0)), + + // Misc locations + KELDAGRIM("keldagrim", "Keldagrim", new WorldPoint(2866, 10180, 0)), + DEATH_PLATEAU("death plateau", "Death Plateau", new WorldPoint(2861, 3592, 0)), + DEATH_PLATEAU_TROLLS("death plateau trolls", "Death Plateau - Trolls", new WorldPoint(2870, 3593, 0)), + MORYTANIA_SLAYER_TOWER("morytania", "Morytania Slayer Tower", new WorldPoint(3429, 3534, 0)), + APE_ATOLL("ape atoll", "Ape Atoll", new WorldPoint(2759, 2788, 0)), + ZANARIS("zanaris", "Zanaris", new WorldPoint(2389, 4435, 0)), + KRAKEN_COVE("kraken cove", "Kraken Cove", new WorldPoint(2280, 10022, 0)), + LIGHTHOUSE("lighthouse", "Lighthouse", new WorldPoint(2515, 3619, 0)), + LIGHTHOUSE_DAGANNOTHS("lighthouse dagannoths", "Lighthouse - Dagannoths", new WorldPoint(2519, 10020, 0)), + WATERBIRTH_ISLAND("waterbirth island", "Waterbirth Island", new WorldPoint(2521, 3740, 0)), + ISLE_OF_STONE("isle of stone", "Isle of Stone", new WorldPoint(2464, 3993, 0)), + ISLE_OF_STONE_DAGANNOTHS("isle of stone dagannoths", "Isle of Stone - Dagannoths", new WorldPoint(2464, 10398, 0)), + DAGANNOTH_KINGS("dagannoth kings", "Dagannoth Kings Lair", new WorldPoint(2899, 4449, 0)), + CERBERUS_LAIR("cerberus lair", "Cerberus Lair", new WorldPoint(1310, 1251, 0)), + THERMONUCLEAR_SMOKE_DEVIL("thermonuclear", "Thermonuclear Smoke Devil", new WorldPoint(2378, 9452, 0)), + ALCHEMICAL_HYDRA("alchemical hydra", "Alchemical Hydra Lair", new WorldPoint(1364, 10265, 0)), + + // Lunar Isle + LUNAR_ISLE("lunar isle", "Lunar Isle", new WorldPoint(2100, 3914, 0)), + LUNAR_ISLE_SUQAHS("lunar isle suqahs", "Lunar Isle - Suqahs", new WorldPoint(2114, 3943, 0)), + + // Mos Le'Harmless + MOS_LEHARMLESS("mos leharmless", "Mos Le'Harmless", new WorldPoint(3680, 2970, 0)), + MOS_LEHARMLESS_CAVE_HORRORS("mos leharmless cave horrors", "Mos Le'Harmless - Cave Horrors", new WorldPoint(3740, 9373, 0)), + + // Mourner Tunnels / Dark beasts + MOURNER_TUNNELS("mourner tunnels", "Mourner Tunnels", new WorldPoint(1991, 4638, 0)), + MOURNER_TUNNELS_DARK_BEASTS("mourner tunnels dark beasts", "Mourner Tunnels - Dark Beasts", new WorldPoint(1992, 4655, 0)), + + // Prifddinas / Elven areas + PRIFDDINAS("prifddinas", "Prifddinas", new WorldPoint(3239, 6075, 0)), + LLETYA("lletya", "Lletya", new WorldPoint(2341, 3171, 0)), + LLETYA_ELVES("lletya elves", "Lletya - Elves", new WorldPoint(2323, 3155, 0)), + + // Araxyte Cave + ARAXYTE_CAVE("araxyte cave", "Araxyte Cave", new WorldPoint(3715, 5765, 0)), + + // Asgarnia Ice Dungeon + ASGARNIA_ICE_DUNGEON("asgarnia ice dungeon", "Asgarnia Ice Dungeon", new WorldPoint(3007, 9550, 0)), + ASGARNIA_ICE_DUNGEON_WYVERNS("asgarnia ice dungeon wyverns", "Asgarnia - Skeletal Wyverns", new WorldPoint(3028, 9549, 0)), + + // TzHaar City + TZHAAR_CITY("tzhaar city", "TzHaar City", new WorldPoint(2444, 5170, 0)), + + // Waterfall Dungeon + WATERFALL_DUNGEON("waterfall dungeon", "Waterfall Dungeon", new WorldPoint(2575, 9861, 0)), + WATERFALL_DUNGEON_FIRE_GIANTS("waterfall fire giants", "Waterfall - Fire Giants", new WorldPoint(2569, 9888, 0)), + + // God Wars Dungeon + GOD_WARS_DUNGEON("god wars dungeon", "God Wars Dungeon", new WorldPoint(2871, 5318, 2)), + GOD_WARS_AVIANSIES("god wars aviansies", "God Wars - Aviansies", new WorldPoint(2871, 5270, 2)), + GOD_WARS_SPIRITUAL_CREATURES("god wars spiritual creatures", "God Wars - Spiritual Creatures", new WorldPoint(2885, 5310, 2)), + + // Grimstone (Sailing) - Fairy Ring DLP + GRIMSTONE_DUNGEON("grimstone dungeon", "Grimstone Dungeon", new WorldPoint(2926, 10455, 0)), + GRIMSTONE_FROST_DRAGONS("grimstone frost dragons", "Grimstone - Frost Dragons", new WorldPoint(2902, 10457, 0)), + GRIMSTONE_FROST_DRAGONS_TASK("grimstone frost dragons task", "Grimstone - Frost Dragons (Task Only)", new WorldPoint(2902, 10457, 0)); + + private final String key; + private final String displayName; + private final WorldPoint worldPoint; + + /** + * Find a SlayerLocation by its key (case-insensitive). + * @param name The location name from the profile + * @return The matching SlayerLocation, or null if not found + */ + public static SlayerLocation fromName(String name) { + if (name == null || name.isEmpty()) { + return null; + } + String normalized = name.toLowerCase().trim(); + return Arrays.stream(values()) + .filter(loc -> loc.key.equals(normalized) || loc.displayName.equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerMaster.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerMaster.java new file mode 100644 index 0000000000..d590318145 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerMaster.java @@ -0,0 +1,29 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +@Getter +@RequiredArgsConstructor +public enum SlayerMaster { + TURAEL("Turael", new WorldPoint(2931, 3536, 0), 1), + SPRIA("Spria", new WorldPoint(2907, 3324, 0), 1), + MAZCHNA("Mazchna", new WorldPoint(3510, 3507, 0), 20), + VANNAKA("Vannaka", new WorldPoint(3145, 9914, 0), 40), + CHAELDAR("Chaeldar", new WorldPoint(2445, 4431, 0), 70), + NIEVE("Nieve", new WorldPoint(2432, 3423, 0), 85), + STEVE("Steve", new WorldPoint(2432, 3423, 0), 85), + DURADEL("Duradel", new WorldPoint(2869, 2982, 1), 100), + KURADAL("Kuradal", new WorldPoint(2869, 2982, 1), 100); // Replaces Duradel after While Guthix Sleeps + // KONAR not included due to location-specific task complexity + + private final String name; + private final WorldPoint location; + private final int combatLevelRequired; + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerOverlay.java new file mode 100644 index 0000000000..93072b0a61 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerOverlay.java @@ -0,0 +1,100 @@ +package net.runelite.client.plugins.microbot.slayer; + +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; + +import javax.inject.Inject; +import java.awt.*; + +public class SlayerOverlay extends OverlayPanel { + + @Inject + SlayerOverlay(SlayerPlugin plugin) { + super(plugin); + setPosition(OverlayPosition.TOP_LEFT); + setNaughty(); + } + + @Override + public Dimension render(Graphics2D graphics) { + try { + panelComponent.setPreferredSize(new Dimension(200, 150)); + panelComponent.getChildren().add(TitleComponent.builder() + .text("Micro Slayer V" + SlayerPlugin.version) + .color(Color.GREEN) + .build()); + + panelComponent.getChildren().add(LineComponent.builder().build()); + + // Show slayer points + panelComponent.getChildren().add(LineComponent.builder() + .left("Slayer Points:") + .right(String.valueOf(SlayerPlugin.getSlayerPoints())) + .rightColor(Color.YELLOW) + .build()); + + // Show current state + panelComponent.getChildren().add(LineComponent.builder() + .left("State:") + .right(SlayerPlugin.getState().getDisplayName()) + .rightColor(getStateColor(SlayerPlugin.getState())) + .build()); + + if (SlayerPlugin.isHasTask()) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Task:") + .right(SlayerPlugin.getCurrentTask()) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Remaining:") + .right(String.valueOf(SlayerPlugin.getTaskRemaining())) + .build()); + + // Show location if set + String location = SlayerPlugin.getCurrentLocation(); + if (location != null && !location.isEmpty()) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Location:") + .right(location) + .build()); + } + } else { + panelComponent.getChildren().add(LineComponent.builder() + .left("No active task") + .build()); + } + + } catch (Exception ex) { + // Overlay render errors are non-critical, silently ignore + } + return super.render(graphics); + } + + private Color getStateColor(SlayerState state) { + switch (state) { + case IDLE: + return Color.GRAY; + case GETTING_TASK: + return Color.MAGENTA; + case SKIPPING_TASK: + return Color.PINK; + case RESTORING_AT_POH: + return new Color(0, 191, 255); // Deep sky blue + case DETECTING_TASK: + return Color.YELLOW; + case BANKING: + return Color.ORANGE; + case TRAVELING: + return Color.CYAN; + case AT_LOCATION: + return Color.GREEN; + case FIGHTING: + return Color.RED; + default: + return Color.WHITE; + } + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java new file mode 100644 index 0000000000..aa70b0cb5e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java @@ -0,0 +1,237 @@ +package net.runelite.client.plugins.microbot.slayer; + +import com.google.inject.Provides; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Hitsplat; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.Projectile; +import net.runelite.api.events.ActorDeath; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.ProjectileMoved; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.PluginConstants; +import net.runelite.client.plugins.microbot.slayer.combat.SlayerDodgeScript; +import net.runelite.client.plugins.microbot.slayer.combat.SlayerFlickerScript; +import net.runelite.client.plugins.microbot.slayer.combat.SlayerHighAlchScript; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.prayer.Rs2Prayer; +import net.runelite.client.ui.overlay.OverlayManager; + +import javax.inject.Inject; +import java.awt.*; + +@PluginDescriptor( + name = PluginConstants.BIGL + "Slayer", + description = "Automated slayer with banking and travel support", + tags = {"slayer", "combat", "task"}, + authors = {"Big L"}, + version = SlayerPlugin.version, + minClientVersion = "2.1.11", + enabledByDefault = PluginConstants.DEFAULT_ENABLED, + isExternal = PluginConstants.IS_EXTERNAL +) +@Slf4j +public class SlayerPlugin extends Plugin { + + static final String version = "1.0.0"; + + @Inject + private SlayerConfig config; + + @Inject + private SlayerScript slayerScript; + + @Inject + private SlayerOverlay slayerOverlay; + + @Inject + private OverlayManager overlayManager; + + @Getter + private static SlayerFlickerScript flickerScript = new SlayerFlickerScript(); + + @Getter + private static SlayerDodgeScript dodgeScript = new SlayerDodgeScript(); + + @Getter + private static SlayerHighAlchScript highAlchScript = new SlayerHighAlchScript(); + + @Getter + private static String currentTask = ""; + + @Getter + private static int taskRemaining = 0; + + @Getter + private static boolean hasTask = false; + + @Getter + @Setter + private static SlayerState state = SlayerState.IDLE; + + @Getter + @Setter + private static String currentLocation = ""; + + @Getter + @Setter + private static int slayerPoints = 0; + + @Provides + SlayerConfig provideConfig(ConfigManager configManager) { + return configManager.getConfig(SlayerConfig.class); + } + + @Override + protected void startUp() throws AWTException { + log.info("Slayer plugin started"); + if (overlayManager != null) { + overlayManager.add(slayerOverlay); + } + flickerScript.start(config); + dodgeScript.setEnabled(config.dodgeProjectiles()); + dodgeScript.run(); + highAlchScript.run(config); + slayerScript.run(config); + } + + @Override + protected void shutDown() { + log.info("Slayer plugin stopped"); + slayerScript.shutdown(); + flickerScript.stop(); + dodgeScript.shutdown(); + highAlchScript.shutdown(); + if (overlayManager != null) { + overlayManager.remove(slayerOverlay); + } + resetTaskInfo(); + } + + @Subscribe + public void onGameTick(GameTick gameTick) { + try { + // Update dodge script enabled state from config + dodgeScript.setEnabled(config.dodgeProjectiles()); + + PrayerFlickStyle style = config.prayerFlickStyle(); + if (style == PrayerFlickStyle.LAZY_FLICK || + style == PrayerFlickStyle.PERFECT_LAZY_FLICK || + style == PrayerFlickStyle.MIXED_LAZY_FLICK) { + flickerScript.onGameTick(); + } + } catch (Exception e) { + log.error("Slayer Plugin onGameTick Error: " + e.getMessage()); + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) { + try { + PrayerFlickStyle style = config.prayerFlickStyle(); + if (style == PrayerFlickStyle.LAZY_FLICK || + style == PrayerFlickStyle.PERFECT_LAZY_FLICK || + style == PrayerFlickStyle.MIXED_LAZY_FLICK) { + flickerScript.onNpcDespawned(npcDespawned); + } + } catch (Exception e) { + log.error("Slayer Plugin onNpcDespawned Error: " + e.getMessage()); + } + } + + @Subscribe + public void onHitsplatApplied(HitsplatApplied event) { + try { + if (event.getActor() != Microbot.getClient().getLocalPlayer()) return; + + final Hitsplat hitsplat = event.getHitsplat(); + PrayerFlickStyle style = config.prayerFlickStyle(); + + if (hitsplat.isMine() && event.getActor().getInteracting() instanceof NPC && + (style == PrayerFlickStyle.LAZY_FLICK || + style == PrayerFlickStyle.PERFECT_LAZY_FLICK || + style == PrayerFlickStyle.MIXED_LAZY_FLICK)) { + flickerScript.onPlayerHit(); + } + } catch (Exception e) { + log.error("Slayer Plugin onHitsplatApplied Error: " + e.getMessage()); + } + } + + @Subscribe + public void onProjectileMoved(ProjectileMoved event) { + try { + if (!config.dodgeProjectiles()) return; + + Projectile projectile = event.getProjectile(); + // Only track projectiles targeting a WorldPoint (AOE attacks), not those targeting actors + if (projectile.getTargetActor() == null) { + dodgeScript.addProjectile(projectile); + } + } catch (Exception e) { + log.error("Slayer Plugin onProjectileMoved Error: " + e.getMessage()); + } + } + + @Subscribe + public void onActorDeath(ActorDeath event) { + try { + // Only handle local player death + if (!(event.getActor() instanceof Player)) { + return; + } + + Player player = (Player) event.getActor(); + if (player != Microbot.getClient().getLocalPlayer()) { + return; + } + + // Player died - log out immediately to pause gravestone timer + log.warn("Player died! Logging out to pause gravestone timer."); + + // Disable prayers immediately + Rs2Prayer.disableAllPrayers(); + + // Reset state + state = SlayerState.IDLE; + + // Stop all scripts first so they don't interfere with logout + slayerScript.shutdown(); + flickerScript.stop(); + dodgeScript.shutdown(); + highAlchScript.shutdown(); + + // Log out to pause the gravestone timer + Rs2Player.logout(); + + // Show message after logout attempt + Microbot.showMessage("You died! Logged out to pause gravestone timer. Recover your items manually."); + log.info("Plugin stopped and logged out due to player death."); + } catch (Exception e) { + log.error("Slayer Plugin onActorDeath Error: " + e.getMessage()); + } + } + + public static void updateTaskInfo(boolean hasSlayerTask, String taskName, int remaining) { + hasTask = hasSlayerTask; + currentTask = taskName != null ? taskName : ""; + taskRemaining = remaining; + } + + private static void resetTaskInfo() { + hasTask = false; + currentTask = ""; + taskRemaining = 0; + state = SlayerState.IDLE; + currentLocation = ""; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPrayer.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPrayer.java new file mode 100644 index 0000000000..b359258950 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPrayer.java @@ -0,0 +1,83 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.client.plugins.microbot.util.prayer.Rs2PrayerEnum; + +/** + * Prayer options for slayer task profiles. + * Defines which protection prayer (if any) to use during combat. + */ +@Getter +@RequiredArgsConstructor +public enum SlayerPrayer { + NONE("None", null), + PROTECT_MELEE("Protect from Melee", Rs2PrayerEnum.PROTECT_MELEE), + PROTECT_MAGIC("Protect from Magic", Rs2PrayerEnum.PROTECT_MAGIC), + PROTECT_RANGED("Protect from Missiles", Rs2PrayerEnum.PROTECT_RANGE); + + private final String displayName; + private final Rs2PrayerEnum prayer; + + /** + * Parses a prayer from a string (case-insensitive, supports multiple formats) + */ + public static SlayerPrayer fromString(String str) { + if (str == null || str.isEmpty()) { + return NONE; + } + + String normalized = str.toUpperCase().trim() + .replace(" ", "_") + .replace("PROTECT_FROM_", "PROTECT_") + .replace("MISSILES", "RANGED"); + + // Try exact match first + for (SlayerPrayer prayer : values()) { + if (prayer.name().equalsIgnoreCase(normalized)) { + return prayer; + } + } + + // Try partial matches + for (SlayerPrayer prayer : values()) { + if (prayer.name().contains(normalized) || normalized.contains(prayer.name())) { + return prayer; + } + if (prayer.displayName.toUpperCase().contains(str.toUpperCase())) { + return prayer; + } + } + + // Common aliases - short forms for quick config entry + switch (normalized) { + case "MELEE": + case "PROT_MELEE": + case "PMELEE": + case "PM": + return PROTECT_MELEE; + case "MAGIC": + case "MAGE": + case "PROT_MAGIC": + case "PROT_MAGE": + case "PMAGIC": + case "PMAGE": + return PROTECT_MAGIC; + case "RANGED": + case "RANGE": + case "PROT_RANGED": + case "PROT_RANGE": + case "PRANGED": + case "PRANGE": + case "PR": + return PROTECT_RANGED; + default: + return NONE; + } + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java new file mode 100644 index 0000000000..8a6354e58d --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java @@ -0,0 +1,4066 @@ +package net.runelite.client.plugins.microbot.slayer; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; +import net.runelite.api.GameObject; +import net.runelite.api.GameState; +import net.runelite.api.TileObject; +import net.runelite.api.Skill; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; +import net.runelite.client.plugins.microbot.inventorysetups.MInventorySetupsPlugin; +import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.combat.Rs2Combat; +import net.runelite.client.plugins.microbot.util.dialogues.Rs2Dialogue; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2Cannon; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.grounditem.LootingParameters; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2LootEngine; +import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; +import net.runelite.client.plugins.grounditems.GroundItem; +import net.runelite.client.plugins.microbot.util.magic.Rs2CombatSpells; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.npc.MonsterLocation; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcManager; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.prayer.Rs2Prayer; +import net.runelite.client.plugins.microbot.util.prayer.Rs2PrayerEnum; +import net.runelite.client.plugins.microbot.util.skills.slayer.Rs2Slayer; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; +import net.runelite.client.plugins.microbot.util.security.Login; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import net.runelite.client.plugins.microbot.slayer.profile.SlayerProfileManager; +import net.runelite.client.plugins.microbot.slayer.profile.SlayerTaskProfileJson; + +import javax.inject.Inject; +import java.awt.event.KeyEvent; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Slf4j +public class SlayerScript extends Script { + + private SlayerConfig config; + private WorldPoint taskDestination = null; + private String taskLocationName = ""; + private boolean initialSetupDone = false; + private long lastLootTime = 0; + private static final int ARRIVAL_DISTANCE = 10; + private static final int LOOT_DELAY_MS = 600; + private static final int SLAYER_REWARD_POINTS_VARBIT = 4068; + private static final int SKIP_TASK_COST = 30; + private static final int MAX_SKIP_ATTEMPTS = 10; + private int skipAttemptCounter = 0; + private String taskBeingSkipped = ""; + + // Block task state tracking + private static final int BLOCK_TASK_COST = 100; + private static final int MAX_BLOCK_ATTEMPTS = 10; + private int blockAttemptCounter = 0; + private String taskBeingBlocked = ""; + + // POH Constants + private static final int HOUSE_PORTAL_ID = 4525; + private static final int[] REJUVENATION_POOL_IDS = {29241, 29240, 29239, 29238, 29237}; + // Portal nexus IDs (Basic, Marble, Gilded) + private static final int[] PORTAL_NEXUS_IDS = {33366, 33408, 33410}; + // Mounted glory IDs (various tiers) + private static final int[] MOUNTED_GLORY_IDS = {13523, 13524, 13525, 13526}; + // Mounted ring of wealth + private static final int MOUNTED_WEALTH_ID = 29156; + + // POH state tracking + private SlayerState stateAfterPoh = SlayerState.BANKING; // What to do after POH restoration + private boolean pohTeleportAttempted = false; + + // Spellbook swap retry tracking + private int spellbookSwapAttempts = 0; + private static final int MAX_SPELLBOOK_SWAP_ATTEMPTS = 5; + + // Break handler resume tracking + private boolean wasLoggedOut = false; + + // Cannon state tracking + private boolean isUsingCannon = false; + private boolean cannonPlaced = false; + private boolean cannonFired = false; + private WorldPoint cannonSpot = null; + + // Crash detection / world hop tracking + private long crashDetectionStartTime = 0; + private long lastWorldHopTime = 0; + private boolean isSearchingForTargets = false; + private static final int CRASH_HOP_DELAY_MS = 20000; // 20 seconds before hopping + private static final int WORLD_HOP_COOLDOWN_MS = 60000; // 1 minute cooldown between hops + + // Task completion loot delay + private long taskCompletedTime = 0; + private boolean taskCompletedLooting = false; + private static final int TASK_COMPLETE_LOOT_DELAY_MS = 5000; // 5 seconds to loot after task complete + + // JSON profile tracking + private SlayerProfileManager profileManager = new SlayerProfileManager(); + private SlayerTaskProfileJson activeJsonProfile = null; + + // All superior slayer monster names (requires "Bigger and Badder" unlock) + private static final Set SUPERIOR_MONSTERS = new HashSet<>(Arrays.asList( + // Regular superiors + "Crushing hand", // Crawling hand + "Chasm Crawler", // Cave crawler + "Screaming banshee", // Banshee + "Screaming twisted banshee", // Twisted banshee + "Giant rockslug", // Rockslug + "Cockathrice", // Cockatrice + "Flaming pyrelord", // Pyrefiend + "Monstrous basilisk", // Basilisk + "Malevolent Mage", // Infernal Mage + "Insatiable Bloodveld", // Bloodveld + "Insatiable mutated Bloodveld", // Mutated Bloodveld + "Vitreous Jelly", // Jelly + "Vitreous warped Jelly", // Warped Jelly + "Cave abomination", // Cave horror + "Abhorrent spectre", // Aberrant spectre + "Repugnant spectre", // Deviant spectre + "Choke devil", // Dust devil + "King kurask", // Kurask + "Marble gargoyle", // Gargoyle + "Nechryarch", // Nechryael + "Greater abyssal demon", // Abyssal demon + "Night beast", // Dark beast + "Nuclear smoke devil", // Smoke devil + // Kebos/newer superiors + "Shadow Wyrm", // Wyrm + "Guardian Drake", // Drake + "Colossal Hydra", // Hydra + "Basilisk Sentinel" // Basilisk Knight + )); + + @Inject + public SlayerScript() { + } + + public boolean run(SlayerConfig config) { + this.config = config; + SlayerPlugin.setState(SlayerState.IDLE); + initialSetupDone = false; + + // Load NPC location data required for Rs2Slayer.getSlayerTaskLocation() + try { + Rs2NpcManager.loadJson(); + log.info("NPC location data loaded successfully"); + } catch (Exception e) { + log.error("Failed to load NPC location data: ", e); + } + + // Load JSON profiles from file + profileManager.loadProfiles(); + + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!Microbot.isLoggedIn()) { + wasLoggedOut = true; + return; + } + if (!super.run()) return; + if (!config.enablePlugin()) return; + + // Handle resume after break/logout + if (wasLoggedOut) { + wasLoggedOut = false; + SlayerState currentState = SlayerPlugin.getState(); + // If we were in a transient state, reset to re-evaluate + if (currentState != SlayerState.IDLE && + currentState != SlayerState.BANKING) { + log.info("Resuming after logout - resetting to DETECTING_TASK to re-evaluate"); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } + + // Read slayer points + int slayerPoints = Microbot.getVarbitValue(SLAYER_REWARD_POINTS_VARBIT); + SlayerPlugin.setSlayerPoints(slayerPoints); + + // Detect task + boolean hasTask = Rs2Slayer.hasSlayerTask(); + String taskName = ""; + int remaining = 0; + + if (hasTask) { + taskName = Rs2Slayer.getSlayerTask(); + remaining = Rs2Slayer.getSlayerTaskSize(); + } + + SlayerPlugin.updateTaskInfo(hasTask, taskName, remaining); + + // State machine + SlayerState currentState = SlayerPlugin.getState(); + + // Update break handler lock state - only allow breaks during safe states + updateBreakHandlerLock(currentState); + + switch (currentState) { + case IDLE: + handleIdleState(hasTask, taskName); + break; + case GETTING_TASK: + handleGettingTaskState(hasTask, taskName); + break; + case SKIPPING_TASK: + handleSkippingTaskState(); + break; + case BLOCKING_TASK: + handleBlockingTaskState(); + break; + case DETECTING_TASK: + handleDetectingTaskState(hasTask); + break; + case RESTORING_AT_POH: + handleRestoringAtPohState(); + break; + case BANKING: + handleBankingState(); + break; + case SWAPPING_SPELLBOOK: + handleSwappingSpellbookState(); + break; + case TRAVELING: + handleTravelingState(); + break; + case AT_LOCATION: + handleAtLocationState(hasTask, remaining); + break; + case FIGHTING: + handleFightingState(hasTask, remaining); + break; + } + + } catch (Exception ex) { + log.error("Slayer script error: ", ex); + } + }, 0, 600, TimeUnit.MILLISECONDS); + return true; + } + + private void handleIdleState(boolean hasTask, String taskName) { + if (hasTask) { + // Check block/skip lists before proceeding with an existing task + if (shouldBlockTask(taskName)) { + log.info("Existing task '{}' is on block list, transitioning to BLOCKING_TASK", taskName); + SlayerPlugin.setState(SlayerState.BLOCKING_TASK); + } else if (isTaskOnBlockList(taskName)) { + log.warn("Existing task '{}' is on block list but cannot afford to block, checking skip", taskName); + if (shouldSkipTask(taskName)) { + log.info("Falling back to skipping existing task '{}'", taskName); + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else { + log.info("Cannot afford to block/skip task '{}', proceeding with task", taskName); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } else if (shouldSkipTask(taskName)) { + log.info("Existing task '{}' is on skip list, transitioning to SKIPPING_TASK", taskName); + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else { + log.info("Task '{}' detected, transitioning to DETECTING_TASK", taskName); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } else if (config.getNewTask()) { + log.info("No task, transitioning to GETTING_TASK"); + SlayerPlugin.setState(SlayerState.GETTING_TASK); + } + } + + private void handleGettingTaskState(boolean hasTask, String taskName) { + // Handle dialogue if we're in one + if (Rs2Dialogue.isInDialogue()) { + handleSlayerDialogue(); + return; + } + + // If we now have a task, check if we should block or skip it + // Block takes priority over skip since it's permanent + if (hasTask) { + if (shouldBlockTask(taskName)) { + // Can afford to block - go block it + log.info("Task '{}' is on block list, transitioning to BLOCKING_TASK", taskName); + SlayerPlugin.setState(SlayerState.BLOCKING_TASK); + } else if (isTaskOnBlockList(taskName)) { + // Task is on block list but can't afford to block - try to skip instead + log.warn("Task '{}' is on block list but cannot afford to block, checking skip option", taskName); + if (shouldSkipTask(taskName)) { + log.info("Falling back to skipping task '{}'", taskName); + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else if (isTaskOnSkipList(taskName)) { + // Also on skip list but can't afford that either + log.error("Task '{}' cannot be blocked or skipped - insufficient points!", taskName); + Microbot.showMessage("Cannot block/skip task '" + taskName + "' - insufficient slayer points. Logging out."); + stopAndLogout(); + } else { + // Can't block, not on skip list - just do the task + log.info("Cannot afford to block task '{}', proceeding with task", taskName); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } else if (shouldSkipTask(taskName)) { + log.info("Task '{}' is on skip list, transitioning to SKIPPING_TASK", taskName); + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else if (isTaskOnSkipList(taskName)) { + // Task is on skip list but we can't afford to skip (insufficient points) + log.error("Task '{}' is on skip list but cannot skip - insufficient points!", taskName); + Microbot.showMessage("Cannot skip task '" + taskName + "' - insufficient slayer points. Logging out."); + stopAndLogout(); + } else { + log.info("Task '{}' received, transitioning to DETECTING_TASK", taskName); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + SlayerMaster master = config.slayerMaster(); + WorldPoint masterLocation = master.getLocation(); + SlayerPlugin.setCurrentLocation(master.getName()); + + // Check if we're close to the master + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation == null) { + return; + } + + int distance = playerLocation.distanceTo(masterLocation); + + if (distance <= 5) { + // We're at the master, interact to get task + Rs2NpcModel masterNpc = Rs2Npc.getNpc(master.getName()); + if (masterNpc != null) { + if (Rs2Npc.interact(masterNpc, "Assignment")) { + log.info("Requesting task from {}", master.getName()); + sleepUntil(Rs2Dialogue::isInDialogue, 3000); + } + } else { + log.warn("Could not find slayer master: {}", master.getName()); + } + } else { + // Walk to the master + if (!Rs2Player.isMoving()) { + log.info("Walking to slayer master: {} at {}", master.getName(), masterLocation); + Rs2Walker.walkTo(masterLocation); + } + } + } + + private void handleSlayerDialogue() { + // Boss task handling is disabled - users should disable "Like a Boss" perk for full AFK + // The boss amount input dialogue ("How many would you like to slay?") will not be auto-handled + // If you see this dialogue, you have "Like a Boss" enabled which is not supported + // if (isBossAmountInputOpen()) { + // handleBossAmountInput(); + // return; + // } + + // Click through continue prompts + if (Rs2Dialogue.hasContinue()) { + Rs2Dialogue.clickContinue(); + sleep(300, 600); + return; + } + + // Look for assignment-related options + if (Rs2Dialogue.hasDialogueOption("I need another assignment")) { + Rs2Dialogue.clickOption("I need another assignment"); + sleep(300, 600); + return; + } + + // Some masters have different dialogue options + if (Rs2Dialogue.hasDialogueOption("assignment")) { + Rs2Dialogue.clickOption("assignment", false); + sleep(300, 600); + return; + } + + // Handle "got any easier tasks?" type options if present + if (Rs2Dialogue.hasDialogueOption("easier")) { + Rs2Dialogue.clickOption("easier", false); + sleep(300, 600); + return; + } + + // Handle the post-task dialogue options (after receiving a task) + // Options are typically: "Got any tips?", "Can I cancel that task?", "Okay great!" + if (Rs2Dialogue.hasDialogueOption("Okay")) { + log.info("Clicking 'Okay' to dismiss post-task dialogue"); + Rs2Dialogue.clickOption("Okay", false); + sleep(300, 600); + return; + } + + // Alternative: some dialogues might say "great" or similar + if (Rs2Dialogue.hasDialogueOption("great")) { + log.info("Clicking 'great' to dismiss post-task dialogue"); + Rs2Dialogue.clickOption("great", false); + sleep(300, 600); + return; + } + + // If we have a task and we're stuck in dialogue, try pressing escape to close + if (Rs2Slayer.hasSlayerTask() && Rs2Dialogue.isInDialogue()) { + log.info("Have task but stuck in dialogue, pressing escape to close"); + Rs2Keyboard.keyPress(KeyEvent.VK_ESCAPE); + sleep(300, 600); + return; + } + } + + /** + * Checks if the boss task amount input dialogue is open. + * This is specifically for when selecting how many boss kills for a boss slayer task. + * The dialogue asks "How many would you like to slay?" or "How many would you like to hunt?" with a number input. + */ + private boolean isBossAmountInputOpen() { + try { + // Check for the specific chatbox input widget that appears for number entry + // Widget 162, 42 is the chatbox input title + String chatText = Rs2Widget.getChildWidgetText(162, 42); + if (chatText == null) { + return false; + } + + // Only trigger for specific boss task amount prompts + // The prompt varies: "How many would you like to slay?" or "How many would you like to hunt?" + // NOT for general "Enter amount" or dialogue that just mentions "boss" + String lowerText = chatText.toLowerCase(); + return lowerText.contains("how many would you like to slay") || + lowerText.contains("how many would you like to hunt"); + } catch (Exception e) { + return false; + } + } + + /** + * Handles the boss task amount selection by entering a value in the valid range + * Boss tasks typically allow selecting between 3-55, we pick a low number to complete quickly + */ + private void handleBossAmountInput() { + log.info("Boss task amount input detected, entering minimum amount"); + // Enter the minimum amount (3 is typical minimum for boss tasks) + // We use the minimum so the task is quick to skip + Rs2Keyboard.typeString("3"); + sleep(200, 400); + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + sleep(600, 1000); + log.info("Boss task amount entered, task will be assigned then skipped"); + } + + private void handleSkippingTaskState() { + // Handle dialogue or rewards interface if open + if (Rs2Dialogue.isInDialogue() || Rs2Widget.isWidgetVisible(SLAYER_REWARDS_GROUP_ID, 0)) { + skipAttemptCounter++; + handleSkipDialogue(); + return; + } + + // Check if we still have a task (skip might have completed) + String currentTask = Rs2Slayer.getSlayerTask(); + if (!Rs2Slayer.hasSlayerTask()) { + log.info("Task '{}' skipped successfully, transitioning to GETTING_TASK", taskBeingSkipped); + resetSkipState(); + SlayerPlugin.setState(SlayerState.GETTING_TASK); + return; + } + + // Check if we got a different task (skip worked and we auto-got new task) + if (!taskBeingSkipped.isEmpty() && !taskBeingSkipped.equalsIgnoreCase(currentTask)) { + log.info("Task changed from '{}' to '{}' - skip successful", taskBeingSkipped, currentTask); + resetSkipState(); + // Check if new task should also be skipped + if (shouldSkipTask(currentTask)) { + log.info("New task '{}' is also on skip list", currentTask); + taskBeingSkipped = currentTask; + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // Track which task we're trying to skip + if (taskBeingSkipped.isEmpty()) { + taskBeingSkipped = currentTask; + log.info("Starting skip process for task: {}", taskBeingSkipped); + } + + // Check if we've exceeded max attempts + if (skipAttemptCounter >= MAX_SKIP_ATTEMPTS) { + log.warn("Failed to skip task after {} attempts, proceeding with task instead", MAX_SKIP_ATTEMPTS); + resetSkipState(); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + return; + } + + // Check if we have enough points to skip + int slayerPoints = SlayerPlugin.getSlayerPoints(); + if (slayerPoints < SKIP_TASK_COST || slayerPoints < config.minPointsToSkip()) { + log.error("CANNOT SKIP TASK - Not enough slayer points! Have: {}, Need: {}, Min reserve: {}", + slayerPoints, SKIP_TASK_COST, config.minPointsToSkip()); + log.error("Task '{}' is on skip list but cannot be skipped. Stopping plugin and logging out.", taskBeingSkipped); + Microbot.showMessage("Cannot skip task '" + taskBeingSkipped + "' - insufficient slayer points. Logging out."); + resetSkipState(); + stopAndLogout(); + return; + } + + SlayerMaster master = config.slayerMaster(); + WorldPoint masterLocation = master.getLocation(); + SlayerPlugin.setCurrentLocation(master.getName() + " (Skipping)"); + + // Check if we're close to the master + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation == null) { + return; + } + + int distance = playerLocation.distanceTo(masterLocation); + + if (distance <= 5) { + // We're at the master, interact to open rewards + Rs2NpcModel masterNpc = Rs2Npc.getNpc(master.getName()); + if (masterNpc != null) { + if (Rs2Npc.interact(masterNpc, "Rewards")) { + log.info("Opening rewards menu to skip task (attempt {})", skipAttemptCounter + 1); + sleepUntil(() -> Rs2Dialogue.isInDialogue() || Rs2Widget.isWidgetVisible(SLAYER_REWARDS_GROUP_ID, 0), 3000); + } + } else { + log.warn("Could not find slayer master: {}", master.getName()); + skipAttemptCounter++; + } + } else { + // Walk to the master + if (!Rs2Player.isMoving()) { + log.info("Walking to slayer master to skip task: {}", master.getName()); + Rs2Walker.walkTo(masterLocation); + } + } + } + + private void resetSkipState() { + skipAttemptCounter = 0; + taskBeingSkipped = ""; + } + + // ==================== POH Methods ==================== + + private void handleRestoringAtPohState() { + SlayerPlugin.setCurrentLocation("POH"); + + // Check if we're in the house (house portal exists) + boolean inHouse = isInPoh(); + + if (!inHouse) { + // Need to teleport to house + if (!pohTeleportAttempted) { + if (teleportToHouse()) { + pohTeleportAttempted = true; + sleepUntil(this::isInPoh, 5000); + } else { + log.warn("Failed to teleport to house, falling back to banking"); + resetPohState(); + SlayerPlugin.setState(stateAfterPoh); + } + } else { + // Teleport was attempted but we're not in house - wait a bit more + sleep(600, 1000); + if (!isInPoh()) { + log.warn("Still not in house after teleport, falling back"); + resetPohState(); + SlayerPlugin.setState(stateAfterPoh); + } + } + return; + } + + // We're in the house - find and use the pool + if (!Rs2Player.isFullHealth() || !isFullPrayer() || Rs2Player.getRunEnergy() < 100) { + if (useRejuvenationPool()) { + log.info("Using rejuvenation pool"); + sleepUntil(() -> Rs2Player.isFullHealth() && isFullPrayer(), 5000); + } else { + log.warn("Could not find rejuvenation pool in house"); + } + } + + // Check if we're fully restored + if (Rs2Player.isFullHealth() && isFullPrayer()) { + log.info("Fully restored at POH, leaving house"); + leaveHouse(); + sleepUntil(() -> !isInPoh(), 3000); + resetPohState(); + + // Transition to next state + log.info("Transitioning to {}", stateAfterPoh); + SlayerPlugin.setState(stateAfterPoh); + } + } + + private boolean isInPoh() { + return Rs2GameObject.findObjectById(HOUSE_PORTAL_ID) != null; + } + + private boolean isFullPrayer() { + int currentPrayer = Rs2Player.getBoostedSkillLevel(Skill.PRAYER); + int maxPrayer = Rs2Player.getRealSkillLevel(Skill.PRAYER); + return currentPrayer >= maxPrayer; + } + + private boolean teleportToHouse() { + PohTeleportMethod method = config.pohTeleportMethod(); + log.info("Teleporting to house using: {}", method.getDisplayName()); + + switch (method) { + case SPELL: + if (Rs2Magic.canCast(MagicAction.TELEPORT_TO_HOUSE)) { + Rs2Magic.cast(MagicAction.TELEPORT_TO_HOUSE); + return true; + } + log.warn("Cannot cast Teleport to House spell"); + break; + + case HOUSE_TAB: + if (Rs2Inventory.hasItem("Teleport to house")) { + Rs2Inventory.interact("Teleport to house", "Break"); + return true; + } + log.warn("No house tabs in inventory"); + break; + + case CONSTRUCTION_CAPE: + if (Rs2Inventory.hasItem("Construct. cape") || Rs2Inventory.hasItem("Construct. cape(t)")) { + String capeName = Rs2Inventory.hasItem("Construct. cape(t)") ? "Construct. cape(t)" : "Construct. cape"; + Rs2Inventory.interact(capeName, "Tele to POH"); + return true; + } + // Also check if worn + // For now, just check inventory + log.warn("No construction cape in inventory"); + break; + + case MAX_CAPE: + if (Rs2Inventory.hasItem("Max cape")) { + Rs2Inventory.interact("Max cape", "Tele to POH"); + return true; + } + log.warn("No max cape in inventory"); + break; + } + + // Fallback to house tab if primary method fails + if (method != PohTeleportMethod.HOUSE_TAB && Rs2Inventory.hasItem("Teleport to house")) { + log.info("Falling back to house tab"); + Rs2Inventory.interact("Teleport to house", "Break"); + return true; + } + + return false; + } + + private boolean useRejuvenationPool() { + for (int poolId : REJUVENATION_POOL_IDS) { + if (Rs2GameObject.exists(poolId)) { + return Rs2GameObject.interact(poolId, "Drink"); + } + } + return false; + } + + private void leaveHouse() { + // Try to use portal nexus to teleport to Grand Exchange (near bank) + if (teleportViaPortalNexus()) { + log.info("Leaving house via portal nexus to Grand Exchange"); + return; + } + + // Try mounted glory to Edgeville (near GE) + if (teleportViaMountedGlory()) { + log.info("Leaving house via mounted glory to Edgeville"); + return; + } + + // Try mounted ring of wealth to GE + if (teleportViaMountedWealth()) { + log.info("Leaving house via mounted ring of wealth to Grand Exchange"); + return; + } + + // Fallback to house portal exit + if (Rs2GameObject.interact(HOUSE_PORTAL_ID, "Enter")) { + log.info("Leaving house via portal (no teleport options found)"); + } + } + + /** + * Attempts to teleport to the Grand Exchange via the portal nexus + * @return true if interaction was successful, false if nexus not found + */ + private boolean teleportViaPortalNexus() { + // Try to find any tier of portal nexus + for (int nexusId : PORTAL_NEXUS_IDS) { + if (Rs2GameObject.exists(nexusId)) { + // Try direct Grand Exchange teleport option first + if (Rs2GameObject.interact(nexusId, "Grand Exchange")) { + return true; + } + // Some nexus configurations might use different option names + if (Rs2GameObject.interact(nexusId, "Varrock Grand Exchange")) { + return true; + } + } + } + return false; + } + + /** + * Attempts to teleport via mounted glory to Edgeville (close to GE) + * @return true if interaction was successful, false if mounted glory not found + */ + private boolean teleportViaMountedGlory() { + for (int gloryId : MOUNTED_GLORY_IDS) { + if (Rs2GameObject.exists(gloryId)) { + if (Rs2GameObject.interact(gloryId, "Edgeville")) { + return true; + } + } + } + return false; + } + + /** + * Attempts to teleport via mounted ring of wealth to Grand Exchange + * @return true if interaction was successful, false if mounted wealth not found + */ + private boolean teleportViaMountedWealth() { + if (Rs2GameObject.exists(MOUNTED_WEALTH_ID)) { + if (Rs2GameObject.interact(MOUNTED_WEALTH_ID, "Grand Exchange")) { + return true; + } + } + return false; + } + + private void resetPohState() { + pohTeleportAttempted = false; + } + + /** + * Checks if we should use POH for restoration based on current state + */ + private boolean shouldUsePoh() { + if (!config.usePohPool()) { + return false; + } + + // Check if we have a way to teleport + PohTeleportMethod method = config.pohTeleportMethod(); + switch (method) { + case SPELL: + if (!Rs2Magic.canCast(MagicAction.TELEPORT_TO_HOUSE)) { + return Rs2Inventory.hasItem("Teleport to house"); // Fallback check + } + return true; + case HOUSE_TAB: + return Rs2Inventory.hasItem("Teleport to house"); + case CONSTRUCTION_CAPE: + return Rs2Inventory.hasItem("Construct. cape") || + Rs2Inventory.hasItem("Construct. cape(t)") || + Rs2Inventory.hasItem("Teleport to house"); + case MAX_CAPE: + return Rs2Inventory.hasItem("Max cape") || + Rs2Inventory.hasItem("Teleport to house"); + default: + return Rs2Inventory.hasItem("Teleport to house"); + } + } + + /** + * Checks if we need POH restoration based on HP/Prayer thresholds + */ + private boolean needsPohRestoration() { + if (!config.usePohPool() || !config.usePohBeforeBanking()) { + return false; + } + + int threshold = config.pohRestoreThreshold(); + if (threshold <= 0) { + return false; + } + + double healthPercent = Rs2Player.getHealthPercentage(); + return healthPercent < threshold && shouldUsePoh(); + } + + // ==================== End POH Methods ==================== + + // Slayer rewards interface widget constants (group 426) + // Slayer rewards interface (widget group 426) + private static final int SLAYER_REWARDS_GROUP_ID = 426; + + private void handleSkipDialogue() { + // Click through continue prompts first + if (Rs2Dialogue.hasContinue()) { + Rs2Dialogue.clickContinue(); + sleep(300, 600); + return; + } + + // Check if slayer rewards interface is open (widget-based, not dialogue) + if (Rs2Widget.isWidgetVisible(SLAYER_REWARDS_GROUP_ID, 0)) { + handleSlayerRewardsInterface(); + return; + } + + // Step 1: Look for "Assignment" or "Assignments" option in main Rewards menu + // This takes us to the task management submenu + if (Rs2Dialogue.hasDialogueOption("Assignment")) { + log.info("Clicking Assignment option"); + Rs2Dialogue.clickOption("Assignment"); + sleep(300, 600); + return; + } + + // Step 2: Look for task cancellation options + // Different masters/interfaces may use different wording + if (Rs2Dialogue.hasDialogueOption("Cancel current task")) { + log.info("Clicking Cancel current task option (costs 30 points)"); + Rs2Dialogue.clickOption("Cancel current task"); + sleep(300, 600); + return; + } + + // Alternative wording - some interfaces say "Skip task" + if (Rs2Dialogue.hasDialogueOption("Skip task")) { + log.info("Clicking Skip task option"); + Rs2Dialogue.clickOption("Skip task"); + sleep(300, 600); + return; + } + + // Partial match for "cancel" in case of different wording + if (Rs2Dialogue.hasDialogueOption("cancel")) { + log.info("Clicking cancel option (partial match)"); + Rs2Dialogue.clickOption("cancel", false); + sleep(300, 600); + return; + } + + // Step 3: Confirm the skip + if (Rs2Dialogue.hasDialogueOption("Yes")) { + log.info("Confirming task skip"); + Rs2Dialogue.clickOption("Yes"); + sleep(600, 1000); + return; + } + + // If we see a "No" option but no "Yes", we might be in wrong menu - go back + if (Rs2Dialogue.hasDialogueOption("No") && !Rs2Dialogue.hasDialogueOption("Yes")) { + log.warn("Found No but not Yes - might be in wrong dialogue state"); + } + } + + /** + * Handles the slayer rewards widget interface for skipping/cancelling a task. + * Flow: Task tab -> Cancel -> Confirm + */ + private void handleSlayerRewardsInterface() { + // Step 3: Confirm overlay + if (Rs2Widget.clickWidget("Confirm", Optional.of(SLAYER_REWARDS_GROUP_ID), 0, false)) { + log.info("Clicking Confirm to finalize task skip"); + sleep(600, 1000); + return; + } + + // Step 2: Cancel button (visible when on Task tab) + if (Rs2Widget.clickWidget("Cancel", Optional.of(SLAYER_REWARDS_GROUP_ID), 0, false)) { + log.info("Clicking Cancel task button (costs 30 points)"); + sleep(600, 1000); + return; + } + + // Step 1: Navigate to Task tab + if (Rs2Widget.clickWidget("Tasks", Optional.of(SLAYER_REWARDS_GROUP_ID), 0, false)) { + log.info("Clicking Tasks tab in slayer rewards interface"); + sleep(400, 700); + return; + } + + log.warn("Could not find Tasks tab, Cancel button, or Confirm in slayer rewards interface"); + } + + /** + * Checks if the given task name should be skipped based on config + */ + private boolean shouldSkipTask(String taskName) { + if (!config.enableAutoSkip() || taskName == null || taskName.isEmpty()) { + return false; + } + + String skipList = config.skipTaskList(); + if (skipList == null || skipList.isEmpty()) { + return false; + } + + // Check if we have enough points + int slayerPoints = SlayerPlugin.getSlayerPoints(); + if (slayerPoints < SKIP_TASK_COST || slayerPoints < config.minPointsToSkip()) { + return false; + } + + // Parse skip list and check if task is in it + String taskLower = taskName.toLowerCase().trim(); + return Arrays.stream(skipList.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(skipTask -> taskLower.contains(skipTask) || skipTask.contains(taskLower)); + } + + /** + * Checks if a task is on the skip list (ignoring point requirements) + * Used to determine if we should stop when we can't afford to skip + */ + private boolean isTaskOnSkipList(String taskName) { + if (!config.enableAutoSkip() || taskName == null || taskName.isEmpty()) { + return false; + } + + String skipList = config.skipTaskList(); + if (skipList == null || skipList.isEmpty()) { + return false; + } + + // Just check if task is in the skip list, don't check points + String taskLower = taskName.toLowerCase().trim(); + return Arrays.stream(skipList.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(skipTask -> taskLower.contains(skipTask) || skipTask.contains(taskLower)); + } + + // ==================== Block Task Methods ==================== + + private void handleBlockingTaskState() { + // Handle dialogue or rewards interface if open + if (Rs2Dialogue.isInDialogue() || Rs2Widget.isWidgetVisible(SLAYER_REWARDS_GROUP_ID, 0)) { + blockAttemptCounter++; + handleBlockDialogue(); + return; + } + + // Check if we still have a task (block might have completed) + String currentTask = Rs2Slayer.getSlayerTask(); + if (!Rs2Slayer.hasSlayerTask()) { + log.info("Task '{}' blocked successfully, transitioning to GETTING_TASK", taskBeingBlocked); + resetBlockState(); + SlayerPlugin.setState(SlayerState.GETTING_TASK); + return; + } + + // Check if we got a different task (block worked and we auto-got new task) + if (!taskBeingBlocked.isEmpty() && !taskBeingBlocked.equalsIgnoreCase(currentTask)) { + log.info("Task changed from '{}' to '{}' - block successful", taskBeingBlocked, currentTask); + resetBlockState(); + // Check if new task should also be blocked or skipped + if (shouldBlockTask(currentTask)) { + log.info("New task '{}' is also on block list", currentTask); + taskBeingBlocked = currentTask; + } else if (shouldSkipTask(currentTask)) { + log.info("New task '{}' is on skip list", currentTask); + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // Track which task we're trying to block + if (taskBeingBlocked.isEmpty()) { + taskBeingBlocked = currentTask; + log.info("Starting block process for task: {}", taskBeingBlocked); + } + + // Check if we've exceeded max attempts + if (blockAttemptCounter >= MAX_BLOCK_ATTEMPTS) { + log.warn("Failed to block task after {} attempts, trying to skip instead", MAX_BLOCK_ATTEMPTS); + resetBlockState(); + // Fall back to skipping if we can + if (shouldSkipTask(currentTask)) { + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // Check if we have enough points to block + int slayerPoints = SlayerPlugin.getSlayerPoints(); + if (slayerPoints < BLOCK_TASK_COST || slayerPoints < config.minPointsToBlock()) { + log.warn("Cannot block task - not enough points. Have: {}, Need: {}, Min reserve: {}", + slayerPoints, BLOCK_TASK_COST, config.minPointsToBlock()); + log.info("Checking if task can be skipped instead..."); + resetBlockState(); + // Try to skip instead if on skip list and can afford + if (shouldSkipTask(currentTask)) { + SlayerPlugin.setState(SlayerState.SKIPPING_TASK); + } else if (isTaskOnSkipList(currentTask)) { + // On skip list but can't afford skip either + log.error("Task '{}' cannot be blocked or skipped - insufficient points!", currentTask); + Microbot.showMessage("Cannot block/skip task '" + currentTask + "' - insufficient slayer points. Logging out."); + stopAndLogout(); + } else { + // Not on skip list, just do the task + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + SlayerMaster master = config.slayerMaster(); + WorldPoint masterLocation = master.getLocation(); + SlayerPlugin.setCurrentLocation(master.getName() + " (Blocking)"); + + // Check if we're close to the master + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation == null) { + return; + } + + int distance = playerLocation.distanceTo(masterLocation); + + if (distance <= 5) { + // We're at the master, interact to open rewards + Rs2NpcModel masterNpc = Rs2Npc.getNpc(master.getName()); + if (masterNpc != null) { + if (Rs2Npc.interact(masterNpc, "Rewards")) { + log.info("Opening rewards menu to block task (attempt {})", blockAttemptCounter + 1); + sleepUntil(() -> Rs2Dialogue.isInDialogue() || Rs2Widget.isWidgetVisible(SLAYER_REWARDS_GROUP_ID, 0), 3000); + } + } else { + log.warn("Could not find slayer master: {}", master.getName()); + blockAttemptCounter++; + } + } else { + // Walk to the master + if (!Rs2Player.isMoving()) { + log.info("Walking to slayer master to block task: {}", master.getName()); + Rs2Walker.walkTo(masterLocation); + } + } + } + + private void handleBlockDialogue() { + // Click through continue prompts first + if (Rs2Dialogue.hasContinue()) { + Rs2Dialogue.clickContinue(); + sleep(300, 600); + return; + } + + // Check if slayer rewards interface is open (widget-based, not dialogue) + if (Rs2Widget.isWidgetVisible(SLAYER_REWARDS_GROUP_ID, 0)) { + handleSlayerRewardsBlockInterface(); + return; + } + + // Standard dialogue options (fallback for older interfaces) + if (Rs2Dialogue.hasDialogueOption("Assignment")) { + log.info("Clicking Assignment option"); + Rs2Dialogue.clickOption("Assignment"); + sleep(300, 600); + return; + } + + if (Rs2Dialogue.hasDialogueOption("Block current task")) { + log.info("Clicking Block current task option (costs 100 points)"); + Rs2Dialogue.clickOption("Block current task"); + sleep(300, 600); + return; + } + + if (Rs2Dialogue.hasDialogueOption("Block task")) { + log.info("Clicking Block task option"); + Rs2Dialogue.clickOption("Block task"); + sleep(300, 600); + return; + } + + if (Rs2Dialogue.hasDialogueOption("block")) { + log.info("Clicking block option (partial match)"); + Rs2Dialogue.clickOption("block", false); + sleep(300, 600); + return; + } + + // Confirm the block + if (Rs2Dialogue.hasDialogueOption("Yes")) { + log.info("Confirming task block"); + Rs2Dialogue.clickOption("Yes"); + sleep(600, 1000); + return; + } + } + + /** + * Handles the slayer rewards widget interface for blocking a task. + * Flow: Task tab -> Block -> Confirm + */ + private void handleSlayerRewardsBlockInterface() { + // Step 3: Confirm overlay + if (Rs2Widget.clickWidget("Confirm", Optional.of(SLAYER_REWARDS_GROUP_ID), 0, false)) { + log.info("Clicking Confirm to finalize task block"); + sleep(600, 1000); + return; + } + + // Step 2: Block button (visible when on Task tab) + if (Rs2Widget.clickWidget("Block", Optional.of(SLAYER_REWARDS_GROUP_ID), 0, false)) { + log.info("Clicking Block task button (costs 100 points)"); + sleep(600, 1000); + return; + } + + // Step 1: Navigate to Task tab + if (Rs2Widget.clickWidget("Tasks", Optional.of(SLAYER_REWARDS_GROUP_ID), 0, false)) { + log.info("Clicking Tasks tab in slayer rewards interface"); + sleep(400, 700); + return; + } + + log.warn("Could not find Tasks tab, Block button, or Confirm in slayer rewards interface"); + } + + private void resetBlockState() { + blockAttemptCounter = 0; + taskBeingBlocked = ""; + } + + /** + * Checks if the given task name should be blocked based on config. + * Block takes priority over skip since it's a permanent solution. + */ + private boolean shouldBlockTask(String taskName) { + if (!config.enableAutoBlock() || taskName == null || taskName.isEmpty()) { + return false; + } + + String blockList = config.blockTaskList(); + if (blockList == null || blockList.isEmpty()) { + return false; + } + + // Check if we have enough points to block + int slayerPoints = SlayerPlugin.getSlayerPoints(); + if (slayerPoints < BLOCK_TASK_COST || slayerPoints < config.minPointsToBlock()) { + return false; + } + + // Parse block list and check if task is in it + String taskLower = taskName.toLowerCase().trim(); + return Arrays.stream(blockList.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(blockTask -> taskLower.contains(blockTask) || blockTask.contains(taskLower)); + } + + /** + * Checks if a task is on the block list (ignoring point requirements) + * Used to determine if we should try to skip instead when we can't afford to block + */ + private boolean isTaskOnBlockList(String taskName) { + if (!config.enableAutoBlock() || taskName == null || taskName.isEmpty()) { + return false; + } + + String blockList = config.blockTaskList(); + if (blockList == null || blockList.isEmpty()) { + return false; + } + + // Just check if task is in the block list, don't check points + String taskLower = taskName.toLowerCase().trim(); + return Arrays.stream(blockList.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(blockTask -> taskLower.contains(blockTask) || blockTask.contains(taskLower)); + } + + private void handleDetectingTaskState(boolean hasTask) { + // Dismiss any open dialogue first (e.g., residual slayer master dialogue) + if (Rs2Dialogue.isInDialogue()) { + if (Rs2Dialogue.hasContinue()) { + Rs2Dialogue.clickContinue(); + sleep(300, 600); + } else { + // Try pressing escape to close dialogue + Rs2Keyboard.keyPress(KeyEvent.VK_ESCAPE); + sleep(300, 600); + } + return; + } + + if (!hasTask) { + log.info("No task found, returning to IDLE"); + SlayerPlugin.setState(SlayerState.IDLE); + resetTravelData(); + return; + } + + String taskName = Rs2Slayer.getSlayerTask(); + + // Look up JSON profile first (preferred) + activeJsonProfile = profileManager.findProfile(taskName); + if (activeJsonProfile != null) { + log.info("Found JSON profile for '{}': setup={}, prayer={}, cannon={}, antipoison={}, antivenom={}", + taskName, activeJsonProfile.getSetup(), activeJsonProfile.getPrayer(), + activeJsonProfile.isCannon(), activeJsonProfile.isAntipoison(), activeJsonProfile.isAntivenom()); + } else { + log.warn("No JSON profile found for task '{}' - prayer/location settings may not apply. " + + "Add a profile to ~/.runelite/slayer-profiles.json or delete the file to regenerate defaults.", + taskName); + } + + // Update flicker script with active profile prayer + if (activeJsonProfile != null && activeJsonProfile.hasPrayer()) { + SlayerPlugin.getFlickerScript().setActivePrayer(activeJsonProfile.getParsedPrayer()); + } else { + SlayerPlugin.getFlickerScript().clearActiveProfile(); + } + + // Determine if we should use cannon for this task + isUsingCannon = shouldUseCannon(taskName); + + // Get task location + if (taskDestination == null) { + // Priority 1: If using cannon and profile has cannonLocation, use that + if (isUsingCannon && activeJsonProfile != null && activeJsonProfile.hasCannonLocation()) { + SlayerLocation cannonLocation = SlayerLocation.fromName(activeJsonProfile.getCannonLocation()); + if (cannonLocation != null) { + taskDestination = cannonLocation.getWorldPoint(); + cannonSpot = taskDestination; // Cannon will be placed here + taskLocationName = cannonLocation.getDisplayName(); + SlayerPlugin.setCurrentLocation(taskLocationName + " (Cannon)"); + log.info("Using profile cannon location for {}: {} at {}", taskName, taskLocationName, taskDestination); + } else { + log.warn("Unknown profile cannon location '{}', falling back to auto-selection", activeJsonProfile.getCannonLocation()); + } + } + + // Priority 2: If not using cannon and profile has location, use that + if (taskDestination == null && !isUsingCannon && activeJsonProfile != null && activeJsonProfile.hasLocation()) { + SlayerLocation profileLocation = SlayerLocation.fromName(activeJsonProfile.getLocation()); + if (profileLocation != null) { + taskDestination = profileLocation.getWorldPoint(); + taskLocationName = profileLocation.getDisplayName(); + SlayerPlugin.setCurrentLocation(taskLocationName); + log.info("Using profile location for {}: {} at {}", taskName, taskLocationName, taskDestination); + } else { + log.warn("Unknown profile location '{}', falling back to auto-selection", activeJsonProfile.getLocation()); + } + } + + // Priority 3: If using cannon but no cannonLocation, try CannonSpot enum + if (taskDestination == null && isUsingCannon) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + cannonSpot = CannonSpot.getClosestSpot(taskName, playerLocation); + if (cannonSpot != null) { + taskDestination = cannonSpot; + taskLocationName = "Cannon spot"; + SlayerPlugin.setCurrentLocation(taskLocationName + " (Cannon)"); + log.info("Cannon spot found for {}: {}", taskName, cannonSpot); + } else { + log.warn("No cannon spot found for task: {}, using regular location", taskName); + isUsingCannon = false; + } + } + + // Priority 4: Fall back to auto-selected task location + if (taskDestination == null) { + MonsterLocation monsterLocation = Rs2Slayer.getSlayerTaskLocation(3, true); + if (monsterLocation != null) { + taskDestination = monsterLocation.getBestClusterCenter(); + taskLocationName = monsterLocation.getLocationName(); + SlayerPlugin.setCurrentLocation(taskLocationName); + log.info("Task location found: {} at {}", taskLocationName, taskDestination); + } else { + log.warn("Could not find location for slayer task"); + return; + } + } + } + + // Check if we need to bank first (initial setup or low supplies) + if (config.enableAutoBanking() && getActiveInventorySetup() != null) { + if (!initialSetupDone || needsBanking()) { + log.info("Banking required, transitioning to BANKING"); + SlayerPlugin.setState(SlayerState.BANKING); + return; + } + } + + // Go to traveling + if (config.enableAutoTravel()) { + log.info("Transitioning to TRAVELING"); + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + log.info("Auto travel disabled, staying in DETECTING_TASK"); + } + } + + private void handleBankingState() { + // Dismiss any open dialogue first (e.g., residual slayer master dialogue) + if (Rs2Dialogue.isInDialogue()) { + if (Rs2Dialogue.hasContinue()) { + Rs2Dialogue.clickContinue(); + sleep(300, 600); + } else { + // Try pressing escape to close dialogue + Rs2Keyboard.keyPress(KeyEvent.VK_ESCAPE); + sleep(300, 600); + } + return; + } + + // Disable prayers when banking + deactivateProfilePrayer(); + + // Safety: if cannonPlaced is still true at banking, the cannon was not picked up before leaving + // This can happen if the player teleported away. Reset the flag to avoid getting stuck. + if (cannonPlaced) { + if (isCannonPlacedNearby()) { + log.info("Cannon found nearby at bank, picking up"); + if (pickupCannon()) { + sleepUntil(() -> !isCannonPlacedNearby(), 3000); + } + } else { + log.warn("Cannon was left at task location (not nearby), resetting cannon state"); + } + cannonPlaced = false; + cannonFired = false; + } + + // Get the active inventory setup (cannon or regular) + var activeSetup = getActiveInventorySetup(); + if (activeSetup == null) { + log.warn("No inventory setup configured, skipping banking"); + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // If bank is not open, walk to bank and open it + if (!Rs2Bank.isOpen()) { + if (!Rs2Player.isMoving()) { + // Try to open bank if we're near one + if (Rs2Bank.openBank()) { + sleepUntil(Rs2Bank::isOpen, 3000); + } else { + log.info("Walking to nearest bank"); + Rs2Bank.walkToBank(); + } + } + return; + } + + // Bank is open, load inventory setup + log.info("Loading inventory setup: {} (cannon: {})", activeSetup.getName(), isUsingCannon); + Rs2InventorySetup inventorySetup = new Rs2InventorySetup(activeSetup, mainScheduledFuture); + + // Check if already matching + if (inventorySetup.doesEquipmentMatch() && inventorySetup.doesInventoryMatch()) { + log.info("Inventory setup matches, closing bank"); + Rs2Bank.closeBank(); + sleepUntil(() -> !Rs2Bank.isOpen(), 2000); + initialSetupDone = true; + + // Check if we need to swap spellbook based on inventory setup + if (needsSpellbookSwap()) { + log.info("Inventory setup requires spellbook swap - transitioning to SWAPPING_SPELLBOOK"); + SlayerPlugin.setState(SlayerState.SWAPPING_SPELLBOOK); + } else if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // Load equipment and inventory + boolean equipmentLoaded = inventorySetup.loadEquipment(); + boolean inventoryLoaded = inventorySetup.loadInventory(); + + if (equipmentLoaded && inventoryLoaded) { + log.info("Inventory setup loaded successfully"); + Rs2Bank.closeBank(); + sleepUntil(() -> !Rs2Bank.isOpen(), 2000); + initialSetupDone = true; + + // Check if we need to swap spellbook based on inventory setup + if (needsSpellbookSwap()) { + log.info("Inventory setup requires spellbook swap - transitioning to SWAPPING_SPELLBOOK"); + SlayerPlugin.setState(SlayerState.SWAPPING_SPELLBOOK); + } else if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } else { + log.warn("Failed to load inventory setup completely"); + } + } + + // Spellbook integer values from InventorySetup + // 0=Standard, 1=Ancient, 2=Lunar, 3=Arceuus, 4=None + private static final int SPELLBOOK_STANDARD = 0; + private static final int SPELLBOOK_ANCIENT = 1; + private static final int SPELLBOOK_LUNAR = 2; + private static final int SPELLBOOK_ARCEUUS = 3; + private static final int SPELLBOOK_NONE = 4; + + // Track the current inventory setup for spellbook checking + private net.runelite.client.plugins.microbot.inventorysetups.InventorySetup currentInventorySetup = null; + + /** + * Checks if the current inventory setup requires a spellbook swap. + * Returns true if the inventory setup has a spellbook defined that doesn't match current. + */ + private boolean needsSpellbookSwap() { + // Get the active inventory setup + var setup = getActiveInventorySetup(); + if (setup == null) { + log.debug("No inventory setup, no spellbook swap needed"); + return false; + } + + // Store reference for the spellbook swap handler + currentInventorySetup = setup; + + // Create Rs2InventorySetup to check spellbook + Rs2InventorySetup inventorySetup = new Rs2InventorySetup(setup, mainScheduledFuture); + + // Check if spellbook matches - if it does, no swap needed + if (inventorySetup.hasSpellBook()) { + log.debug("Spellbook matches inventory setup, no swap needed"); + return false; + } + + // Spellbook doesn't match - need to swap + // Get the required spellbook from the inventory setup + int requiredSpellbook = setup.getSpellBook(); + if (requiredSpellbook == SPELLBOOK_NONE || requiredSpellbook < 0) { + log.debug("Inventory setup has no spellbook requirement (value: {})", requiredSpellbook); + return false; + } + + log.info("Inventory setup requires spellbook {} but player is on different spellbook - swap required", + getSpellbookName(requiredSpellbook)); + return true; + } + + /** + * Gets a human-readable name for a spellbook integer value. + */ + private String getSpellbookName(int spellbook) { + switch (spellbook) { + case SPELLBOOK_STANDARD: return "Standard"; + case SPELLBOOK_ANCIENT: return "Ancient"; + case SPELLBOOK_LUNAR: return "Lunar"; + case SPELLBOOK_ARCEUUS: return "Arceuus"; + default: return "Unknown (" + spellbook + ")"; + } + } + + /** + * Gets the target spellbook from the current inventory setup. + * Returns null if no setup or no spellbook requirement. + * Spellbook values: 0=Standard, 1=Ancient, 2=Lunar, 3=Arceuus, 4=None + */ + private Rs2Spellbook getRequiredSpellbook() { + if (currentInventorySetup == null) { + return null; + } + + int spellbook = currentInventorySetup.getSpellBook(); + + // If spellbook is NONE or invalid, no requirement + if (spellbook == SPELLBOOK_NONE || spellbook < 0) { + return null; + } + + // Convert InventorySetup spellbook int to Rs2Spellbook + switch (spellbook) { + case SPELLBOOK_ANCIENT: + return Rs2Spellbook.ANCIENT; + case SPELLBOOK_LUNAR: + return Rs2Spellbook.LUNAR; + case SPELLBOOK_STANDARD: + default: + return Rs2Spellbook.MODERN; + } + } + + // Occult Altar IDs for POH (Basic, Marble, Gilded, Ancient variants) + private static final int[] OCCULT_ALTAR_IDS = {29149, 29150, 29151, 29152}; + + /** + * Handles swapping spellbook in POH based on inventory setup requirements. + * Uses the occult altar to switch to the required spellbook. + */ + private void handleSwappingSpellbookState() { + SlayerPlugin.setCurrentLocation("POH (Spellbook)"); + + // Get the required spellbook from inventory setup + Rs2Spellbook requiredSpellbook = getRequiredSpellbook(); + if (requiredSpellbook == null) { + log.warn("No spellbook requirement found, skipping swap"); + resetPohState(); + currentInventorySetup = null; + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // If we're already on the required spellbook, skip to traveling + if (Rs2Magic.isSpellbook(requiredSpellbook)) { + log.info("Already on {} spellbook, transitioning to TRAVELING", requiredSpellbook); + resetPohState(); + currentInventorySetup = null; + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + return; + } + + // Check if we're in the house (house portal exists) + boolean inHouse = isInPoh(); + + if (!inHouse) { + // Need to teleport to house + if (!pohTeleportAttempted) { + if (teleportToHouse()) { + pohTeleportAttempted = true; + sleepUntil(this::isInPoh, 5000); + } else { + log.warn("Failed to teleport to house for spellbook swap, falling back to TRAVELING"); + resetPohState(); + currentInventorySetup = null; + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } + } else { + // Teleport was attempted but we're not in house - wait a bit more + sleep(600, 1000); + if (!isInPoh()) { + log.warn("Still not in house after teleport for spellbook swap, falling back"); + resetPohState(); + currentInventorySetup = null; + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } + } + return; + } + + // Check if the occult altar dialog is genuinely open (only if a "Select an Option" dialog is visible) + if (Rs2Dialogue.hasSelectAnOption()) { + log.info("Occult altar dialog already open, handling spellbook selection"); + if (handleSpellbookWidget(requiredSpellbook)) { + return; + } + } + + // Find and interact with the occult altar + TileObject occultAltar = findOccultAltar(); + if (occultAltar != null) { + // Try right-click option first (e.g., "Ancient Magicks", "Lunar", "Standard") + String altarOption = getAltarOptionForSpellbook(requiredSpellbook); + log.info("Attempting occult altar right-click option: '{}'", altarOption); + + if (Rs2GameObject.interact(occultAltar, altarOption)) { + // Wait briefly for either direct spellbook switch or a dialog to open + sleepUntil(() -> Rs2Magic.isSpellbook(requiredSpellbook) || Rs2Dialogue.hasSelectAnOption(), 3000); + + // Check if the spellbook already switched (right-click option worked directly) + if (Rs2Magic.isSpellbook(requiredSpellbook)) { + log.info("Successfully switched to {} spellbook via right-click", requiredSpellbook); + finishSpellbookSwap(); + return; + } + + // Right-click option may have opened a dialog - handle it + if (Rs2Dialogue.hasSelectAnOption() && handleSpellbookWidget(requiredSpellbook)) { + return; + } + } else { + // Right-click option didn't match - try left-click "Venerate" which opens dialog + log.info("Right-click option '{}' not found, trying Venerate", altarOption); + if (Rs2GameObject.interact(occultAltar, "Venerate")) { + sleepUntil(Rs2Dialogue::hasSelectAnOption, 3000); + if (!handleSpellbookWidget(requiredSpellbook)) { + log.warn("Venerate used but could not handle spellbook dialog"); + } + } else { + // Last resort: plain interact + log.info("Trying plain interact on occult altar"); + Rs2GameObject.interact(occultAltar); + sleepUntil(Rs2Dialogue::hasSelectAnOption, 3000); + handleSpellbookWidget(requiredSpellbook); + } + } + } else { + log.warn("Could not find occult altar in house - ensure POH has an occult altar"); + spellbookSwapAttempts++; + if (spellbookSwapAttempts >= MAX_SPELLBOOK_SWAP_ATTEMPTS) { + log.warn("Max spellbook swap attempts reached, skipping swap"); + spellbookSwapAttempts = 0; + leaveHouse(); + sleepUntil(() -> !isInPoh(), 3000); + resetPohState(); + currentInventorySetup = null; + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } + } + } + + /** + * Handles a spellbook selection widget/dialog if one is open. + * The occult altar can open a dialog with options like "Ancient Magicks", "Lunar", etc. + * Returns true if the widget was handled (regardless of success), false if no widget found. + */ + private boolean handleSpellbookWidget(Rs2Spellbook requiredSpellbook) { + // Check for a "Select an Option" dialog (standard game dialog) + if (Rs2Dialogue.hasSelectAnOption()) { + String dialogOption = getDialogOptionForSpellbook(requiredSpellbook); + log.info("Spellbook selection dialog detected, clicking option: '{}'", dialogOption); + Rs2Dialogue.clickOption(dialogOption); + sleepUntil(() -> Rs2Magic.isSpellbook(requiredSpellbook), 3000); + + if (Rs2Magic.isSpellbook(requiredSpellbook)) { + log.info("Successfully switched to {} spellbook via dialog", requiredSpellbook); + finishSpellbookSwap(); + } else { + log.warn("Dialog option clicked but spellbook didn't change"); + } + return true; + } + + // Check for widget-based spellbook selection (search by text) + String widgetText = getWidgetTextForSpellbook(requiredSpellbook); + Widget spellbookWidget = Rs2Widget.findWidget(widgetText); + if (spellbookWidget != null) { + log.info("Found spellbook widget with text '{}', clicking", widgetText); + Rs2Widget.clickWidget(spellbookWidget); + sleepUntil(() -> Rs2Magic.isSpellbook(requiredSpellbook), 3000); + + if (Rs2Magic.isSpellbook(requiredSpellbook)) { + log.info("Successfully switched to {} spellbook via widget", requiredSpellbook); + finishSpellbookSwap(); + } else { + log.warn("Widget clicked but spellbook didn't change"); + } + return true; + } + + return false; + } + + /** + * Completes the spellbook swap by leaving POH and transitioning to next state. + */ + private void finishSpellbookSwap() { + spellbookSwapAttempts = 0; + + // Set up autocast while stationary at POH (before leaving house) + setupAutoCastIfNeeded(); + + leaveHouse(); + sleepUntil(() -> !isInPoh(), 3000); + resetPohState(); + currentInventorySetup = null; + + if (config.enableAutoTravel()) { + SlayerPlugin.setState(SlayerState.TRAVELING); + } else { + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + } + } + + /** + * Gets the occult altar right-click menu option for the given spellbook. + */ + private String getAltarOptionForSpellbook(Rs2Spellbook spellbook) { + switch (spellbook) { + case ANCIENT: + return "Ancient Magicks"; + case LUNAR: + return "Lunar"; + case MODERN: + default: + return "Standard"; + } + } + + /** + * Gets the dialog option text for selecting a spellbook in a selection dialog. + * Uses partial match so it works with Rs2Dialogue.clickOption(). + */ + private String getDialogOptionForSpellbook(Rs2Spellbook spellbook) { + switch (spellbook) { + case ANCIENT: + return "Ancient"; + case LUNAR: + return "Lunar"; + case MODERN: + default: + return "Standard"; + } + } + + /** + * Gets the widget text to search for when clicking a spellbook option in a widget. + */ + private String getWidgetTextForSpellbook(Rs2Spellbook spellbook) { + switch (spellbook) { + case ANCIENT: + return "Ancient Magicks"; + case LUNAR: + return "Lunar"; + case MODERN: + default: + return "Standard"; + } + } + + /** + * Finds the occult altar in the POH. + */ + private TileObject findOccultAltar() { + for (int altarId : OCCULT_ALTAR_IDS) { + TileObject altar = Rs2GameObject.findObjectById(altarId); + if (altar != null) { + return altar; + } + } + return null; + } + + private void handleTravelingState() { + // Dismiss any open dialogue first + if (Rs2Dialogue.isInDialogue()) { + if (Rs2Dialogue.hasContinue()) { + Rs2Dialogue.clickContinue(); + sleep(300, 600); + } else { + Rs2Keyboard.keyPress(KeyEvent.VK_ESCAPE); + sleep(300, 600); + } + return; + } + + if (taskDestination == null) { + log.warn("No destination set, returning to DETECTING_TASK"); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + return; + } + + // Check if we've arrived + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation != null && playerLocation.distanceTo(taskDestination) <= ARRIVAL_DISTANCE) { + log.info("Arrived at task location: {}", taskLocationName); + SlayerPlugin.setState(SlayerState.AT_LOCATION); + return; + } + + // Walk to destination if not already moving + if (!Rs2Player.isMoving()) { + log.info("Walking to task location: {}", taskLocationName); + Rs2Walker.walkTo(taskDestination); + } + } + + private void handleAtLocationState(boolean hasTask, int remaining) { + if (!hasTask || remaining <= 0) { + log.info("Task complete or no task"); + // Pick up cannon if placed + if (cannonPlaced) { + log.info("Picking up cannon after task completion"); + if (pickupCannon()) { + sleepUntil(() -> !isCannonPlacedNearby(), 3000); + cannonPlaced = false; + cannonFired = false; + } + } + resetTravelData(); + initialSetupDone = false; + + // Use POH after task if enabled + if (config.usePohPool() && config.usePohAfterTask() && shouldUsePoh()) { + log.info("Task complete, restoring at POH before getting new task"); + stateAfterPoh = config.getNewTask() ? SlayerState.GETTING_TASK : SlayerState.IDLE; + SlayerPlugin.setState(SlayerState.RESTORING_AT_POH); + return; + } + + if (config.getNewTask()) { + log.info("Transitioning to GETTING_TASK for new assignment"); + SlayerPlugin.setState(SlayerState.GETTING_TASK); + } else { + SlayerPlugin.setState(SlayerState.IDLE); + } + return; + } + + // Place cannon if using cannon and not yet placed + if (isUsingCannon && !cannonPlaced && cannonSpot != null) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation != null && playerLocation.distanceTo(cannonSpot) <= 2) { + if (hasCannonParts()) { + log.info("Placing cannon at {}", cannonSpot); + if (setupCannon()) { + // Wait for cannon assembly animation to complete (takes several seconds) + log.info("Waiting for cannon assembly..."); + sleep(6000, 7000); // Cannon assembly takes about 6 seconds + + // Assume cannon is placed if we got this far + cannonPlaced = true; + log.info("Cannon assembly complete, attempting to load..."); + + // Try to load/fire the cannon + boolean refillResult = Rs2Cannon.refill(); + log.info("Rs2Cannon.refill() returned: {}", refillResult); + if (refillResult) { + log.info("Cannon loaded and firing!"); + cannonFired = true; + } else { + log.info("Initial refill returned false - will retry via maintenance"); + } + return; + } else { + log.warn("setupCannon() returned false - couldn't interact with cannon base"); + } + } else { + log.warn("Missing cannon parts, cannot place cannon"); + isUsingCannon = false; + } + } else { + // Walk closer to cannon spot + log.debug("Walking to cannon spot, distance: {}", playerLocation != null ? playerLocation.distanceTo(cannonSpot) : "unknown"); + if (!Rs2Player.isMoving()) { + Rs2Walker.walkTo(cannonSpot); + } + return; + } + } + + // If cannon is placed but not loaded yet, try to load it + if (cannonPlaced && !cannonFired) { + log.info("Cannon placed but not loaded, attempting Rs2Cannon.refill()..."); + boolean refillResult = Rs2Cannon.refill(); + if (refillResult) { + log.info("Cannon loaded successfully!"); + cannonFired = true; + } else { + log.debug("Rs2Cannon.refill() returned false - cannon may need more time or already firing"); + } + return; // Give cannon priority before combat + } + + // Check if we should disable cannon due to low cannonballs + // This will pick up cannon, disable cannon mode, and reset destination + if (checkAndDisableCannonIfLow()) { + // Re-detect task to get new location and setup without cannon + log.info("Cannon disabled, transitioning to DETECTING_TASK to reconfigure"); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + return; + } + + // Check if we should use POH for restoration (before banking) + if (config.usePohPool() && config.usePohBeforeBanking() && needsPohRestoration()) { + log.info("Low HP, restoring at POH"); + stateAfterPoh = SlayerState.AT_LOCATION; // Return to location after POH + SlayerPlugin.setState(SlayerState.RESTORING_AT_POH); + return; + } + + // Check if we need to bank for supplies + if (config.enableAutoBanking() && needsBanking()) { + // Pick up cannon before leaving - it can't be picked up from the bank + if (cannonPlaced) { + log.info("Picking up cannon before banking (at location)"); + if (!handleCannonPickup()) { + return; // Cannon pickup in progress + } + cannonPlaced = false; + cannonFired = false; + } + // Go to POH first to heal before banking (saves supplies) + if (config.usePohPool() && shouldUsePoh()) { + log.info("Low on supplies - healing at POH before banking"); + stateAfterPoh = SlayerState.BANKING; + SlayerPlugin.setState(SlayerState.RESTORING_AT_POH); + } else { + log.info("Low on supplies, transitioning to BANKING"); + SlayerPlugin.setState(SlayerState.BANKING); + } + return; + } + + // Set up autocast before entering combat (player is stationary at task location) + setupAutoCastIfNeeded(); + + // Transition to fighting if auto combat is enabled + if (config.enableAutoCombat()) { + SlayerPlugin.setState(SlayerState.FIGHTING); + } + } + + private void handleFightingState(boolean hasTask, int remaining) { + // Check if task is complete + if (!hasTask || remaining <= 0) { + // Start the loot delay timer if not already started + if (!taskCompletedLooting) { + log.info("Task complete - looting for {} seconds before transitioning", + TASK_COMPLETE_LOOT_DELAY_MS / 1000); + taskCompletedLooting = true; + taskCompletedTime = System.currentTimeMillis(); + deactivateProfilePrayer(); + } + + // Continue looting during the delay period + long timeSinceComplete = System.currentTimeMillis() - taskCompletedTime; + if (timeSinceComplete < TASK_COMPLETE_LOOT_DELAY_MS) { + // Handle looting during the delay + if (config.enableLooting()) { + handleLooting(); + } + return; + } + + // Loot delay complete, now transition + log.info("Loot delay complete, transitioning"); + taskCompletedLooting = false; + + // Pick up cannon if placed - must complete before transitioning + if (cannonPlaced) { + if (!handleCannonPickup()) { + // Cannon pickup in progress, don't transition yet + return; + } + // Cannon picked up successfully + cannonPlaced = false; + cannonFired = false; + } + + resetTravelData(); + initialSetupDone = false; + + // Use POH after task if enabled + if (config.usePohPool() && config.usePohAfterTask() && shouldUsePoh()) { + log.info("Task complete, restoring at POH before getting new task"); + stateAfterPoh = config.getNewTask() ? SlayerState.GETTING_TASK : SlayerState.IDLE; + SlayerPlugin.setState(SlayerState.RESTORING_AT_POH); + return; + } + + if (config.getNewTask()) { + log.info("Transitioning to GETTING_TASK for new assignment"); + SlayerPlugin.setState(SlayerState.GETTING_TASK); + } else { + SlayerPlugin.setState(SlayerState.IDLE); + } + return; + } + + // Reset task completed flag if task is not complete (e.g., got a new task) + if (taskCompletedLooting) { + taskCompletedLooting = false; + } + + // IMMEDIATELY activate prayer when in FIGHTING state + // This ensures prayer is on as soon as we enter combat area + activateProfilePrayer(); + + // Ensure flicker script has the correct prayer from profile + if (activeJsonProfile != null && activeJsonProfile.hasPrayer()) { + SlayerPlugin.getFlickerScript().setActivePrayer(activeJsonProfile.getParsedPrayer()); + } + + // Handle drinking potions EARLY - before combat checks + // This ensures super antifire, combat potions, etc. are active + handlePotions(); + + // Handle fungicide spray refill (for zygomite tasks) + handleFungicideRefill(); + + // Handle eating food BEFORE POH check + // This ensures we eat food first, only use POH if still low HP after eating + boolean ateFood = handleEating(); + + // Handle cannon maintenance (repair and refill) + if (cannonPlaced) { + handleCannonMaintenance(); + } + + // Check if we should disable cannon due to low cannonballs + // This will pick up cannon, disable cannon mode, and reset destination + if (checkAndDisableCannonIfLow()) { + // Re-detect task to get new location and setup without cannon + log.info("Cannon disabled mid-task, transitioning to DETECTING_TASK to reconfigure"); + deactivateProfilePrayer(); + SlayerPlugin.setState(SlayerState.DETECTING_TASK); + return; + } + + // Check if we should use POH for restoration (before banking) + // Skip POH if we just ate food - give it time to heal before teleporting + // POH acts as a "panic teleport" if HP drops dangerously low even after eating + if (!ateFood && config.usePohPool() && config.usePohBeforeBanking() && needsPohRestoration()) { + log.info("Low HP - using POH as panic teleport"); + deactivateProfilePrayer(); + stateAfterPoh = SlayerState.TRAVELING; // Return to task after POH + SlayerPlugin.setState(SlayerState.RESTORING_AT_POH); + return; + } + + // Check if we need to bank + if (config.enableAutoBanking() && needsBanking()) { + // Pick up cannon before leaving - it can't be picked up from the bank + if (cannonPlaced) { + log.info("Picking up cannon before banking"); + if (!handleCannonPickup()) { + return; // Cannon pickup in progress + } + cannonPlaced = false; + cannonFired = false; + } + deactivateProfilePrayer(); + // Go to POH first to heal before banking (saves supplies) + if (config.usePohPool() && shouldUsePoh()) { + log.info("Low on supplies - healing at POH before banking"); + stateAfterPoh = SlayerState.BANKING; + SlayerPlugin.setState(SlayerState.RESTORING_AT_POH); + } else { + log.info("Low on supplies, transitioning to BANKING"); + SlayerPlugin.setState(SlayerState.BANKING); + } + return; + } + + // Handle looting (combat check is done inside handleLooting based on forceLoot setting) + if (config.enableLooting()) { + handleLooting(); + } + + // Handle combat + handleCombat(); + } + + /** + * Handles eating food when HP is low. + * @return true if food was eaten, false otherwise + */ + private boolean handleEating() { + double healthPercent = Rs2Player.getHealthPercentage(); + if (healthPercent < config.eatAtHealthPercent()) { + List food = Rs2Inventory.getInventoryFood(); + if (!food.isEmpty()) { + Rs2ItemModel foodItem = food.get(0); + if (Rs2Inventory.interact(foodItem, "Eat")) { + log.info("Eating {} at {}% HP", foodItem.getName(), healthPercent); + return true; + } + } + } + return false; + } + + private void handlePotions() { + // Only drink one potion per tick - return after each successful drink + + // Handle prayer potions + if (config.prayerFlickStyle() != PrayerFlickStyle.OFF || config.drinkPrayerAt() > 0) { + int prayerPoints = Rs2Player.getBoostedSkillLevel(Skill.PRAYER); + if (prayerPoints < config.drinkPrayerAt()) { + Rs2ItemModel prayerPotion = Rs2Inventory.get(item -> + item != null && item.getName() != null && + (item.getName().toLowerCase().contains("prayer potion") || + item.getName().toLowerCase().contains("super restore"))); + if (prayerPotion != null) { + if (Rs2Inventory.interact(prayerPotion, "Drink")) { + log.info("Drinking {} at {} prayer points", prayerPotion.getName(), prayerPoints); + return; // One potion per tick + } + } + } + } + + // Handle combat potions (super combat, divine, ranging, magic, etc.) + if (config.useCombatPotions()) { + // Check melee potions - uses Attack skill + int attackLevel = Rs2Player.getRealSkillLevel(Skill.ATTACK); + int boostedAttack = Rs2Player.getBoostedSkillLevel(Skill.ATTACK); + if (boostedAttack <= attackLevel) { + Rs2ItemModel meleePotion = Rs2Inventory.get(item -> + item != null && item.getName() != null && + (item.getName().toLowerCase().contains("super combat") || + item.getName().toLowerCase().contains("divine super combat") || + item.getName().toLowerCase().contains("super attack"))); + if (meleePotion != null) { + if (Rs2Inventory.interact(meleePotion, "Drink")) { + log.info("Drinking {} (attack boost worn off)", meleePotion.getName()); + return; // One potion per tick + } + } + } + + // Check ranging potions - uses Ranged skill + int rangedLevel = Rs2Player.getRealSkillLevel(Skill.RANGED); + int boostedRanged = Rs2Player.getBoostedSkillLevel(Skill.RANGED); + if (boostedRanged <= rangedLevel) { + Rs2ItemModel rangingPotion = Rs2Inventory.get(item -> + item != null && item.getName() != null && + (item.getName().toLowerCase().contains("ranging potion") || + item.getName().toLowerCase().contains("divine ranging") || + item.getName().toLowerCase().contains("bastion potion") || + item.getName().toLowerCase().contains("divine bastion"))); + if (rangingPotion != null) { + if (Rs2Inventory.interact(rangingPotion, "Drink")) { + log.info("Drinking {} (ranged boost worn off)", rangingPotion.getName()); + return; // One potion per tick + } + } + } + + // Check magic potions - uses Magic skill + int magicLevel = Rs2Player.getRealSkillLevel(Skill.MAGIC); + int boostedMagic = Rs2Player.getBoostedSkillLevel(Skill.MAGIC); + if (boostedMagic <= magicLevel) { + Rs2ItemModel magicPotion = Rs2Inventory.get(item -> + item != null && item.getName() != null && + (item.getName().toLowerCase().contains("magic potion") || + item.getName().toLowerCase().contains("divine magic") || + item.getName().toLowerCase().contains("battlemage potion") || + item.getName().toLowerCase().contains("divine battlemage"))); + if (magicPotion != null) { + if (Rs2Inventory.interact(magicPotion, "Drink")) { + log.info("Drinking {} (magic boost worn off)", magicPotion.getName()); + return; // One potion per tick + } + } + } + } + + // Handle antipoison when poisoned - check poison status first + // Poison varp: 0 = not poisoned, >0 and <1000000 = poisoned, >1000000 = venomed + int poisonVarp = Microbot.getClient().getVarpValue(102); + if (poisonVarp > 0 && poisonVarp < 1000000) { + // Actually poisoned - try to cure + if (Rs2Player.drinkAntiPoisonPotion()) { + log.info("Drank antipoison (poison varp={})", poisonVarp); + return; // One potion per tick + } else { + // Fallback: manually check for poison and drink antidote + if (handleAntipoison()) { + return; // One potion per tick + } + } + } + + // Handle antivenom when venomed (poison varp > 1000000) + if (poisonVarp > 1000000) { + if (handleAntivenom()) { + return; // One potion per tick + } + } + + // Handle goading potions (from JSON profile) - for burst/barrage tasks + // Rs2Player.drinkGoadingPotion() checks if goaded buff has worn off + if (activeJsonProfile != null && activeJsonProfile.shouldUseGoading()) { + if (Rs2Player.drinkGoadingPotion()) { + log.info("Drank goading potion (task profile requires goading)"); + return; // One potion per tick + } + } + + // Handle super antifire (from JSON profile) - for dragon tasks + if (activeJsonProfile != null && activeJsonProfile.isSuperAntifire()) { + handleSuperAntifire(); + } + } + + /** + * Handles refilling the fungicide spray during zygomite tasks. + * The auto-spray slayer perk consumes charges automatically, so we just + * need to use fungicide on the spray when charges get low. + */ + private void handleFungicideRefill() { + // Find fungicide spray in inventory + Rs2ItemModel spray = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().startsWith("fungicide spray")); + + if (spray == null) { + return; // No spray in inventory, not a zygomite task + } + + // Parse charge count from name (e.g., "Fungicide spray 5") + int charges = parseFungicideCharges(spray.getName()); + if (charges > 2) { + return; // Spray still has enough charges + } + + // Find fungicide refill in inventory + Rs2ItemModel fungicide = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().equalsIgnoreCase("fungicide")); + + if (fungicide == null) { + log.debug("Fungicide spray low ({} charges) but no fungicide refills in inventory", charges); + return; + } + + log.info("Refilling fungicide spray ({} charges)", charges); + Rs2Inventory.combine(fungicide, spray); + sleep(600, 900); + } + + /** + * Parses the charge count from a fungicide spray item name. + * Expected format: "Fungicide spray X" where X is 0-10. + */ + private int parseFungicideCharges(String name) { + if (name == null) return -1; + try { + String[] parts = name.split(" "); + return Integer.parseInt(parts[parts.length - 1]); + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Varbit for antifire protection. + * 0 = no protection, >0 = protected (ticks remaining) + */ + private static final int ANTIFIRE_VARBIT = 3981; + private static final int SUPER_ANTIFIRE_VARBIT = 6101; + + /** + * Handles drinking super antifire potions when protection wears off. + * Checks the antifire varbit to see if we still have protection. + */ + private void handleSuperAntifire() { + // Check if we have super antifire protection active + int superAntifireTimer = Microbot.getVarbitValue(SUPER_ANTIFIRE_VARBIT); + int regularAntifireTimer = Microbot.getVarbitValue(ANTIFIRE_VARBIT); + + // If we have protection, no need to drink + if (superAntifireTimer > 0 || regularAntifireTimer > 0) { + return; + } + + // Try extended super antifire first (longer duration) + Rs2ItemModel extendedSuperAntifire = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("extended super antifire")); + + if (extendedSuperAntifire != null) { + if (Rs2Inventory.interact(extendedSuperAntifire, "Drink")) { + log.info("Drinking {} for dragon protection", extendedSuperAntifire.getName()); + } + return; + } + + // Fall back to regular super antifire + Rs2ItemModel superAntifire = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("super antifire") && + !item.getName().toLowerCase().contains("extended")); + + if (superAntifire != null) { + if (Rs2Inventory.interact(superAntifire, "Drink")) { + log.info("Drinking {} for dragon protection", superAntifire.getName()); + } + return; + } + + // Fall back to regular antifire if no super antifire available + Rs2ItemModel antifire = Rs2Inventory.get(item -> + item != null && item.getName() != null && + (item.getName().toLowerCase().contains("antifire") || + item.getName().toLowerCase().contains("anti-fire")) && + !item.getName().toLowerCase().contains("super")); + + if (antifire != null) { + if (Rs2Inventory.interact(antifire, "Drink")) { + log.info("Drinking {} for dragon protection (no super antifire)", antifire.getName()); + } + } else { + log.warn("No antifire potions in inventory for dragon task!"); + } + } + + /** + * Handles drinking antivenom when envenomed. + * Uses poison varbit value > 1000000 to detect venom (poison is < 1000000). + * Supports antivenom, antivenom+, and anti-venom+ potions. + * @return true if a potion was drunk, false otherwise + */ + private boolean handleAntivenom() { + // Check if envenomed - venom is poison varbit > 1000000 + int poisonVarp = Microbot.getClient().getVarpValue(102); // VarPlayerID.POISON + if (poisonVarp <= 1000000) { + // Not venomed - if still poisoned, let antipoison handle it + return false; + } + + // Try antivenom potions first + Rs2ItemModel antivenomPotion = Rs2Inventory.get(item -> + item != null && item.getName() != null && + (item.getName().toLowerCase().contains("anti-venom") || + item.getName().toLowerCase().contains("antivenom"))); + + if (antivenomPotion != null) { + if (Rs2Inventory.interact(antivenomPotion, "Drink")) { + log.info("Drinking {} (venomed)", antivenomPotion.getName()); + return true; + } + } else { + // Antidote++ can provide some venom protection + Rs2ItemModel antidotePotion = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("antidote++")); + + if (antidotePotion != null) { + if (Rs2Inventory.interact(antidotePotion, "Drink")) { + log.info("Drinking {} (venomed, no antivenom)", antidotePotion.getName()); + return true; + } + } else { + log.warn("Venomed but no antivenom potions in inventory!"); + } + } + return false; + } + + /** + * Varbit for poison status. + * 0 = not poisoned, >0 = poisoned (damage value) + * Values > 1000000 indicate venom (handled by handleAntivenom) + */ + private static final int POISON_VARP = 102; + + /** + * Handles drinking antipoison/antidote potions when poisoned. + * This is a fallback if Rs2Player.drinkAntiPoisonPotion() doesn't work. + * @return true if a potion was drunk, false otherwise + */ + private boolean handleAntipoison() { + // Check if poisoned (but not venomed - venom is > 1000000) + int poisonVarp = Microbot.getClient().getVarpValue(POISON_VARP); + if (poisonVarp <= 0 || poisonVarp > 1000000) { + // Not poisoned (or venomed, which is handled separately) + return false; + } + + log.info("Poisoned (varp={}), looking for antipoison/antidote", poisonVarp); + + // Try antidote++ first (best) + Rs2ItemModel antidotePlusPlus = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("antidote++")); + + if (antidotePlusPlus != null) { + if (Rs2Inventory.interact(antidotePlusPlus, "Drink")) { + log.info("Drinking {} (poisoned)", antidotePlusPlus.getName()); + return true; + } + return false; + } + + // Try antidote+ + Rs2ItemModel antidotePlus = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("antidote+") && + !item.getName().toLowerCase().contains("antidote++")); + + if (antidotePlus != null) { + if (Rs2Inventory.interact(antidotePlus, "Drink")) { + log.info("Drinking {} (poisoned)", antidotePlus.getName()); + return true; + } + return false; + } + + // Try superantipoison + Rs2ItemModel superAntipoison = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("superantipoison")); + + if (superAntipoison != null) { + if (Rs2Inventory.interact(superAntipoison, "Drink")) { + log.info("Drinking {} (poisoned)", superAntipoison.getName()); + return true; + } + return false; + } + + // Try regular antipoison + Rs2ItemModel antipoison = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("antipoison") && + !item.getName().toLowerCase().contains("super")); + + if (antipoison != null) { + if (Rs2Inventory.interact(antipoison, "Drink")) { + log.info("Drinking {} (poisoned)", antipoison.getName()); + return true; + } + return false; + } + + // Try any antidote (generic) + Rs2ItemModel antidote = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().contains("antidote")); + + if (antidote != null) { + if (Rs2Inventory.interact(antidote, "Drink")) { + log.info("Drinking {} (poisoned)", antidote.getName()); + return true; + } + return false; + } + + log.warn("Poisoned but no antipoison/antidote potions in inventory!"); + return false; + } + + private static final int MIN_ARROW_STACK = 9; // Loot 10+ arrows + private static final int MIN_RUNE_STACK = 1; // Loot 2+ runes + + private void handleLooting() { + // Delay between looting to avoid spam + if (System.currentTimeMillis() - lastLootTime < LOOT_DELAY_MS) { + return; + } + + // Don't loot if inventory is full (unless we can eat for space) + if (Rs2Inventory.isFull() && !config.eatForLootSpace()) { + return; + } + + // Don't loot while in combat unless force loot is enabled + if (Rs2Combat.inCombat() && !config.forceLoot()) { + return; + } + + // Parse exclusion list + Set excludedItems = parseItemList(config.lootExcludeList()); + + // Build looting parameters + int maxPrice = config.maxLootValue() > 0 ? config.maxLootValue() : Integer.MAX_VALUE; + LootingParameters params = new LootingParameters( + config.minLootValue(), + maxPrice, + config.attackRadius(), + 1, // minQuantity + 1, // minInvSlots + config.delayedLooting(), + config.onlyLootMyItems() + ); + params.setEatFoodForSpace(config.eatForLootSpace()); + + // Build the loot engine with exclusion filter + Rs2LootEngine.Builder builder = Rs2LootEngine.with(params) + .withLootAction(groundItem -> { + // Check exclusion list before looting + if (isItemExcluded(groundItem.getName(), excludedItems)) { + log.debug("Skipping excluded item: {}", groundItem.getName()); + return; // Don't loot excluded items + } + Rs2GroundItem.coreLoot(groundItem); + }); + + // Add custom item list if using ITEM_LIST or MIXED style + LootStyle style = config.lootStyle(); + if (style == LootStyle.ITEM_LIST || style == LootStyle.MIXED) { + addCustomItemList(builder, config.lootItemList()); + } + + // Add GE price-based looting if using GE_PRICE_RANGE or MIXED style + // Also add value filtering if minLootValue > 0 to ensure all looting respects the min value + if (style == LootStyle.GE_PRICE_RANGE || style == LootStyle.MIXED || config.minLootValue() > 0) { + builder.addByValue(); + } + + // Add specific loot types based on config + if (config.lootBones() && config.buryBones()) { + builder.addBones(); // This will also bury them + } else if (config.lootBones()) { + // Just loot bones without burying - add as custom filter + addBonesWithoutBury(builder); + } + + if (config.scatterAshes()) { + builder.addAshes(); + } + + if (config.lootCoins()) { + int minCoinStack = config.minCoinStack(); + if (minCoinStack > 0) { + // Use custom filter with minimum stack size + Predicate coinFilter = gi -> { + if (gi.getName() == null) return false; + String name = gi.getName().toLowerCase(); + return name.equals("coins") && gi.getQuantity() >= minCoinStack; + }; + builder.addCustom("coins", coinFilter, null); + } else { + // Loot all coins + builder.addCoins(); + } + } + + if (config.lootUntradables()) { + builder.addUntradables(); + } + + if (config.lootArrows()) { + builder.addArrows(MIN_ARROW_STACK); + } + + if (config.lootRunes()) { + builder.addRunes(MIN_RUNE_STACK); + } + + // Execute the looting pass + boolean looted = builder.loot(); + if (looted) { + lastLootTime = System.currentTimeMillis(); + } + } + + /** + * Parses the exclusion list from config into a set of lowercase item names. + */ + private Set parseItemList(String csvNames) { + Set items = new HashSet<>(); + if (csvNames == null || csvNames.trim().isEmpty()) { + return items; + } + + Arrays.stream(csvNames.split(",")) + .map(s -> s == null ? "" : s.trim().toLowerCase()) + .filter(s -> !s.isEmpty()) + .forEach(items::add); + + return items; + } + + /** + * Matches an item name against a pattern. + * Uses exact matching by default. Supports wildcards (*) for partial matching. + * Examples: "bones" matches only "bones", "*bones" matches "superior dragon bones", + * "dragon*" matches "dragon dagger", "*dragon*" matches "superior dragon bones". + */ + private boolean matchesItemPattern(String itemName, String pattern) { + if (pattern.equals("*")) { + return true; + } + + // Exact match when no wildcards + if (!pattern.contains("*")) { + return itemName.equals(pattern); + } + + // Convert wildcard pattern to regex + String regex = pattern + .replace(".", "\\.") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("*", ".*"); + + return itemName.matches(regex); + } + + /** + * Checks if an item name matches any entry in the exclusion list. + * Uses exact matching by default. Use wildcards (*) for partial matching. + */ + private boolean isItemExcluded(String itemName, Set excludedItems) { + if (itemName == null || excludedItems.isEmpty()) { + return false; + } + + String lowerName = itemName.trim().toLowerCase(); + for (String pattern : excludedItems) { + if (matchesItemPattern(lowerName, pattern)) { + return true; + } + } + return false; + } + + /** + * Adds custom item names from the config to the loot engine. + * Uses exact matching by default. Use wildcards (*) for partial matching. + */ + private void addCustomItemList(Rs2LootEngine.Builder builder, String csvNames) { + if (csvNames == null || csvNames.trim().isEmpty()) { + return; + } + + final Set itemPatterns = parseItemList(csvNames); + + if (itemPatterns.isEmpty()) { + return; + } + + Predicate byNames = gi -> { + final String name = gi.getName() == null ? "" : gi.getName().trim().toLowerCase(); + for (String pattern : itemPatterns) { + if (matchesItemPattern(name, pattern)) { + return true; + } + } + return false; + }; + + builder.addCustom("itemList", byNames, null); + } + + /** + * Adds bones to the loot engine without the auto-bury feature. + */ + private void addBonesWithoutBury(Rs2LootEngine.Builder builder) { + Predicate isBones = gi -> { + final String name = gi.getName() == null ? "" : gi.getName().trim().toLowerCase(); + return name.contains("bones") && !name.contains("bonemeal"); + }; + + builder.addCustom("bones", isBones, null); + } + + private void handleCombat() { + // Check if we're using AoE burst/barrage combat style + if (activeJsonProfile != null && activeJsonProfile.isAoeStyle()) { + handleAoeCombat(); + return; + } + + // Get target monster names (use variant if specified, otherwise use slayer task monsters) + List targetMonsters = getTargetMonsterNames(); + + // Log target monsters for debugging + if (targetMonsters == null || targetMonsters.isEmpty()) { + log.warn("No target monsters found for current task. Task: {}, Profile: {}", + Rs2Slayer.getSlayerTask(), + activeJsonProfile != null ? "found" : "null"); + + // Fallback: try to attack anything that's attacking us ONLY at task location + if (taskDestination != null) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation != null && playerLocation.distanceTo(taskDestination) <= config.attackRadius()) { + Rs2NpcModel anyAttacker = findAnyNpcAttackingUs(); + if (anyAttacker != null) { + log.info("Fallback: Attacking {} that is attacking us (at task location)", anyAttacker.getName()); + if (Rs2Npc.interact(anyAttacker, "Attack")) { + sleepUntil(Rs2Player::isInteracting, 1000); + } + } + } + } + return; + } + + log.debug("Target monsters: {}", targetMonsters); + + // Check for superiors first - they take priority over everything + if (config.prioritizeSuperiors()) { + Rs2NpcModel superior = findNearbySuperior(); + if (superior != null) { + log.info("Superior monster detected: {}! Attacking.", superior.getName()); + if (Rs2Npc.interact(superior, "Attack")) { + sleepUntil(Rs2Player::isInteracting, 1000); + } + return; + } + } + + // Check if we're already in combat with a living target + Actor currentInteracting = Rs2Player.getInteracting(); + if (currentInteracting != null) { + // Verify the target is actually alive - don't idle on dead NPCs + boolean targetAlive = true; + if (currentInteracting instanceof Rs2NpcModel) { + Rs2NpcModel npc = (Rs2NpcModel) currentInteracting; + targetAlive = !npc.isDead() && npc.getHealthRatio() != 0; + } + if (targetAlive) { + log.debug("In combat with: {}", currentInteracting.getName()); + return; + } + log.debug("Current target {} is dead, finding new target", currentInteracting.getName()); + } + + // Check if we're being attacked but not fighting back + boolean beingAttacked = isBeingAttacked(); + if (beingAttacked) { + // First try to find attacker from target list + Rs2NpcModel attacker = findNpcAttackingUs(targetMonsters); + if (attacker != null) { + log.info("Retaliating against target monster: {}", attacker.getName()); + if (Rs2Npc.interact(attacker, "Attack")) { + sleepUntil(Rs2Player::isInteracting, 1000); + } + return; + } + + // Fallback: attack ANY NPC that is attacking us ONLY if we're at the task location + if (taskDestination != null) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation != null && playerLocation.distanceTo(taskDestination) <= config.attackRadius()) { + Rs2NpcModel anyAttacker = findAnyNpcAttackingUs(); + if (anyAttacker != null) { + log.info("Fallback: Retaliating against {} (at task location, not in target list)", anyAttacker.getName()); + if (Rs2Npc.interact(anyAttacker, "Attack")) { + sleepUntil(Rs2Player::isInteracting, 1000); + } + return; + } + } + } + // No attacker found, fall through to find new targets proactively + } + + // Find attackable NPCs matching target monsters + // Prioritize NPCs that are already attacking us (interacting with player) + List attackableNpcs = Rs2Npc.getAttackableNpcs(true) + .filter(npc -> npc.getName() != null) + .filter(npc -> targetMonsters.stream() + .anyMatch(monster -> matchesTargetMonster(npc.getName(), monster))) + .filter(npc -> taskDestination == null || + npc.getWorldLocation().distanceTo(taskDestination) <= config.attackRadius()) + .sorted(Comparator + // First priority: NPCs already attacking us (interacting with player) + .comparingInt((Rs2NpcModel npc) -> + npc.getInteracting() == Microbot.getClient().getLocalPlayer() ? 0 : 1) + // Second priority: closest distance + .thenComparingInt(npc -> + Rs2Player.getWorldLocation().distanceTo(npc.getWorldLocation()))) + .collect(Collectors.toList()); + + if (attackableNpcs.isEmpty()) { + log.debug("No attackable slayer monsters found nearby matching: {}", targetMonsters); + + // Debug: log nearby NPCs to help diagnose + List nearbyNpcNames = Rs2Npc.getAttackableNpcs(true) + .filter(npc -> npc.getName() != null) + .filter(npc -> taskDestination == null || + npc.getWorldLocation().distanceTo(taskDestination) <= config.attackRadius()) + .map(Rs2NpcModel::getName) + .distinct() + .collect(Collectors.toList()); + if (!nearbyNpcNames.isEmpty()) { + log.info("Nearby attackable NPCs: {} (looking for: {})", nearbyNpcNames, targetMonsters); + } + + // Crash detection - check if we've been unable to find targets for too long + if (config.hopWhenCrashed() && taskDestination != null) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + boolean atTaskLocation = playerLocation != null && + playerLocation.distanceTo(taskDestination) <= config.attackRadius(); + + if (atTaskLocation) { + // Start or check crash detection timer + if (!isSearchingForTargets) { + isSearchingForTargets = true; + crashDetectionStartTime = System.currentTimeMillis(); + log.debug("Started crash detection timer at task location"); + } else { + long timeSinceStart = System.currentTimeMillis() - crashDetectionStartTime; + if (timeSinceStart >= CRASH_HOP_DELAY_MS) { + // Check cooldown + long timeSinceLastHop = System.currentTimeMillis() - lastWorldHopTime; + if (timeSinceLastHop >= WORLD_HOP_COOLDOWN_MS) { + log.info("No targets found for {} seconds - hopping worlds (likely crashed)", + CRASH_HOP_DELAY_MS / 1000); + hopToNewWorld(); + return; + } else { + log.debug("Would hop but on cooldown ({} seconds remaining)", + (WORLD_HOP_COOLDOWN_MS - timeSinceLastHop) / 1000); + } + } + } + } + } + return; + } + + // Found targets - reset crash detection timer + if (isSearchingForTargets) { + isSearchingForTargets = false; + crashDetectionStartTime = 0; + log.debug("Reset crash detection timer - found targets"); + } + + // Attack the first NPC (prioritizes those attacking us, then closest) + Rs2NpcModel target = attackableNpcs.get(0); + if (Rs2Npc.interact(target, "Attack")) { + log.info("Attacking {}", target.getName()); + sleepUntil(Rs2Player::isInteracting, 1000); + } + } + + /** + * Hops to a new world to avoid crashed spots. + * Uses the configured world list if provided, otherwise picks a random members world. + */ + private void hopToNewWorld() { + try { + // Disable prayers before hopping + deactivateProfilePrayer(); + deactivateOffensivePrayers(); + + // Get world from config list or random + int world = getHopWorld(); + if (world <= 0) { + log.warn("Could not determine a valid world to hop to"); + return; + } + log.info("Hopping to world {}", world); + + Microbot.hopToWorld(world); + + // Wait for switch world confirmation dialog + sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null, 5000); + Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); + + // Wait for hop to complete + sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING, 5000); + sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN, 10000); + + // Brief pause after hopping + sleep(1500, 2500); + + // Reset crash detection state + isSearchingForTargets = false; + crashDetectionStartTime = 0; + lastWorldHopTime = System.currentTimeMillis(); + + log.info("Successfully hopped to world {}", world); + } catch (Exception e) { + log.error("Error hopping worlds: {}", e.getMessage()); + // Reset state even on error + isSearchingForTargets = false; + crashDetectionStartTime = 0; + } + } + + /** + * Gets a world to hop to from the config list, or a random members world if list is empty. + * Avoids hopping to the current world. + */ + private int getHopWorld() { + String worldList = config.hopWorldList(); + int currentWorld = Microbot.getClient().getWorld(); + + if (worldList != null && !worldList.trim().isEmpty()) { + // Parse comma-separated world list + List worlds = Arrays.stream(worldList.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(s -> { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + log.warn("Invalid world number in hop list: {}", s); + return -1; + } + }) + .filter(w -> w > 0 && w != currentWorld) + .collect(Collectors.toList()); + + if (!worlds.isEmpty()) { + int selectedWorld = worlds.get(new Random().nextInt(worlds.size())); + log.debug("Selected world {} from config list (available: {})", selectedWorld, worlds); + return selectedWorld; + } else { + log.warn("No valid worlds in hop list (or all filtered out), falling back to random"); + } + } + + // Fallback to random members world + return Login.getRandomWorld(true, null); + } + + /** + * Checks if any NPC is currently attacking the player. + */ + private boolean isBeingAttacked() { + return Rs2Npc.getNpcsForPlayer() + .anyMatch(npc -> npc.getInteracting() == Microbot.getClient().getLocalPlayer()); + } + + /** + * Finds an NPC from the target list that is currently attacking the player. + * Used when we're "in combat" (being attacked) but not attacking back. + */ + private Rs2NpcModel findNpcAttackingUs(List targetMonsters) { + return Rs2Npc.getNpcsForPlayer() + .filter(npc -> npc.getName() != null) + .filter(npc -> npc.getInteracting() == Microbot.getClient().getLocalPlayer()) + .filter(npc -> targetMonsters.stream() + .anyMatch(monster -> matchesTargetMonster(npc.getName(), monster))) + .filter(npc -> taskDestination == null || + npc.getWorldLocation().distanceTo(taskDestination) <= config.attackRadius() + 5) + .findFirst() + .orElse(null); + } + + /** + * Finds ANY NPC that is currently attacking the player (regardless of target list). + * Used as a fallback when variant/target list doesn't match. + */ + private Rs2NpcModel findAnyNpcAttackingUs() { + return Rs2Npc.getNpcsForPlayer() + .filter(npc -> npc.getName() != null) + .filter(npc -> npc.getInteracting() == Microbot.getClient().getLocalPlayer()) + .filter(npc -> !npc.isDead()) + .findFirst() + .orElse(null); + } + + /** + * Handles AoE combat using burst/barrage spells. + * Waits for monsters to stack using goading, then casts the appropriate spell. + */ + private void handleAoeCombat() { + // Ensure we have a valid profile for AoE combat + if (activeJsonProfile == null) { + log.warn("No active profile for AoE combat"); + return; + } + + // Get the combat style and min stack size from profile + SlayerCombatStyle style = activeJsonProfile.getParsedStyle(); + if (style == null) { + log.warn("No combat style defined in profile for AoE combat"); + return; + } + + // Determine which spell to use based on style + Rs2CombatSpells spell = (style == SlayerCombatStyle.BARRAGE) + ? Rs2CombatSpells.ICE_BARRAGE + : Rs2CombatSpells.ICE_BURST; + + // Safety check: re-set autocast if it was cleared (e.g., weapon swap mid-combat) + if (Rs2Magic.getCurrentAutoCastSpell() != spell) { + log.info("Autocast was reset, re-setting to {}", spell.name()); + Rs2Combat.setAutoCastSpell(spell, false); + sleepUntil(() -> Rs2Magic.getCurrentAutoCastSpell() == spell, 3000); + return; + } + + int minStackSize = activeJsonProfile.getMinStackSize(); + if (minStackSize < 1) { + minStackSize = 3; // Default minimum + } + + // Activate protection prayer + activateProfilePrayer(); + + // If already in combat with a valid target, let autocast handle it + if (Rs2Player.isInteracting()) { + Actor currentTarget = Rs2Player.getInteracting(); + if (currentTarget != null && !currentTarget.isDead()) { + log.debug("Already in combat with {} - autocast will continue", currentTarget.getName()); + return; + } + } + + // Get target monster names (use variant if specified) + List targetMonsters = getTargetMonsterNames(); + if (targetMonsters == null || targetMonsters.isEmpty()) { + log.warn("No target monsters found for current task"); + return; + } + + // Find all nearby target monsters (including those already in combat with us) + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation == null) { + return; + } + + List nearbyMonsters = Rs2Npc.getNpcsForPlayer() + .filter(npc -> npc.getName() != null) + .filter(npc -> targetMonsters.stream() + .anyMatch(monster -> npc.getName().equalsIgnoreCase(monster))) + .filter(npc -> !npc.isDead()) + .filter(npc -> taskDestination == null || + npc.getWorldLocation().distanceTo(taskDestination) <= config.attackRadius()) + .collect(Collectors.toList()); + + if (nearbyMonsters.isEmpty()) { + log.debug("No target monsters found nearby for AoE combat"); + return; + } + + // Count monsters within AoE range (3x3 for burst, 5x5 for barrage centered on target) + int aoeRange = (style == SlayerCombatStyle.BARRAGE) ? 2 : 1; + + // Find the best target (one with most monsters stacked nearby) + Rs2NpcModel bestTarget = null; + int maxStackedCount = 0; + + for (Rs2NpcModel potentialTarget : nearbyMonsters) { + WorldPoint targetLoc = potentialTarget.getWorldLocation(); + int stackedCount = 0; + + for (Rs2NpcModel monster : nearbyMonsters) { + WorldPoint monsterLoc = monster.getWorldLocation(); + // Check if within AoE range of the potential target + if (Math.abs(targetLoc.getX() - monsterLoc.getX()) <= aoeRange && + Math.abs(targetLoc.getY() - monsterLoc.getY()) <= aoeRange && + targetLoc.getPlane() == monsterLoc.getPlane()) { + stackedCount++; + } + } + + if (stackedCount > maxStackedCount) { + maxStackedCount = stackedCount; + bestTarget = potentialTarget; + } + } + + // Check if we have enough monsters stacked + if (maxStackedCount < minStackSize) { + // If using goading, we need to attack first to trigger the aggro effect + // Goading works by attracting NPCs when you're already in combat + if (activeJsonProfile.shouldUseGoading() && bestTarget != null) { + log.info("Attacking {} to trigger goading aggro ({} monsters nearby)", + bestTarget.getName(), nearbyMonsters.size()); + if (Rs2Npc.interact(bestTarget, "Attack")) { + sleepUntil(Rs2Player::isInteracting, 1000); + } + return; + } + + log.debug("Waiting for monsters to stack: {}/{} (goading active: {})", + maxStackedCount, minStackSize, activeJsonProfile.shouldUseGoading()); + // Just wait for more monsters to come + return; + } + + if (bestTarget == null) { + log.debug("No suitable target found for AoE combat"); + return; + } + + // Attack the best target - autocast will handle spell casting + log.info("Attacking {} with autocast {} ({} monsters stacked)", + bestTarget.getName(), spell.name(), maxStackedCount); + + if (Rs2Npc.interact(bestTarget, "Attack")) { + sleepUntil(Rs2Player::isInteracting, 1000); + } + } + + /** + * Gets the list of monster names to target. + * If a variant is specified in the JSON profile, uses that instead of the slayer task monsters. + * This allows targeting specific monsters like "rune dragons" when task is "metal dragons". + */ + private List getTargetMonsterNames() { + // Check if profile specifies a variant + if (activeJsonProfile != null && activeJsonProfile.hasVariant()) { + String variant = activeJsonProfile.getVariant(); + log.debug("Using variant monster: {}", variant); + return Arrays.asList(variant); + } + + // Try standard slayer monster list first + List monsters = Rs2Slayer.getSlayerMonsters(); + if (monsters != null && !monsters.isEmpty()) { + return monsters; + } + + // Fallback: convert task name from plural to singular + // e.g., "Frost Dragons" -> "Frost dragon" + String taskName = Rs2Slayer.getSlayerTask(); + if (taskName != null && !taskName.isEmpty()) { + String singularName = convertTaskNameToNpcName(taskName); + log.debug("Using fallback NPC name: {} (from task: {})", singularName, taskName); + return Arrays.asList(singularName); + } + + return null; + } + + /** + * Returns true if using a specific variant from the profile (exact match required). + */ + private boolean isUsingVariant() { + return activeJsonProfile != null && activeJsonProfile.hasVariant(); + } + + /** + * Checks if an NPC name matches the target monster name. + * Uses exact matching for variants, broader matching for general task names. + */ + private boolean matchesTargetMonster(String npcName, String targetMonster) { + if (npcName == null || targetMonster == null) { + return false; + } + String npcLower = npcName.toLowerCase(); + String targetLower = targetMonster.toLowerCase(); + + if (isUsingVariant()) { + // Exact match for variants (e.g., "baby blue dragon" should NOT match "blue dragon") + return npcLower.equals(targetLower); + } else { + // Broader match for general task names + // e.g., task "blue dragons" with monster "blue dragon" should match NPC "Blue dragon" + return npcLower.contains(targetLower) || targetLower.contains(npcLower); + } + } + + /** + * Converts a slayer task name (often plural) to the likely NPC name (often singular). + * Examples: "Frost Dragons" -> "Frost dragon", "Abyssal Demons" -> "Abyssal demon" + */ + private String convertTaskNameToNpcName(String taskName) { + if (taskName == null || taskName.isEmpty()) { + return taskName; + } + + String result = taskName.trim(); + + // Remove trailing 's' for simple plurals (Dragons -> Dragon) + if (result.endsWith("s") && !result.endsWith("ss")) { + result = result.substring(0, result.length() - 1); + } + + // Convert to title case (first letter uppercase, rest lowercase for each word) + // "FROST DRAGON" -> "Frost dragon", "frost dragon" -> "Frost dragon" + String[] words = result.split(" "); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < words.length; i++) { + if (words[i].length() > 0) { + if (i == 0) { + // First word: capitalize first letter + sb.append(Character.toUpperCase(words[i].charAt(0))); + if (words[i].length() > 1) { + sb.append(words[i].substring(1).toLowerCase()); + } + } else { + // Subsequent words: all lowercase + sb.append(" ").append(words[i].toLowerCase()); + } + } + } + + return sb.toString(); + } + + /** + * Finds a nearby superior slayer monster that is attackable + * @return The superior NPC if found, null otherwise + */ + private Rs2NpcModel findNearbySuperior() { + return Rs2Npc.getAttackableNpcs(true) + .filter(npc -> npc.getName() != null) + .filter(npc -> SUPERIOR_MONSTERS.contains(npc.getName())) + .filter(npc -> taskDestination == null || + npc.getWorldLocation().distanceTo(taskDestination) <= config.attackRadius() + 5) // Slightly larger radius for superiors + .min(Comparator.comparingInt(npc -> + Rs2Player.getWorldLocation().distanceTo(npc.getWorldLocation()))) + .orElse(null); + } + + /** + * Checks if we need to bank based on food, potion, and cannonball thresholds + */ + private boolean needsBanking() { + // Check food threshold + int foodThreshold = config.foodThreshold(); + if (foodThreshold > 0) { + int foodCount = Rs2Inventory.getInventoryFood().size(); + if (foodCount < foodThreshold) { + log.info("Food count ({}) below threshold ({})", foodCount, foodThreshold); + return true; + } + } + + // Check prayer potion threshold + int potionThreshold = config.potionThreshold(); + if (potionThreshold > 0) { + int prayerDoses = getPrayerPotionDoses(); + if (prayerDoses < potionThreshold) { + log.info("Prayer potion doses ({}) below threshold ({})", prayerDoses, potionThreshold); + return true; + } + } + + // Check cannonball threshold if using cannon + if (isUsingCannon && config.enableCannon()) { + int cannonballThreshold = config.cannonballThreshold(); + if (cannonballThreshold > 0) { + int cannonballCount = getCannonballCount(); + if (cannonballCount < cannonballThreshold) { + log.info("Cannonball count ({}) below threshold ({})", cannonballCount, cannonballThreshold); + return true; + } + } + } + + // Check fungicide spray - bank if spray is empty and no refills remain + Rs2ItemModel fungicideSpray = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().toLowerCase().startsWith("fungicide spray")); + if (fungicideSpray != null) { + int charges = parseFungicideCharges(fungicideSpray.getName()); + boolean hasRefills = Rs2Inventory.get(item -> + item != null && item.getName() != null && + item.getName().equalsIgnoreCase("fungicide")) != null; + if (charges <= 0 && !hasRefills) { + log.info("Fungicide spray empty and no refills remaining"); + return true; + } + } + + return false; + } + + /** + * Gets total prayer potion doses in inventory (prayer potions and super restores) + */ + private int getPrayerPotionDoses() { + return Rs2Inventory.items() + .filter(item -> item != null && item.getName() != null) + .filter(item -> { + String name = item.getName().toLowerCase(); + // Only count prayer-restoring potions + return (name.contains("prayer potion") || + name.contains("super restore") || + name.contains("sanfew serum") || + name.contains("blighted super restore")) && + (name.contains("(4)") || name.contains("(3)") || + name.contains("(2)") || name.contains("(1)")); + }) + .mapToInt(item -> { + String name = item.getName(); + if (name.contains("(4)")) return 4; + if (name.contains("(3)")) return 3; + if (name.contains("(2)")) return 2; + if (name.contains("(1)")) return 1; + return 0; + }) + .sum(); + } + + /** + * Updates the BreakHandler lock state based on current slayer state. + * Only allows breaks during safe states (BANKING, TRAVELING). + */ + private void updateBreakHandlerLock(SlayerState currentState) { + try { + // Only allow breaks during safe states + boolean allowBreak = (currentState == SlayerState.BANKING || + currentState == SlayerState.TRAVELING); + + // Set lock state (true = locked/no breaks, false = unlocked/can break) + boolean shouldLock = !allowBreak; + + // Only update if state changed to avoid spam + if (BreakHandlerScript.isLockState() != shouldLock) { + BreakHandlerScript.setLockState(shouldLock); + log.debug("BreakHandler lock state set to {} (state: {})", shouldLock, currentState); + } + } catch (Exception e) { + // BreakHandler might not be enabled, ignore errors + log.debug("BreakHandler not available: {}", e.getMessage()); + } + } + + private void resetTravelData() { + taskDestination = null; + taskLocationName = ""; + SlayerPlugin.setCurrentLocation(""); + // Reset cannon state + isUsingCannon = false; + cannonSpot = null; + // Reset active JSON profile + activeJsonProfile = null; + // Reset task completion loot delay + taskCompletedLooting = false; + // Note: cannonPlaced is NOT reset here - we track actual cannon placement separately + } + + // ==================== Prayer Methods ==================== + + /** + * Sets up autocast spell for AoE (burst/barrage) profiles. + * Should be called when the player is stationary (at POH after spellbook swap, + * or at task location before combat) to avoid UI interaction being interrupted by movement. + */ + private void setupAutoCastIfNeeded() { + if (activeJsonProfile == null) { + log.debug("setupAutoCastIfNeeded: No active profile"); + return; + } + if (!activeJsonProfile.isAoeStyle()) { + log.debug("setupAutoCastIfNeeded: Profile style is not AOE (style: {})", activeJsonProfile.getStyle()); + return; + } + + SlayerCombatStyle style = activeJsonProfile.getParsedStyle(); + if (style == null) { + log.warn("setupAutoCastIfNeeded: Could not parse combat style from profile"); + return; + } + + Rs2CombatSpells spell = (style == SlayerCombatStyle.BARRAGE) + ? Rs2CombatSpells.ICE_BARRAGE + : Rs2CombatSpells.ICE_BURST; + + Rs2CombatSpells currentSpell = Rs2Magic.getCurrentAutoCastSpell(); + log.debug("setupAutoCastIfNeeded: Current autocast={}, desired={}", + currentSpell != null ? currentSpell.name() : "NONE", spell.name()); + + if (currentSpell != spell) { + log.info("Setting autocast to {} (current: {})", spell.name(), + currentSpell != null ? currentSpell.name() : "NONE"); + + // Use Rs2Combat API (requires Combat Options tab to have a hotkey assigned) + Rs2Combat.setAutoCastSpell(spell, false); + sleepUntil(() -> Rs2Magic.getCurrentAutoCastSpell() == spell, 5000); + + Rs2CombatSpells afterSpell = Rs2Magic.getCurrentAutoCastSpell(); + if (afterSpell == spell) { + log.info("Autocast set to {} successfully", spell.name()); + } else { + log.warn("Failed to set autocast to {} - current is now: {}. " + + "Ensure Combat Options tab has a hotkey assigned in OSRS settings.", spell.name(), + afterSpell != null ? afterSpell.name() : "NONE"); + } + } else { + log.debug("Autocast already set to {}", spell.name()); + } + } + + /** + * Activates the appropriate prayer based on the active JSON profile and prayer style. + * For flicking modes, prayer activation is handled by the flicker script. + */ + private void activateProfilePrayer() { + PrayerFlickStyle style = config.prayerFlickStyle(); + + // Activate offensive prayers if enabled (independent of protection prayer style) + activateOffensivePrayer(); + + // Don't activate protection prayer if style is OFF + if (style == PrayerFlickStyle.OFF) { + return; + } + + // Flicking modes are handled by FlickerScript, not here + if (style == PrayerFlickStyle.LAZY_FLICK || + style == PrayerFlickStyle.PERFECT_LAZY_FLICK || + style == PrayerFlickStyle.MIXED_LAZY_FLICK) { + return; + } + + // Check JSON profile for prayer (ALWAYS_ON mode) + if (activeJsonProfile != null && activeJsonProfile.hasPrayer()) { + SlayerPrayer slayerPrayer = activeJsonProfile.getParsedPrayer(); + if (slayerPrayer != null && slayerPrayer != SlayerPrayer.NONE) { + activatePrayer(slayerPrayer); + } + } else if (style == PrayerFlickStyle.ALWAYS_ON) { + log.warn("ALWAYS_ON mode but no profile prayer found - activeJsonProfile={}, hasPrayer={}", + activeJsonProfile != null ? "present" : "null", + activeJsonProfile != null ? activeJsonProfile.hasPrayer() : "N/A"); + } + } + + /** + * Helper method to activate a specific SlayerPrayer + */ + private void activatePrayer(SlayerPrayer slayerPrayer) { + if (slayerPrayer.getPrayer() != null) { + Rs2PrayerEnum prayer = slayerPrayer.getPrayer(); + if (!Rs2Prayer.isPrayerActive(prayer)) { + Rs2Prayer.toggle(prayer, true); + log.debug("Activated prayer: {}", slayerPrayer.getDisplayName()); + } + } + } + + /** + * Deactivates profile-specific prayers. + */ + private void deactivateProfilePrayer() { + PrayerFlickStyle style = config.prayerFlickStyle(); + + if (style == PrayerFlickStyle.OFF) { + return; + } + + // Disable JSON profile prayer + if (activeJsonProfile != null && activeJsonProfile.hasPrayer()) { + SlayerPrayer slayerPrayer = activeJsonProfile.getParsedPrayer(); + if (slayerPrayer != null && slayerPrayer != SlayerPrayer.NONE && slayerPrayer.getPrayer() != null) { + Rs2Prayer.toggle(slayerPrayer.getPrayer(), false); + } + } + + // Disable offensive prayers + if (config.useOffensivePrayers()) { + deactivateOffensivePrayers(); + } + } + + // Varbits for prayer unlock status + private static final int RIGOUR_UNLOCKED_VARBIT = 5451; + private static final int AUGURY_UNLOCKED_VARBIT = 5452; + + /** + * Activates the best offensive prayer based on the current combat style. + * Uses the style from JSON profile if available, otherwise defaults to melee. + */ + private void activateOffensivePrayer() { + if (!config.useOffensivePrayers()) { + return; + } + + SlayerCombatStyle combatStyle = SlayerCombatStyle.MELEE; + if (activeJsonProfile != null && activeJsonProfile.hasStyle()) { + combatStyle = activeJsonProfile.getParsedStyle(); + } + + int prayerLevel = Microbot.getClient().getRealSkillLevel(Skill.PRAYER); + + switch (combatStyle) { + case MELEE: + activateBestMeleePrayer(prayerLevel); + break; + case RANGED: + activateBestRangedPrayer(prayerLevel); + break; + case MAGIC: + case BURST: + case BARRAGE: + activateBestMagicPrayer(prayerLevel); + break; + } + } + + /** + * Activates the best melee offensive prayer available. + * Priority: Piety > Chivalry > Ultimate Strength + Incredible Reflexes + */ + private void activateBestMeleePrayer(int prayerLevel) { + // Piety requires 70 Prayer and Knight Waves completion + if (prayerLevel >= 70) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.PIETY)) { + Rs2Prayer.toggle(Rs2PrayerEnum.PIETY, true); + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.PIETY)) { + log.debug("Activated Piety"); + return; + } + } else { + return; // Already active + } + } + + // Chivalry requires 60 Prayer and Knight Waves completion + if (prayerLevel >= 60) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.CHIVALRY)) { + Rs2Prayer.toggle(Rs2PrayerEnum.CHIVALRY, true); + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.CHIVALRY)) { + log.debug("Activated Chivalry"); + return; + } + } else { + return; + } + } + + // Fallback to separate strength + attack prayers + if (prayerLevel >= 31 && !Rs2Prayer.isPrayerActive(Rs2PrayerEnum.ULTIMATE_STRENGTH)) { + Rs2Prayer.toggle(Rs2PrayerEnum.ULTIMATE_STRENGTH, true); + } + if (prayerLevel >= 34 && !Rs2Prayer.isPrayerActive(Rs2PrayerEnum.INCREDIBLE_REFLEXES)) { + Rs2Prayer.toggle(Rs2PrayerEnum.INCREDIBLE_REFLEXES, true); + } + } + + /** + * Activates the best ranged offensive prayer available. + * Priority: Rigour > Eagle Eye > Hawk Eye > Sharp Eye + */ + private void activateBestRangedPrayer(int prayerLevel) { + // Rigour requires 74 Prayer and Rigour scroll + if (prayerLevel >= 74) { + boolean rigourUnlocked = Microbot.getVarbitValue(RIGOUR_UNLOCKED_VARBIT) == 1; + if (rigourUnlocked) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.RIGOUR)) { + Rs2Prayer.toggle(Rs2PrayerEnum.RIGOUR, true); + log.debug("Activated Rigour"); + } + return; + } + } + + // Eagle Eye requires 44 Prayer + if (prayerLevel >= 44) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.EAGLE_EYE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.EAGLE_EYE, true); + log.debug("Activated Eagle Eye"); + } + return; + } + + // Hawk Eye requires 26 Prayer + if (prayerLevel >= 26) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.HAWK_EYE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.HAWK_EYE, true); + log.debug("Activated Hawk Eye"); + } + return; + } + + // Sharp Eye requires 8 Prayer + if (prayerLevel >= 8) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.SHARP_EYE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.SHARP_EYE, true); + log.debug("Activated Sharp Eye"); + } + } + } + + /** + * Activates the best magic offensive prayer available. + * Priority: Augury > Mystic Might > Mystic Lore > Mystic Will + */ + private void activateBestMagicPrayer(int prayerLevel) { + // Augury requires 77 Prayer and Augury scroll + if (prayerLevel >= 77) { + boolean auguryUnlocked = Microbot.getVarbitValue(AUGURY_UNLOCKED_VARBIT) == 1; + if (auguryUnlocked) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.AUGURY)) { + Rs2Prayer.toggle(Rs2PrayerEnum.AUGURY, true); + log.debug("Activated Augury"); + } + return; + } + } + + // Mystic Might requires 45 Prayer + if (prayerLevel >= 45) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.MYSTIC_MIGHT)) { + Rs2Prayer.toggle(Rs2PrayerEnum.MYSTIC_MIGHT, true); + log.debug("Activated Mystic Might"); + } + return; + } + + // Mystic Lore requires 27 Prayer + if (prayerLevel >= 27) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.MYSTIC_LORE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.MYSTIC_LORE, true); + log.debug("Activated Mystic Lore"); + } + return; + } + + // Mystic Will requires 9 Prayer + if (prayerLevel >= 9) { + if (!Rs2Prayer.isPrayerActive(Rs2PrayerEnum.MYSTIC_WILL)) { + Rs2Prayer.toggle(Rs2PrayerEnum.MYSTIC_WILL, true); + log.debug("Activated Mystic Will"); + } + } + } + + /** + * Deactivates all offensive prayers. + */ + private void deactivateOffensivePrayers() { + // Melee prayers + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.PIETY)) { + Rs2Prayer.toggle(Rs2PrayerEnum.PIETY, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.CHIVALRY)) { + Rs2Prayer.toggle(Rs2PrayerEnum.CHIVALRY, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.ULTIMATE_STRENGTH)) { + Rs2Prayer.toggle(Rs2PrayerEnum.ULTIMATE_STRENGTH, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.INCREDIBLE_REFLEXES)) { + Rs2Prayer.toggle(Rs2PrayerEnum.INCREDIBLE_REFLEXES, false); + } + + // Ranged prayers + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.RIGOUR)) { + Rs2Prayer.toggle(Rs2PrayerEnum.RIGOUR, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.EAGLE_EYE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.EAGLE_EYE, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.HAWK_EYE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.HAWK_EYE, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.SHARP_EYE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.SHARP_EYE, false); + } + + // Magic prayers + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.AUGURY)) { + Rs2Prayer.toggle(Rs2PrayerEnum.AUGURY, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.MYSTIC_MIGHT)) { + Rs2Prayer.toggle(Rs2PrayerEnum.MYSTIC_MIGHT, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.MYSTIC_LORE)) { + Rs2Prayer.toggle(Rs2PrayerEnum.MYSTIC_LORE, false); + } + if (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.MYSTIC_WILL)) { + Rs2Prayer.toggle(Rs2PrayerEnum.MYSTIC_WILL, false); + } + } + + /** + * Checks if prayer should be active based on style and combat state + */ + private boolean shouldPrayerBeActive() { + PrayerFlickStyle style = config.prayerFlickStyle(); + + switch (style) { + case OFF: + return false; + case ALWAYS_ON: + return true; + default: + // Flicking modes handled separately + return false; + } + } + + // ==================== Cannon Helper Methods ==================== + + /** + * Checks if we should use cannon for the given task + */ + private boolean shouldUseCannon(String taskName) { + if (taskName == null || taskName.isEmpty()) { + return false; + } + + // MASTER TOGGLE: Config enableCannon must be ON for any cannon usage + if (!config.enableCannon()) { + return false; + } + + // Check JSON profile for cannon setting (per-task preference) + if (activeJsonProfile != null) { + if (activeJsonProfile.isCannon()) { + // Profile wants cannon - verify we have a spot for it + if (CannonSpot.hasSpotForTask(taskName) || activeJsonProfile.hasCannonLocation()) { + log.info("Using cannon for '{}' based on JSON profile", taskName); + return true; + } else { + log.warn("JSON profile specifies cannon for '{}' but no cannon spot/location found", taskName); + return false; + } + } else { + // Profile exists but cannon is disabled for this task + return false; + } + } + + // No profile found - fall back to cannon task list filter + // Check if task has a predefined cannon spot + if (!CannonSpot.hasSpotForTask(taskName)) { + return false; + } + + // Check if cannon task list is configured (optional filter) + String cannonTaskList = config.cannonTaskList(); + if (cannonTaskList != null && !cannonTaskList.isEmpty()) { + String taskLower = taskName.toLowerCase().trim(); + boolean isInList = Arrays.stream(cannonTaskList.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(cannonTask -> taskLower.contains(cannonTask) || cannonTask.contains(taskLower)); + if (!isInList) { + log.info("Task '{}' not in cannon task list, skipping cannon", taskName); + return false; + } + } + + // Check if we have an inventory setup configured in the profile + if (activeJsonProfile == null || !activeJsonProfile.hasSetup()) { + log.warn("Cannon enabled but no inventory setup configured in profile"); + return false; + } + + return true; + } + + /** + * Gets the active inventory setup based on task profile, cannon usage, and config. + * If cannon is being used, automatically tries to find a cannon variant by appending + * "-cannon" suffix (e.g., "melee" -> "melee-cannon", "ranged" -> "ranged-cannon"). + * Checks JSON profile first, then falls back to text-based profile. + */ + private net.runelite.client.plugins.microbot.inventorysetups.InventorySetup getActiveInventorySetup() { + String setupName = null; + + // Check JSON profile for setup name + if (activeJsonProfile != null && activeJsonProfile.hasSetup()) { + setupName = activeJsonProfile.getSetup(); + log.debug("Using setup name '{}' from JSON profile", setupName); + } + + // If we have a setup name from a profile, try to find it + if (setupName != null && !setupName.isEmpty()) { + // If using cannon, try to find cannon variant first (e.g., "melee" -> "melee-cannon") + if (isUsingCannon && !setupName.toLowerCase().endsWith("-cannon")) { + String cannonName = setupName + "-cannon"; + net.runelite.client.plugins.microbot.inventorysetups.InventorySetup cannonSetup = findInventorySetupByName(cannonName); + if (cannonSetup != null) { + log.info("Using cannon inventory setup '{}' (cannon enabled)", cannonName); + return cannonSetup; + } + log.debug("Cannon setup '{}' not found, falling back to '{}'", cannonName, setupName); + } + + // Try to find the inventory setup by name + net.runelite.client.plugins.microbot.inventorysetups.InventorySetup profileSetup = findInventorySetupByName(setupName); + if (profileSetup != null) { + log.debug("Using inventory setup '{}' from profile", setupName); + return profileSetup; + } else { + log.warn("Could not find inventory setup '{}' specified in profile", setupName); + } + } + + // No profile setup found - inventory setup must be defined in slayer-profiles.json + log.warn("No inventory setup found. Please configure 'setup' in slayer-profiles.json for this task."); + return null; + } + + /** + * Finds an inventory setup by name from the inventory setups plugin + */ + private net.runelite.client.plugins.microbot.inventorysetups.InventorySetup findInventorySetupByName(String name) { + if (name == null || name.isEmpty()) { + return null; + } + + // Look up from InventorySetupsPlugin by name + var allSetups = MInventorySetupsPlugin.getInventorySetups(); + if (allSetups != null) { + var found = allSetups.stream() + .filter(java.util.Objects::nonNull) + .filter(setup -> setup.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + if (found != null) { + log.debug("Found inventory setup '{}' from InventorySetupsPlugin", name); + return found; + } + } + + log.warn("Inventory setup '{}' not found. " + + "Please create an inventory setup with this name in the Inventory Setups plugin.", name); + return null; + } + + /** + * Checks if player has cannon parts in inventory + */ + private boolean hasCannonParts() { + return Rs2Inventory.hasItem("Cannon base") && + Rs2Inventory.hasItem("Cannon stand") && + Rs2Inventory.hasItem("Cannon barrels") && + Rs2Inventory.hasItem("Cannon furnace"); + } + + /** + * Gets the count of cannonballs in inventory + */ + private int getCannonballCount() { + Rs2ItemModel cannonballs = Rs2Inventory.get("Cannonball"); + if (cannonballs != null) { + return cannonballs.getQuantity(); + } + // Also check for granite cannonballs + Rs2ItemModel graniteCannonballs = Rs2Inventory.get("Granite cannonball"); + if (graniteCannonballs != null) { + return graniteCannonballs.getQuantity(); + } + return 0; + } + + /** + * Checks if we should disable cannon mode due to low cannonballs. + * If cannonballs are below threshold, this method: + * 1. Picks up the cannon if placed + * 2. Disables cannon mode + * 3. Resets travel data so we switch to non-cannon location + * 4. Returns true to indicate we should bank/re-setup + * + * @return true if cannon mode was disabled and we need to re-setup + */ + private boolean checkAndDisableCannonIfLow() { + if (!isUsingCannon || !config.enableCannon()) { + return false; + } + + int cannonballCount = getCannonballCount(); + int threshold = config.cannonballThreshold(); + + // Only disable if we're actually below threshold + if (threshold > 0 && cannonballCount < threshold) { + log.info("Cannonballs ({}) below threshold ({}), disabling cannon mode", cannonballCount, threshold); + + // Pick up cannon if placed + if (cannonPlaced) { + log.info("Picking up cannon due to low cannonballs"); + if (pickupCannon()) { + sleepUntil(() -> !isCannonPlacedNearby(), 3000); + } + cannonPlaced = false; + cannonFired = false; + } + + // Disable cannon mode + isUsingCannon = false; + cannonFired = false; + cannonSpot = null; + + // Reset travel data so we recalculate with non-cannon location + taskDestination = null; + taskLocationName = ""; + + log.info("Cannon mode disabled, will switch to non-cannon setup and location"); + return true; + } + + return false; + } + + /** + * Handles cannon maintenance - repair if broken, refill if needed. + * Based on AIOFighter's simple approach: just repair and refill. + */ + private void handleCannonMaintenance() { + log.debug("Cannon maintenance - cannonPlaced: {}, cannonFired: {}", cannonPlaced, cannonFired); + + // Try to repair first (if broken) + if (Rs2Cannon.repair()) { + log.info("Repaired cannon"); + cannonFired = true; // Cannon is working if we repaired it + return; + } + + // Refill cannon with cannonballs - this also starts the cannon firing + if (Rs2Cannon.refill()) { + log.info("Refilled cannon with cannonballs"); + cannonFired = true; // Mark as fired since refill starts the cannon + } + } + + /** + * Sets up the cannon by using the cannon base from inventory + * @return true if setup was initiated successfully + */ + private boolean setupCannon() { + // Use cannon base from inventory to start setup + if (Rs2Inventory.interact("Cannon base", "Set-up")) { + log.info("Setting up cannon..."); + return true; + } + return false; + } + + /** + * Starts the cannon by refilling it with cannonballs. + * Uses Rs2Cannon.refill() which handles loading cannonballs and starting the cannon. + * @return true if cannon was refilled/started successfully + */ + private boolean fireCannon() { + // Just use Rs2Cannon.refill() - this handles loading cannonballs and starting the cannon + // This is the same approach used by AIOFighter + if (Rs2Cannon.refill()) { + log.info("Cannon refilled and started via Rs2Cannon.refill()"); + return true; + } + log.debug("Rs2Cannon.refill() returned false - cannon may already be full or not placed"); + return false; + } + + /** + * Handles the full cannon pickup process including walking to it if needed. + * @return true if cannon was successfully picked up (no longer exists), false if still in progress + */ + private boolean handleCannonPickup() { + // Check if cannon still exists + var cannon = Rs2GameObject.findObject("Dwarf multicannon", true, 50, false, Rs2Player.getWorldLocation()); + if (cannon == null) { + log.info("Cannon already picked up or not found"); + return true; // Cannon doesn't exist, we're done + } + + WorldPoint cannonLocation = cannon.getWorldLocation(); + int distance = Rs2Player.getWorldLocation().distanceTo(cannonLocation); + + // If too far, walk to cannon first + if (distance > 5) { + log.info("Walking to cannon to pick it up (distance: {})", distance); + Rs2Walker.walkTo(cannonLocation, 2); + return false; // Still in progress + } + + // Try to pick up cannon + if (Rs2GameObject.interact(cannon, "Pick-up")) { + log.info("Picking up cannon..."); + sleepUntil(() -> !isCannonPlacedNearby(), 5000); + + // Check if pickup was successful + if (!isCannonPlacedNearby()) { + log.info("Cannon picked up successfully"); + return true; + } else { + log.warn("Cannon pickup may have failed, retrying next tick"); + return false; + } + } + + log.warn("Failed to interact with cannon"); + return false; // Will retry next tick + } + + /** + * Picks up the cannon by interacting with it + * @return true if pickup was initiated successfully + */ + private boolean pickupCannon() { + if (Rs2GameObject.interact("Dwarf multicannon", "Pick-up")) { + log.info("Picking up cannon..."); + return true; + } + return false; + } + + /** + * Checks if a cannon is placed nearby + * @return true if cannon object is found nearby + */ + private boolean isCannonPlacedNearby() { + return Rs2GameObject.findObject("Dwarf multicannon", true, 10, false, Rs2Player.getWorldLocation()) != null; + } + + /** + * Resets cannon state + */ + private void resetCannonState() { + isUsingCannon = false; + cannonPlaced = false; + cannonFired = false; + cannonSpot = null; + } + + // ==================== End Cannon Helper Methods ==================== + + /** + * Stops the plugin and logs out the player. + * Used when we can't complete a required action (e.g., can't skip a task on skip list) + */ + private void stopAndLogout() { + log.info("Stopping plugin and logging out..."); + + // Disable prayers + deactivateProfilePrayer(); + + // Set state to idle + SlayerPlugin.setState(SlayerState.IDLE); + + // Wait briefly for combat to end if we're fighting + if (Rs2Combat.inCombat()) { + log.info("Waiting for combat to end before logout..."); + sleepUntil(() -> !Rs2Combat.inCombat(), 10000); + } + + // Logout + Rs2Player.logout(); + sleep(1000, 2000); + + // Stop the script + shutdown(); + } + + @Override + public void shutdown() { + log.info("Slayer script shutting down"); + // Unlock break handler on shutdown + try { + BreakHandlerScript.setLockState(false); + } catch (Exception e) { + // BreakHandler might not be enabled, ignore + } + // Disable prayers on shutdown + deactivateProfilePrayer(); + // Note: We don't pick up cannon on shutdown - user may want to keep it + resetTravelData(); + resetSkipState(); + resetPohState(); + resetCannonState(); + initialSetupDone = false; + super.shutdown(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerState.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerState.java new file mode 100644 index 0000000000..38a676ec36 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerState.java @@ -0,0 +1,25 @@ +package net.runelite.client.plugins.microbot.slayer; + +public enum SlayerState { + IDLE("Idle"), + GETTING_TASK("Getting Task"), + SKIPPING_TASK("Skipping Task"), + BLOCKING_TASK("Blocking Task"), + DETECTING_TASK("Detecting Task"), + RESTORING_AT_POH("Restoring at POH"), + BANKING("Banking"), + SWAPPING_SPELLBOOK("Swapping Spellbook"), + TRAVELING("Traveling"), + AT_LOCATION("At Location"), + FIGHTING("Fighting"); + + private final String displayName; + + SlayerState(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/AttackStyle.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/AttackStyle.java new file mode 100644 index 0000000000..14b6046a0e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/AttackStyle.java @@ -0,0 +1,11 @@ +package net.runelite.client.plugins.microbot.slayer.combat; + +/** + * Combat attack styles for determining protection prayer. + */ +public enum AttackStyle { + MAGE, + RANGED, + MELEE, + MIXED +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerDodgeScript.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerDodgeScript.java new file mode 100644 index 0000000000..5f94f53f54 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerDodgeScript.java @@ -0,0 +1,121 @@ +package net.runelite.client.plugins.microbot.slayer.combat; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Projectile; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Script that handles dodging AOE projectile attacks. + * Used for monsters like Adamant Dragons that have poison pool attacks. + */ +@Slf4j +public class SlayerDodgeScript extends Script { + + public final List projectiles = new ArrayList<>(); + private boolean enabled = false; + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public boolean run() { + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!enabled) return; + if (!Microbot.isLoggedIn()) return; + + // Remove expired projectiles + int cycle = Microbot.getClient().getGameCycle(); + projectiles.removeIf(projectile -> cycle >= projectile.getEndCycle()); + + if (projectiles.isEmpty()) return; + + // Get all dangerous points from projectiles + WorldPoint[] dangerousPoints = projectiles.stream() + .map(Projectile::getTargetPoint) + .toArray(WorldPoint[]::new); + + WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); + + // Check if any projectile is targeting near the player + boolean inDanger = projectiles.stream() + .anyMatch(p -> p.getTargetPoint().distanceTo(playerLocation) < 2); + + if (inDanger) { + WorldPoint safePoint = calculateSafePoint(playerLocation, dangerousPoints); + if (safePoint != null && !safePoint.equals(playerLocation)) { + log.info("Dodging projectile! Moving from {} to {}", playerLocation, safePoint); + Rs2Walker.walkFastCanvas(safePoint); + } + } + + } catch (Exception e) { + log.debug("Dodge script error: {}", e.getMessage()); + } + }, 0, 200, TimeUnit.MILLISECONDS); + return true; + } + + /** + * Calculates the nearest safe point away from all dangerous projectile targets. + * A safe point is at least 2 tiles away from all dangerous points. + */ + private WorldPoint calculateSafePoint(WorldPoint playerLocation, WorldPoint[] dangerousPoints) { + int searchRadius = 5; + int minDistance = Integer.MAX_VALUE; + WorldPoint bestPoint = null; + + // Search in a square area around the player + for (int dx = -searchRadius; dx <= searchRadius; dx++) { + for (int dy = -searchRadius; dy <= searchRadius; dy++) { + if (dx == 0 && dy == 0) continue; // Skip current position + + WorldPoint candidate = playerLocation.dx(dx).dy(dy); + + // Check if this point is safe (at least 2 tiles away from all dangerous points) + boolean isSafe = Arrays.stream(dangerousPoints) + .allMatch(p -> p.distanceTo(candidate) >= 2); + + if (isSafe) { + int distanceToPlayer = candidate.distanceTo(playerLocation); + // Prefer closer safe tiles that are reachable + if (distanceToPlayer < minDistance && Rs2Tile.isTileReachable(candidate)) { + minDistance = distanceToPlayer; + bestPoint = candidate; + } + } + } + } + + return bestPoint != null ? bestPoint : playerLocation; + } + + /** + * Adds a projectile to track for dodging. + * Only projectiles targeting a WorldPoint (not an actor) should be added. + */ + public void addProjectile(Projectile projectile) { + if (projectile != null && projectile.getTargetActor() == null) { + projectiles.add(projectile); + } + } + + @Override + public void shutdown() { + projectiles.clear(); + super.shutdown(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerFlickerScript.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerFlickerScript.java new file mode 100644 index 0000000000..0d52fb6d15 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerFlickerScript.java @@ -0,0 +1,249 @@ +package net.runelite.client.plugins.microbot.slayer.combat; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.events.NpcDespawned; +import net.runelite.client.plugins.microbot.slayer.PrayerFlickStyle; +import net.runelite.client.plugins.microbot.slayer.SlayerConfig; +import net.runelite.client.plugins.microbot.slayer.SlayerPrayer; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcManager; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.prayer.Rs2Prayer; +import net.runelite.client.plugins.microbot.util.prayer.Rs2PrayerEnum; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * Handles prayer flicking for slayer combat. + * Based on AIOFighter's FlickerScript. + */ +@Slf4j +public class SlayerFlickerScript { + + // Default attack speed (4 ticks = 2.4 seconds) used when NPC stats are unavailable + private static final int DEFAULT_ATTACK_SPEED = 4; + + public static final AtomicReference> currentMonstersAttackingUsRef = new AtomicReference<>(new ArrayList<>()); + private final AtomicReference> npcsRef = new AtomicReference<>(new ArrayList<>()); + + private SlayerConfig config; + private SlayerPrayer activePrayer = null; + private int tickToFlick = 0; + private boolean isRunning = false; + + public void start(SlayerConfig config) { + this.config = config; + this.isRunning = true; + log.info("SlayerFlickerScript started"); + } + + public void stop() { + this.isRunning = false; + currentMonstersAttackingUsRef.set(new ArrayList<>()); + activePrayer = null; + log.info("SlayerFlickerScript stopped"); + } + + /** + * Sets the active prayer for flicking + */ + public void setActivePrayer(SlayerPrayer prayer) { + if (this.activePrayer != prayer) { + this.activePrayer = prayer; + log.info("FlickerScript: Active prayer set to {}", prayer != null ? prayer.getDisplayName() : "null"); + } + } + + /** + * Clears active prayer (for task reset) + */ + public void clearActiveProfile() { + this.activePrayer = null; + } + + public boolean isRunning() { + return isRunning; + } + + /** + * Called every game tick from the plugin + */ + public void onGameTick() { + if (!isRunning || config == null) { + return; + } + + PrayerFlickStyle style = config.prayerFlickStyle(); + if (style != PrayerFlickStyle.LAZY_FLICK && + style != PrayerFlickStyle.PERFECT_LAZY_FLICK && + style != PrayerFlickStyle.MIXED_LAZY_FLICK) { + return; + } + + // Determine flick timing based on style + switch (style) { + case LAZY_FLICK: + tickToFlick = 1; + break; + case PERFECT_LAZY_FLICK: + tickToFlick = 0; + break; + case MIXED_LAZY_FLICK: + tickToFlick = Rs2Random.betweenInclusive(0, 1); + break; + } + + // Update NPC snapshot + npcsRef.set(Rs2Npc.getNpcsForPlayer().collect(Collectors.toList())); + + // Remove monsters that no longer exist + currentMonstersAttackingUsRef.updateAndGet(monsters -> { + List updated = new ArrayList<>(monsters); + updated.removeIf(monster -> + npcsRef.get().stream().noneMatch(npc -> npc.getIndex() == monster.npc.getIndex()) + ); + return updated; + }); + + // Process monsters: decrement timers, flick prayer on exact tick, reset attacks + for (SlayerMonster monster : currentMonstersAttackingUsRef.get()) { + monster.lastAttack--; + if (monster.lastAttack == tickToFlick && !monster.npc.isDead()) { + Rs2PrayerEnum prayer = getPrayerToActivate(monster); + if (prayer != null && !Rs2Prayer.isPrayerActive(prayer)) { + log.debug("Flicking {} on (monster: {}, tick: {})", prayer, monster.npc.getName(), monster.lastAttack); + Rs2Prayer.toggle(prayer, true); + } + } + resetLastAttack(false); + } + + // Disable prayers if no monsters attacking and not interacting + if (currentMonstersAttackingUsRef.get().isEmpty() && + !Rs2Player.isInteracting() && + (Rs2Prayer.isPrayerActive(Rs2PrayerEnum.PROTECT_MELEE) || + Rs2Prayer.isPrayerActive(Rs2PrayerEnum.PROTECT_MAGIC) || + Rs2Prayer.isPrayerActive(Rs2PrayerEnum.PROTECT_RANGE) || + Rs2Prayer.isQuickPrayerEnabled())) { + Rs2Prayer.disableAllPrayers(); + } + } + + /** + * Gets the appropriate prayer to activate for a specific monster. + * Uses the profile prayer if set, otherwise determines from the monster's attack style. + */ + private Rs2PrayerEnum getPrayerToActivate(SlayerMonster monster) { + if (activePrayer != null && activePrayer != SlayerPrayer.NONE) { + return activePrayer.getPrayer(); + } + + if (monster.attackStyle != null) { + switch (monster.attackStyle) { + case MAGE: return Rs2PrayerEnum.PROTECT_MAGIC; + case RANGED: return Rs2PrayerEnum.PROTECT_RANGE; + default: return Rs2PrayerEnum.PROTECT_MELEE; + } + } + return Rs2PrayerEnum.PROTECT_MELEE; + } + + /** + * Called when an NPC despawns + */ + public void onNpcDespawned(NpcDespawned npcDespawned) { + if (!isRunning) return; + + int idx = npcDespawned.getNpc().getIndex(); + currentMonstersAttackingUsRef.updateAndGet(monsters -> + monsters.stream() + .filter(m -> m.npc.getIndex() != idx) + .collect(Collectors.toList()) + ); + } + + /** + * Called when player is hit - resets attack timers + */ + public void onPlayerHit() { + if (!isRunning) return; + + // Reset attack timers when we get hit (helps re-sync timing) + resetLastAttack(true); + // Note: Don't disable prayers here - if we got hit, we want protection ON + } + + public void resetLastAttack(boolean forceReset) { + List npcs = npcsRef.get(); + currentMonstersAttackingUsRef.updateAndGet(monsters -> { + List updated = new ArrayList<>(monsters); + for (Rs2NpcModel npc : npcs) { + SlayerMonster m = updated.stream() + .filter(x -> x.npc.getIndex() == npc.getIndex()) + .findFirst() + .orElse(null); + + String style = Rs2NpcManager.getAttackStyle(npc.getId()); + // Default to MELEE if no attack style data available + AttackStyle attackStyle = (style != null) ? mapToAttackStyle(style) : AttackStyle.MELEE; + + if (m != null) { + int attackSpeed = m.getAttackSpeed(); + if (forceReset && (m.lastAttack <= 0 || npc.getAnimation() != -1)) { + m.lastAttack = attackSpeed; + } + if ((!npc.isDead() && m.lastAttack <= 0) || + (!npc.isDead() && npc.getGraphic() != -1)) { + m.lastAttack = (npc.getGraphic() != -1 && attackStyle != AttackStyle.MELEE) + ? attackSpeed - 2 + tickToFlick + : attackSpeed; + } + if (m.lastAttack <= -attackSpeed / 2) { + updated.remove(m); + } + } else if (!npc.isDead()) { + var stats = Rs2NpcManager.getStats(npc.getId()); + // Create monster even without stats, using defaults + SlayerMonster toAdd = new SlayerMonster(npc, stats); + toAdd.attackStyle = attackStyle; + updated.add(toAdd); + log.debug("Added monster to tracking: {} (stats: {}, style: {})", + npc.getName(), stats != null ? "available" : "using defaults", attackStyle); + } + } + return updated; + }); + } + + /** + * Maps a string attack style to AttackStyle enum + */ + private AttackStyle mapToAttackStyle(String style) { + if (style == null || style.isEmpty()) { + return AttackStyle.MELEE; + } + + String lowerCaseStyle = style.toLowerCase(); + + if (lowerCaseStyle.contains(",")) { + String[] styles = lowerCaseStyle.split(","); + lowerCaseStyle = styles[0].trim(); + } + + if (lowerCaseStyle.contains("melee") || lowerCaseStyle.contains("crush") || + lowerCaseStyle.contains("slash") || lowerCaseStyle.contains("stab")) { + return AttackStyle.MELEE; + } else if (lowerCaseStyle.contains("magic")) { + return AttackStyle.MAGE; + } else if (lowerCaseStyle.contains("ranged")) { + return AttackStyle.RANGED; + } else { + return AttackStyle.MELEE; + } + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerHighAlchScript.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerHighAlchScript.java new file mode 100644 index 0000000000..f40641a5b9 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerHighAlchScript.java @@ -0,0 +1,221 @@ +package net.runelite.client.plugins.microbot.slayer.combat; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.slayer.SlayerConfig; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.item.Rs2ExplorersRing; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; +import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.magic.Rs2Spells; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.settings.Rs2Settings; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import net.runelite.client.util.Text; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Handles high alching for slayer combat. + * Only alchs items that are explicitly on the whitelist. + */ +@Slf4j +public class SlayerHighAlchScript extends Script { + + // Randomized interval between alchs (in ticks) + private static final int MIN_TICKS = (int) Math.ceil(30.0 / 0.6); + private static final int MAX_TICKS = (int) Math.floor(45.0 / 0.6); + + private int lastAlchCheckTick = -1; + private int nextAlchIntervalTicks = 0; + private Set alchWhitelist = Collections.emptySet(); + private Set alchExcludeList = Collections.emptySet(); + + public boolean run(SlayerConfig config) { + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!Microbot.isLoggedIn() || !super.run() || !config.enableHighAlch()) { + return; + } + + // Parse whitelist and exclusion list from config + updateWhitelist(config.highAlchItemList()); + updateExcludeList(config.highAlchExcludeList()); + + if (alchWhitelist.isEmpty()) { + return; + } + + // Find items in inventory that match the whitelist and are not excluded + List itemsToAlch = Rs2Inventory.getList(item -> + isOnWhitelist(item.getName()) && !isExcluded(item.getName())); + + if (itemsToAlch.isEmpty()) { + return; + } + + // Check tick-based cooldown + int currentTick = Microbot.getClient().getTickCount(); + if (lastAlchCheckTick != -1 && currentTick - lastAlchCheckTick < nextAlchIntervalTicks) { + return; + } + + lastAlchCheckTick = currentTick; + nextAlchIntervalTicks = Rs2Random.nextInt(MIN_TICKS, MAX_TICKS, 1.5, true); + + // Try Explorer's Ring first (free alchs) + if (Rs2ExplorersRing.hasRing() && Rs2ExplorersRing.hasCharges()) { + for (Rs2ItemModel item : itemsToAlch) { + if (!isRunning()) { + break; + } + + log.debug("High alching {} with Explorer's Ring", item.getName()); + Rs2ExplorersRing.highAlch(item); + } + Rs2ExplorersRing.closeInterface(); + } else if (Rs2Magic.canCast(Rs2Spells.HIGH_LEVEL_ALCHEMY)) { + // Use normal high alchemy spell + for (Rs2ItemModel item : itemsToAlch) { + if (!isRunning()) { + break; + } + + log.debug("High alching {} with spell", item.getName()); + Rs2Magic.alch(item); + + // Handle high value item warning + if (item.getHaPrice() > Rs2Settings.getMinimumItemValueAlchemyWarning()) { + sleepUntil(() -> Rs2Widget.hasWidget("Proceed to cast High Alchemy on it")); + if (Rs2Widget.hasWidget("Proceed to cast High Alchemy on it")) { + Rs2Keyboard.keyPress('1'); + } + } + Rs2Player.waitForAnimation(); + } + } + } catch (Exception ex) { + Microbot.logStackTrace(this.getClass().getSimpleName(), ex); + } + }, 0, 600, TimeUnit.MILLISECONDS); + return true; + } + + /** + * Updates the whitelist from the config string. + */ + private void updateWhitelist(String csvNames) { + if (csvNames == null || csvNames.trim().isEmpty()) { + alchWhitelist = Collections.emptySet(); + return; + } + + alchWhitelist = Arrays.stream(csvNames.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Text::standardize) + .collect(Collectors.toCollection(HashSet::new)); + } + + /** + * Updates the exclusion list from the config string. + */ + private void updateExcludeList(String csvNames) { + if (csvNames == null || csvNames.trim().isEmpty()) { + alchExcludeList = Collections.emptySet(); + return; + } + + alchExcludeList = Arrays.stream(csvNames.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Text::standardize) + .collect(Collectors.toCollection(HashSet::new)); + } + + /** + * Checks if an item name matches any entry in the exclusion list. + * Supports wildcards (*) for pattern matching. + */ + private boolean isExcluded(String itemName) { + if (itemName == null || alchExcludeList.isEmpty()) { + return false; + } + + String normalizedName = Text.standardize(itemName); + if (normalizedName.isEmpty()) { + return false; + } + + for (String pattern : alchExcludeList) { + if (matchesPattern(normalizedName, pattern)) { + return true; + } + } + return false; + } + + /** + * Checks if an item name matches any entry in the whitelist. + * Supports wildcards (*) for pattern matching. + */ + private boolean isOnWhitelist(String itemName) { + if (itemName == null || alchWhitelist.isEmpty()) { + return false; + } + + String normalizedName = Text.standardize(itemName); + if (normalizedName.isEmpty()) { + return false; + } + + for (String pattern : alchWhitelist) { + if (matchesPattern(normalizedName, pattern)) { + return true; + } + } + return false; + } + + /** + * Matches an item name against a pattern that may contain wildcards (*). + */ + private boolean matchesPattern(String itemName, String pattern) { + if (pattern.equals("*")) { + return true; + } + + // Exact match + if (!pattern.contains("*")) { + return itemName.equals(pattern); + } + + // Convert wildcard pattern to regex + // Escape regex special chars except *, then convert * to .* + String regex = pattern + .replace(".", "\\.") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("*", ".*"); + + return itemName.matches(regex); + } + + @Override + public void shutdown() { + super.shutdown(); + alchWhitelist = Collections.emptySet(); + alchExcludeList = Collections.emptySet(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerMonster.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerMonster.java new file mode 100644 index 0000000000..3ac43ca716 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/combat/SlayerMonster.java @@ -0,0 +1,36 @@ +package net.runelite.client.plugins.microbot.slayer.combat; + +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcManager; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcStats; + +/** + * Tracks a monster attacking the player for prayer flicking purposes. + */ +public class SlayerMonster { + // Default attack speed (4 ticks = 2.4 seconds) used when NPC data is unavailable + private static final int DEFAULT_ATTACK_SPEED = 4; + + public Rs2NpcModel npc; + public Rs2NpcStats rs2NpcStats; + public AttackStyle attackStyle = AttackStyle.MELEE; // Default to melee + public int lastAttack = 0; + + public SlayerMonster(Rs2NpcModel npc, Rs2NpcStats rs2NpcStats) { + this.npc = npc; + this.rs2NpcStats = rs2NpcStats; + int attackSpeed = Rs2NpcManager.getAttackSpeed(npc.getId()); + // Use default if attack speed is invalid + this.lastAttack = (attackSpeed > 0) ? attackSpeed : DEFAULT_ATTACK_SPEED; + } + + /** + * Gets the attack speed, using default if stats unavailable + */ + public int getAttackSpeed() { + if (rs2NpcStats != null && rs2NpcStats.getAttackSpeed() > 0) { + return rs2NpcStats.getAttackSpeed(); + } + return DEFAULT_ATTACK_SPEED; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerProfileManager.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerProfileManager.java new file mode 100644 index 0000000000..2fd7fe1b43 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerProfileManager.java @@ -0,0 +1,548 @@ +package net.runelite.client.plugins.microbot.slayer.profile; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * Manages loading and saving of slayer task profiles from JSON. + * Profiles are stored in ~/.runelite/slayer-profiles.json + */ +@Slf4j +public class SlayerProfileManager { + + private static final String PROFILE_FILE_NAME = "slayer-profiles.json"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private Map profiles = new HashMap<>(); + private Path profilePath; + + public SlayerProfileManager() { + this.profilePath = RuneLite.RUNELITE_DIR.toPath().resolve(PROFILE_FILE_NAME); + } + + /** + * Loads profiles from the JSON file. + * Creates a default file with examples if it doesn't exist. + */ + public void loadProfiles() { + if (!Files.exists(profilePath)) { + log.info("Slayer profiles file not found, creating default at: {}", profilePath); + createDefaultProfiles(); + return; + } + + try (Reader reader = Files.newBufferedReader(profilePath, StandardCharsets.UTF_8)) { + Type type = new TypeToken>() {}.getType(); + profiles = GSON.fromJson(reader, type); + + if (profiles == null) { + profiles = new HashMap<>(); + } + + log.info("Loaded {} slayer task profiles from {}", profiles.size(), profilePath); + + // Log loaded profiles + for (Map.Entry entry : profiles.entrySet()) { + SlayerTaskProfileJson p = entry.getValue(); + log.debug(" {} -> setup={}, prayer={}, cannon={}, antipoison={}, antivenom={}", + entry.getKey(), p.getSetup(), p.getPrayer(), p.isCannon(), + p.isAntipoison(), p.isAntivenom()); + } + + } catch (Exception e) { + log.error("Failed to load slayer profiles from {}: {}", profilePath, e.getMessage()); + profiles = new HashMap<>(); + } + } + + /** + * Saves current profiles to the JSON file. + */ + public void saveProfiles() { + try (Writer writer = Files.newBufferedWriter(profilePath, StandardCharsets.UTF_8)) { + GSON.toJson(profiles, writer); + log.info("Saved {} slayer task profiles to {}", profiles.size(), profilePath); + } catch (Exception e) { + log.error("Failed to save slayer profiles to {}: {}", profilePath, e.getMessage()); + } + } + + /** + * Creates a default profiles file with slayer task configurations. + */ + private void createDefaultProfiles() { + profiles = new HashMap<>(); + + // Aberrant spectres + SlayerTaskProfileJson aberrantSpectres = new SlayerTaskProfileJson(); + aberrantSpectres.setSetup("melee"); + aberrantSpectres.setStyle("melee"); + aberrantSpectres.setPrayer("pmage"); + aberrantSpectres.setLocation("slayer tower aberrant spectres"); + profiles.put("aberrant spectres", aberrantSpectres); + + // Abyssal demons - Venator bow + SlayerTaskProfileJson abyssalDemons = new SlayerTaskProfileJson(); + abyssalDemons.setSetup("ven"); + abyssalDemons.setStyle("range"); + abyssalDemons.setPrayer("pmelee"); + abyssalDemons.setLocation("catacombs abyssal demons"); + profiles.put("abyssal demons", abyssalDemons); + + // Ankou - Venator bow + SlayerTaskProfileJson ankou = new SlayerTaskProfileJson(); + ankou.setSetup("ven"); + ankou.setStyle("range"); + ankou.setPrayer("pmelee"); + ankou.setGoading(true); + ankou.setLocation("catacombs ankou"); + profiles.put("ankou", ankou); + + // Araxytes - Venator bow + SlayerTaskProfileJson araxytes = new SlayerTaskProfileJson(); + araxytes.setSetup("ven"); + araxytes.setStyle("range"); + araxytes.setPrayer("pmage"); + araxytes.setLocation("araxyte cave"); + profiles.put("araxytes", araxytes); + + // Aviansies + SlayerTaskProfileJson aviansies = new SlayerTaskProfileJson(); + aviansies.setSetup("ranged"); + aviansies.setStyle("ranged"); + aviansies.setPrayer("prange"); + aviansies.setLocation("god wars aviansies"); + profiles.put("aviansies", aviansies); + + // Basilisks + SlayerTaskProfileJson basilisks = new SlayerTaskProfileJson(); + basilisks.setSetup("basilisk"); + basilisks.setStyle("melee"); + basilisks.setPrayer("pmelee"); + basilisks.setLocation("fremennik basilisk"); + profiles.put("basilisks", basilisks); + + // Black demons + SlayerTaskProfileJson blackDemons = new SlayerTaskProfileJson(); + blackDemons.setSetup("melee"); + blackDemons.setStyle("melee"); + blackDemons.setPrayer("pmelee"); + blackDemons.setCannon(true); + blackDemons.setLocation("catacombs black demons"); + blackDemons.setCannonLocation("chasm black demons"); + profiles.put("black demons", blackDemons); + + // Black dragons + SlayerTaskProfileJson blackDragons = new SlayerTaskProfileJson(); + blackDragons.setSetup("dragon"); + blackDragons.setStyle("melee"); + blackDragons.setPrayer("none"); + blackDragons.setLocation("taverley baby black dragons"); + profiles.put("black dragons", blackDragons); + + // Bloodveld - Venator bow + SlayerTaskProfileJson bloodveld = new SlayerTaskProfileJson(); + bloodveld.setSetup("ven"); + bloodveld.setStyle("range"); + bloodveld.setPrayer("pmelee"); + bloodveld.setVariant("mutated bloodveld"); + bloodveld.setLocation("catacombs mutated bloodvelds"); + bloodveld.setCannonLocation("catacombs mutated bloodvelds"); + profiles.put("bloodveld", bloodveld); + + // Blue dragons + SlayerTaskProfileJson blueDragons = new SlayerTaskProfileJson(); + blueDragons.setSetup("blue dragon"); + blueDragons.setStyle("melee"); + blueDragons.setPrayer("none"); + blueDragons.setVariant("baby blue dragon"); + blueDragons.setLocation("taverley baby blue dragons"); + profiles.put("blue dragons", blueDragons); + + // Cave horrors + SlayerTaskProfileJson caveHorrors = new SlayerTaskProfileJson(); + caveHorrors.setSetup("melee"); + caveHorrors.setStyle("melee"); + caveHorrors.setPrayer("pmelee"); + caveHorrors.setAntipoison(true); + caveHorrors.setLocation("mos leharmless cave horrors"); + profiles.put("cave horrors", caveHorrors); + + // Cave kraken + SlayerTaskProfileJson caveKraken = new SlayerTaskProfileJson(); + caveKraken.setSetup("magic"); + caveKraken.setStyle("magic"); + caveKraken.setPrayer("pmage"); + caveKraken.setLocation("kraken cove"); + profiles.put("cave kraken", caveKraken); + + // Cockatrice + SlayerTaskProfileJson cockatrice = new SlayerTaskProfileJson(); + cockatrice.setSetup("melee"); + cockatrice.setStyle("melee"); + cockatrice.setLocation("fremennik cockatrice"); + profiles.put("cockatrice", cockatrice); + + // Dagannoth - Venator bow + SlayerTaskProfileJson dagannoth = new SlayerTaskProfileJson(); + dagannoth.setSetup("ven"); + dagannoth.setStyle("range"); + dagannoth.setPrayer("pmelee"); + dagannoth.setLocation("catacombs dagannoth"); + dagannoth.setCannonLocation("lighthouse dagannoths"); + profiles.put("dagannoth", dagannoth); + + // Dark beasts + SlayerTaskProfileJson darkBeasts = new SlayerTaskProfileJson(); + darkBeasts.setSetup("stab"); + darkBeasts.setStyle("melee"); + darkBeasts.setPrayer("pmage"); + darkBeasts.setLocation("mourner tunnels dark beasts"); + profiles.put("dark beasts", darkBeasts); + + // Drakes + SlayerTaskProfileJson drakes = new SlayerTaskProfileJson(); + drakes.setSetup("melee"); + drakes.setStyle("melee"); + drakes.setPrayer("pmelee"); + drakes.setLocation("karuulm drakes"); + profiles.put("drakes", drakes); + + // Dust devils - Burst + SlayerTaskProfileJson dustDevils = new SlayerTaskProfileJson(); + dustDevils.setSetup("burst"); + dustDevils.setStyle("burst"); + dustDevils.setPrayer("pmage"); + dustDevils.setGoading(true); + dustDevils.setMinStackSize(4); + dustDevils.setLocation("catacombs dust devils"); + profiles.put("dust devils", dustDevils); + + // Elves + SlayerTaskProfileJson elves = new SlayerTaskProfileJson(); + elves.setSetup("melee"); + elves.setStyle("melee"); + elves.setPrayer("prange"); + elves.setLocation("lletya elves"); + profiles.put("elves", elves); + + // Fire giants - Venator bow + SlayerTaskProfileJson fireGiants = new SlayerTaskProfileJson(); + fireGiants.setSetup("ven"); + fireGiants.setStyle("range"); + fireGiants.setPrayer("pmelee"); + fireGiants.setLocation("catacombs fire giants"); + fireGiants.setCannonLocation("waterfall fire giants"); + profiles.put("fire giants", fireGiants); + + // Fossil Island Wyverns + SlayerTaskProfileJson fossilWyverns = new SlayerTaskProfileJson(); + fossilWyverns.setSetup("wyvern"); + fossilWyverns.setStyle("melee"); + fossilWyverns.setPrayer("pmelee"); + fossilWyverns.setVariant("spitting wyvern"); + fossilWyverns.setLocation("fossil island wyverns"); + profiles.put("fossil island wyverns", fossilWyverns); + + // Frost dragons + SlayerTaskProfileJson frostDragons = new SlayerTaskProfileJson(); + frostDragons.setSetup("dragon"); + frostDragons.setStyle("melee"); + frostDragons.setPrayer("pmelee"); + frostDragons.setSuperAntifire(true); + frostDragons.setLocation("grimstone frost dragons"); + profiles.put("frost dragons", frostDragons); + + // Gargoyles + SlayerTaskProfileJson gargoyles = new SlayerTaskProfileJson(); + gargoyles.setSetup("melee"); + gargoyles.setStyle("melee"); + gargoyles.setPrayer("pmelee"); + gargoyles.setLocation("slayer tower gargoyles"); + profiles.put("gargoyles", gargoyles); + + // Greater demons + SlayerTaskProfileJson greaterDemons = new SlayerTaskProfileJson(); + greaterDemons.setSetup("demon"); + greaterDemons.setStyle("melee"); + greaterDemons.setPrayer("pmelee"); + greaterDemons.setLocation("catacombs greater demons"); + greaterDemons.setCannonLocation("chasm greater demons"); + profiles.put("greater demons", greaterDemons); + + // Hellhounds - Venator bow + SlayerTaskProfileJson hellhounds = new SlayerTaskProfileJson(); + hellhounds.setSetup("ven"); + hellhounds.setStyle("range"); + hellhounds.setPrayer("pmelee"); + hellhounds.setGoading(true); + hellhounds.setLocation("catacombs hellhounds"); + hellhounds.setCannonLocation("stronghold hellhounds"); + profiles.put("hellhounds", hellhounds); + + // Hydras + SlayerTaskProfileJson hydras = new SlayerTaskProfileJson(); + hydras.setSetup("ranged"); + hydras.setStyle("ranged"); + hydras.setPrayer("prange"); + hydras.setLocation("karuulm hydras"); + profiles.put("hydras", hydras); + + // Kalphite + SlayerTaskProfileJson kalphite = new SlayerTaskProfileJson(); + kalphite.setSetup("kalphite"); + kalphite.setStyle("melee"); + kalphite.setPrayer("pmelee"); + kalphite.setCannon(true); + kalphite.setAntipoison(true); + kalphite.setVariant("kalphite soldier"); + kalphite.setLocation("kalphite lair"); + kalphite.setCannonLocation("kalphite lair"); + profiles.put("kalphite", kalphite); + + // Kurask + SlayerTaskProfileJson kurask = new SlayerTaskProfileJson(); + kurask.setSetup("kurask"); + kurask.setStyle("melee"); + kurask.setPrayer("pmelee"); + kurask.setLocation("fremennik kurask"); + profiles.put("kurask", kurask); + + // Metal dragons (covers iron, steel, mithril assigned as "metal dragons") + SlayerTaskProfileJson metalDragons = new SlayerTaskProfileJson(); + metalDragons.setSetup("dragon"); + metalDragons.setStyle("melee"); + metalDragons.setPrayer("pmelee"); + metalDragons.setVariant("steel dragons"); + metalDragons.setLocation("brimhaven steel dragons"); + metalDragons.setSuperAntifire(true); + profiles.put("metal dragons", metalDragons); + + // Mutated Zygomites + SlayerTaskProfileJson mutatedZygomites = new SlayerTaskProfileJson(); + mutatedZygomites.setSetup("melee"); + mutatedZygomites.setStyle("melee"); + mutatedZygomites.setPrayer("pmelee"); + mutatedZygomites.setAntipoison(true); + mutatedZygomites.setLocation("zanaris"); + profiles.put("mutated zygomites", mutatedZygomites); + + // Zygomites (alternate name) + SlayerTaskProfileJson zygomites = new SlayerTaskProfileJson(); + zygomites.setSetup("zygo"); + zygomites.setStyle("melee"); + zygomites.setPrayer("prange"); + zygomites.setAntipoison(true); + zygomites.setLocation("zanaris"); + profiles.put("zygomites", zygomites); + + // Nechryael - Burst + SlayerTaskProfileJson nechryael = new SlayerTaskProfileJson(); + nechryael.setSetup("burst"); + nechryael.setStyle("burst"); + nechryael.setPrayer("pmelee"); + nechryael.setGoading(true); + nechryael.setMinStackSize(4); + nechryael.setVariant("greater nechryael"); + nechryael.setLocation("catacombs nechryael"); + profiles.put("nechryael", nechryael); + + // Red dragons + SlayerTaskProfileJson redDragons = new SlayerTaskProfileJson(); + redDragons.setSetup("ranged"); + redDragons.setStyle("ranged"); + redDragons.setPrayer("pmelee"); + redDragons.setSuperAntifire(true); + redDragons.setLocation("brimhaven red dragons"); + profiles.put("red dragons", redDragons); + + // Skeletal wyverns + SlayerTaskProfileJson skeletalWyverns = new SlayerTaskProfileJson(); + skeletalWyverns.setSetup("wyvern"); + skeletalWyverns.setStyle("melee"); + skeletalWyverns.setPrayer("pmelee"); + skeletalWyverns.setLocation("asgarnia ice dungeon wyverns"); + profiles.put("skeletal wyverns", skeletalWyverns); + + // Smoke devils - Burst + SlayerTaskProfileJson smokeDevils = new SlayerTaskProfileJson(); + smokeDevils.setSetup("burst"); + smokeDevils.setStyle("burst"); + smokeDevils.setPrayer("pmage"); + smokeDevils.setGoading(true); + smokeDevils.setMinStackSize(4); + smokeDevils.setLocation("smoke devil dungeon"); + profiles.put("smoke devils", smokeDevils); + + // Spiritual creatures + SlayerTaskProfileJson spiritualCreatures = new SlayerTaskProfileJson(); + spiritualCreatures.setSetup("melee"); + spiritualCreatures.setStyle("melee"); + spiritualCreatures.setPrayer("pmelee"); + spiritualCreatures.setLocation("god wars spiritual creatures"); + profiles.put("spiritual creatures", spiritualCreatures); + + // Spiritual mages + SlayerTaskProfileJson spiritualMages = new SlayerTaskProfileJson(); + spiritualMages.setSetup("melee"); + spiritualMages.setStyle("melee"); + spiritualMages.setPrayer("pmelee"); + spiritualMages.setLocation("god wars spiritual creatures"); + profiles.put("spiritual mages", spiritualMages); + + // Spiritual warriors + SlayerTaskProfileJson spiritualWarriors = new SlayerTaskProfileJson(); + spiritualWarriors.setSetup("melee"); + spiritualWarriors.setStyle("melee"); + spiritualWarriors.setPrayer("pmelee"); + spiritualWarriors.setLocation("god wars spiritual creatures"); + profiles.put("spiritual warriors", spiritualWarriors); + + // Spiritual rangers + SlayerTaskProfileJson spiritualRangers = new SlayerTaskProfileJson(); + spiritualRangers.setSetup("melee"); + spiritualRangers.setStyle("melee"); + spiritualRangers.setPrayer("pmelee"); + spiritualRangers.setLocation("god wars spiritual creatures"); + profiles.put("spiritual rangers", spiritualRangers); + + // Suqahs + SlayerTaskProfileJson suqahs = new SlayerTaskProfileJson(); + suqahs.setSetup("stab"); + suqahs.setStyle("melee"); + suqahs.setPrayer("pmage"); + suqahs.setCannon(true); + suqahs.setLocation("lunar isle suqahs"); + suqahs.setCannonLocation("lunar isle suqahs"); + profiles.put("suqahs", suqahs); + + // Trolls + SlayerTaskProfileJson trolls = new SlayerTaskProfileJson(); + trolls.setSetup("melee"); + trolls.setStyle("melee"); + trolls.setPrayer("pmelee"); + trolls.setCannon(true); + trolls.setLocation("death plateau"); + trolls.setCannonLocation("death plateau"); + profiles.put("trolls", trolls); + + // Turoth + SlayerTaskProfileJson turoth = new SlayerTaskProfileJson(); + turoth.setSetup("melee"); + turoth.setStyle("melee"); + turoth.setPrayer("pmelee"); + turoth.setLocation("fremennik turoth"); + profiles.put("turoth", turoth); + + // TzHaar + SlayerTaskProfileJson tzhaar = new SlayerTaskProfileJson(); + tzhaar.setSetup("melee"); + tzhaar.setStyle("melee"); + tzhaar.setPrayer("pmelee"); + tzhaar.setLocation("tzhaar city"); + profiles.put("tzhaar", tzhaar); + + // Waterfiends + SlayerTaskProfileJson waterfiends = new SlayerTaskProfileJson(); + waterfiends.setSetup("ranged"); + waterfiends.setStyle("ranged"); + waterfiends.setPrayer("pmage"); + waterfiends.setLocation("ancient cavern"); + profiles.put("waterfiends", waterfiends); + + // Wyrms + SlayerTaskProfileJson wyrms = new SlayerTaskProfileJson(); + wyrms.setSetup("wyrm"); + wyrms.setStyle("melee"); + wyrms.setPrayer("pmage"); + wyrms.setLocation("karuulm wyrms"); + profiles.put("wyrms", wyrms); + + // Save the default profiles + saveProfiles(); + log.info("Created default slayer profiles with {} example entries", profiles.size()); + } + + /** + * Finds a profile for the given task name. + * Matching is case-insensitive and supports partial matches. + * + * @param taskName The slayer task name + * @return The matching profile, or null if not found + */ + public SlayerTaskProfileJson findProfile(String taskName) { + if (taskName == null || taskName.isEmpty()) { + return null; + } + + String taskLower = taskName.toLowerCase().trim(); + + // Try exact match first + for (Map.Entry entry : profiles.entrySet()) { + if (entry.getKey().equalsIgnoreCase(taskName)) { + return entry.getValue(); + } + } + + // Try partial match (task name contains profile key or vice versa) + for (Map.Entry entry : profiles.entrySet()) { + String profileKey = entry.getKey().toLowerCase(); + if (taskLower.contains(profileKey) || profileKey.contains(taskLower)) { + return entry.getValue(); + } + } + + return null; + } + + /** + * Adds or updates a profile for a task. + * + * @param taskName The task name (will be lowercased) + * @param profile The profile to save + */ + public void setProfile(String taskName, SlayerTaskProfileJson profile) { + profiles.put(taskName.toLowerCase().trim(), profile); + } + + /** + * Removes a profile for a task. + * + * @param taskName The task name to remove + */ + public void removeProfile(String taskName) { + profiles.remove(taskName.toLowerCase().trim()); + } + + /** + * Gets all loaded profiles. + */ + public Map getAllProfiles() { + return new HashMap<>(profiles); + } + + /** + * Gets the path to the profiles file. + */ + public Path getProfilePath() { + return profilePath; + } + + /** + * Reloads profiles from disk. + */ + public void reloadProfiles() { + loadProfiles(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerTaskProfileJson.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerTaskProfileJson.java new file mode 100644 index 0000000000..614534b7da --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/profile/SlayerTaskProfileJson.java @@ -0,0 +1,192 @@ +package net.runelite.client.plugins.microbot.slayer.profile; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import net.runelite.client.plugins.microbot.slayer.SlayerCombatStyle; +import net.runelite.client.plugins.microbot.slayer.SlayerPrayer; + +/** + * JSON model for a slayer task profile. + * Defines task-specific settings like gear setup, prayer, cannon, and potions. + */ +@Data +public class SlayerTaskProfileJson { + + /** + * Inventory setup name (e.g., "melee", "ranged", "magic", "burst"). + * When cannon is enabled, will look for "{setup}-cannon" variant. + */ + @SerializedName("setup") + private String setup; + + /** + * Combat style to use: melee, ranged, magic, burst, barrage. + * Burst/barrage styles use AoE spells and goading potions. + */ + @SerializedName("style") + private String style; + + /** + * Prayer to use. Supports shortcuts: pmelee, pm, pmage, pmagic, prange, pr, qp + */ + @SerializedName("prayer") + private String prayer; + + /** + * Whether to use cannon for this task. + */ + @SerializedName("cannon") + private boolean cannon = false; + + /** + * Whether to use goading potions to attract monsters. + * Automatically enabled for burst/barrage styles. + */ + @SerializedName("goading") + private boolean goading = false; + + /** + * Whether this task requires antipoison potions. + */ + @SerializedName("antipoison") + private boolean antipoison = false; + + /** + * Whether this task requires antivenom potions. + */ + @SerializedName("antivenom") + private boolean antivenom = false; + + /** + * Whether this task requires super antifire potions. + * Used for dragon tasks to provide full dragonfire protection. + */ + @SerializedName("superAntifire") + private boolean superAntifire = false; + + /** + * Specific monster variant to target (optional). + * Used when task name is generic but you want a specific monster. + * Examples: "metal dragons" -> "rune dragons", "kalphite" -> "kalphite soldiers" + */ + @SerializedName("variant") + private String variant; + + /** + * Preferred location for this task (optional). + * Use "auto" or leave null for automatic selection. + * This is used when NOT using a cannon. + */ + @SerializedName("location") + private String location; + + /** + * Preferred location when using cannon for this task (optional). + * If cannon is enabled and this is set, it will be used instead of location. + * The cannon will also be placed at this location. + */ + @SerializedName("cannonLocation") + private String cannonLocation; + + /** + * Whether to use special attack for this task. + */ + @SerializedName("useSpecial") + private boolean useSpecial = false; + + /** + * Minimum special attack energy before using special (0-100). + */ + @SerializedName("specialThreshold") + private int specialThreshold = 50; + + /** + * Minimum number of monsters to stack before casting burst/barrage. + * Default is 3 for efficient AoE. + */ + @SerializedName("minStackSize") + private int minStackSize = 3; + + /** + * Gets the parsed SlayerPrayer enum from the prayer string. + */ + public SlayerPrayer getParsedPrayer() { + return SlayerPrayer.fromString(prayer); + } + + /** + * Gets the parsed SlayerCombatStyle enum from the style string. + */ + public SlayerCombatStyle getParsedStyle() { + return SlayerCombatStyle.fromString(style); + } + + /** + * Checks if this profile has a specific setup defined. + */ + public boolean hasSetup() { + return setup != null && !setup.isEmpty(); + } + + /** + * Checks if this profile has a specific combat style defined. + */ + public boolean hasStyle() { + return style != null && !style.isEmpty(); + } + + /** + * Checks if this profile has a specific prayer defined. + */ + public boolean hasPrayer() { + return prayer != null && !prayer.isEmpty() && getParsedPrayer() != SlayerPrayer.NONE; + } + + /** + * Checks if this profile has a specific monster variant defined. + */ + public boolean hasVariant() { + return variant != null && !variant.isEmpty(); + } + + /** + * Checks if this profile has a preferred location (non-cannon). + */ + public boolean hasLocation() { + return location != null && !location.isEmpty() && !location.equalsIgnoreCase("auto"); + } + + /** + * Checks if this profile has a preferred cannon location. + */ + public boolean hasCannonLocation() { + return cannonLocation != null && !cannonLocation.isEmpty() && !cannonLocation.equalsIgnoreCase("auto"); + } + + /** + * Gets the cannon location string. + */ + public String getCannonLocation() { + return cannonLocation; + } + + /** + * Checks if goading should be used (explicit or implicit via burst/barrage style). + */ + public boolean shouldUseGoading() { + if (goading) { + return true; + } + // Automatically use goading for burst/barrage styles + SlayerCombatStyle combatStyle = getParsedStyle(); + return combatStyle != null && combatStyle.isAoeStyle(); + } + + /** + * Checks if this is a burst/barrage AoE combat style. + */ + public boolean isAoeStyle() { + SlayerCombatStyle combatStyle = getParsedStyle(); + return combatStyle != null && combatStyle.isAoeStyle(); + } +} diff --git a/src/main/resources/net/runelite/client/plugins/microbot/slayer/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/slayer/docs/README.md new file mode 100644 index 0000000000..5783d435cb --- /dev/null +++ b/src/main/resources/net/runelite/client/plugins/microbot/slayer/docs/README.md @@ -0,0 +1,295 @@ +# Slayer Plugin + +An automated slayer plugin that handles the full slayer loop: getting tasks from a master, banking for supplies, traveling to task locations, fighting monsters, looting, and returning for new tasks. + +## Features + +### Task Management +- **Auto Get Task**: Automatically walks to your chosen slayer master and requests a new assignment +- **Auto Skip Tasks**: Skip unwanted tasks from a configurable list (costs 30 slayer points) +- **Auto Block Tasks**: Permanently block tasks from a configurable list (costs 100 slayer points) +- **Point Safety**: Configurable minimum point reserves prevent accidentally spending all your points + +### Combat +- **Auto Combat**: Automatically targets and attacks slayer task monsters within a configurable radius +- **Combat Styles**: Supports melee, ranged, magic, burst, and barrage via task profiles +- **Superior Priority**: Detects and prioritizes superior slayer monster spawns (requires "Bigger and Badder" unlock) +- **Prayer Management**: Multiple prayer flicking styles (Always On, Lazy Flick, Perfect Lazy Flick, Mixed Lazy Flick) +- **Offensive Prayers**: Activates Piety/Rigour/Augury based on combat style +- **AOE Dodging**: Automatically dodges AOE projectile attacks (e.g., dragon poison pools) +- **Combat Potions**: Automatically drinks stat-boosting potions +- **World Hopping**: Hops worlds when crashed (no targets found for 20 seconds) +- **Goading Potions**: Supports goading for burst/barrage AoE stacking + +### Banking +- **Auto Banking**: Banks when food or prayer potions run low +- **Configurable Thresholds**: Set food count and prayer dose thresholds to trigger banking +- **Inventory Setups Integration**: Uses Microbot Inventory Setups to load correct gear per task (via task profiles) + +### POH (Player Owned House) +- **Rejuvenation Pool**: Uses POH pool to restore HP, Prayer, and Run energy +- **Multiple Teleport Methods**: House Tab, Teleport Spell, Construction Cape, Max Cape +- **Flexible Timing**: Use before banking, after tasks, or when HP drops below a threshold +- **Spellbook Swap**: Handles spellbook swapping via lectern for tasks requiring Ancient spells + +### Cannon +- **Auto Cannon**: Places, reloads, and picks up your dwarf multicannon +- **Predefined Spots**: 40+ optimized cannon placement spots for supported tasks +- **Cannonball Threshold**: Banks when cannonballs run low +- **Task Filtering**: Optionally restrict cannon use to specific tasks only + +### Looting +- **Loot Styles**: Mixed (list + price), Item List only, or GE Price Range only +- **Price Filtering**: Set minimum and maximum GE value thresholds +- **Item Lists**: Include and exclude lists with wildcard support (e.g., `*clue scroll*`, `*bones`) +- **Coins & Stackables**: Configurable coin stack minimums, arrow stacks, rune stacks +- **Bones & Ashes**: Auto-pickup and bury bones or scatter demonic ashes +- **Untradables**: Loot clue scrolls, keys, and other untradable drops +- **Force Loot**: Pick up items even while in combat +- **Eat for Space**: Eat food to make room for valuable loot +- **Delayed Looting**: Wait to let items pile up before looting +- **High Alch**: Alch configured items from inventory while fighting + +### Safety +- **Death Handling**: On player death, immediately disables prayers, stops all scripts, and logs out to pause the gravestone timer +- **Break Handler**: Integrates with Microbot break handler; only allows breaks during safe states (Idle, At Location) + +### Overlay +- **Slayer Points**: Current point total +- **State**: Current plugin state with color-coded status +- **Task**: Active slayer task name +- **Remaining**: Monsters left to kill +- **Location**: Current task location name + +## Configuration + +### General Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| Enable Plugin | Enabled | Toggle the slayer plugin on/off | +| Auto Travel | Enabled | Automatically travel to slayer task location | +| Slayer Master | Duradel | Which slayer master to get tasks from | +| Get New Task | Enabled | Automatically get a new task when current task is complete | + +### Task Management + +| Setting | Default | Description | +|---------|---------|-------------| +| Auto Skip Tasks | Disabled | Automatically skip tasks on the skip list (costs 30 points) | +| Skip Task List | Empty | Comma-separated list of task names to skip (e.g., `Black demons, Hellhounds`) | +| Min Points to Skip | 100 | Minimum slayer points required before skipping (keeps a reserve) | +| Auto Block Tasks | Disabled | Automatically block tasks on the block list (costs 100 points, permanent) | +| Block Task List | Empty | Comma-separated list of task names to permanently block (e.g., `Spiritual creatures, Drakes`) | +| Min Points to Block | 150 | Minimum slayer points required before blocking (keeps a reserve) | + +### Banking + +| Setting | Default | Description | +|---------|---------|-------------| +| Auto Banking | Enabled | Automatically bank when supplies are low | +| Food Threshold | 3 | Bank when food count drops below this amount (0 to disable) | +| Prayer Potion Threshold | 4 | Bank when prayer potion/super restore doses drop below this amount (0 to disable) | + +### POH (House) + +| Setting | Default | Description | +|---------|---------|-------------| +| Use POH Pool | Disabled | Use POH rejuvenation pool to restore HP/Prayer/Run energy | +| Teleport Method | House Tab | How to teleport to your house (House Tab, Spell, Construction Cape, Max Cape) | +| Use Before Banking | Enabled | Use POH pool to restore before going to bank (saves supplies) | +| Use After Task | Enabled | Use POH pool after completing a task (before getting new task) | +| Restore Below HP % | 50 | Use POH when HP drops below this percentage (0 to only restore at task end) | + +### Combat + +| Setting | Default | Description | +|---------|---------|-------------| +| Auto Combat | Enabled | Automatically attack slayer task monsters | +| Attack Radius | 10 | Maximum tile distance to search for monsters | +| Prioritize Superiors | Enabled | Always attack superior slayer monsters first when they spawn | +| Eat at HP % | 50 | Eat food when health drops below this percentage | +| Prayer Style | Off | Prayer management style: Off, Always On, Lazy Flick, Perfect Lazy Flick, Mixed Lazy Flick | +| Drink Prayer At | 20 | Drink prayer potion when prayer points drop below this amount | +| Use Combat Potions | Disabled | Drink combat potions (attack, strength, defence, ranging, magic) | +| Use Offensive Prayers | Disabled | Activate offensive prayers (Piety/Rigour/Augury) based on combat style | +| Dodge AOE Attacks | Disabled | Automatically dodge AOE projectile attacks | +| Hop When Crashed | Enabled | Hop worlds if no attackable targets found for 20 seconds | +| Hop World List | Empty | Comma-separated list of worlds to hop to (e.g., `390,391,392`). Leave empty for random members world. | + +### Cannon + +| Setting | Default | Description | +|---------|---------|-------------| +| Enable Cannon | Disabled | Use dwarf multicannon for tasks with predefined cannon spots | +| Cannonball Threshold | 100 | Bank when cannonball count drops below this amount | +| Cannon Tasks | Empty | Only use cannon for these tasks (comma-separated). Leave empty to cannon all supported tasks. | + +### Loot + +| Setting | Default | Description | +|---------|---------|-------------| +| Enable Looting | Enabled | Pick up loot from killed monsters | +| Loot Style | Mixed | How to determine what to loot: Mixed (list + price), Item List only, GE Price Range only | +| Item List | Empty | Comma-separated items to always loot. Supports `*` wildcards (e.g., `totem piece, *clue scroll*`) | +| Exclude List | Empty | Comma-separated items to NEVER loot. Supports `*` wildcards (e.g., `vial, jug, *bones`) | +| Min Loot Value | 1000 | Minimum GE value to loot (0 to disable price filtering) | +| Max Loot Value | 0 | Maximum GE value to loot (0 for no limit) | +| Loot Coins | Enabled | Pick up coin stacks | +| Min Coin Stack | 0 | Only loot coins if stack is at least this amount (0 = loot all) | +| Loot Arrows | Disabled | Pick up arrow stacks (10+ arrows) | +| Loot Runes | Disabled | Pick up rune stacks (2+ runes) | +| Loot Untradables | Enabled | Pick up untradable items (clue scrolls, keys, etc.) | +| Loot Bones | Disabled | Pick up bones from killed monsters | +| Bury Bones | Disabled | Automatically bury bones after picking them up | +| Scatter Ashes | Disabled | Pick up and scatter demonic/infernal ashes | +| Force Loot | Disabled | Loot items even while in combat | +| Only Loot My Items | Disabled | Only loot items dropped by/for you | +| Delayed Looting | Disabled | Wait before looting (lets items pile up) | +| Eat For Loot Space | Disabled | Eat food to make room for valuable loot | +| Enable High Alch | Disabled | High alch items from your inventory while fighting | +| High Alch Items | Empty | Comma-separated items to high alch. Supports `*` wildcards | +| High Alch Exclude | Empty | Comma-separated items to NEVER high alch. Supports `*` wildcards | + +## Supported Slayer Masters + +| Master | Combat Level | Location | +|--------|-------------|----------| +| Turael | 1 | Burthorpe | +| Spria | 1 | Draynor Village | +| Mazchna | 20 | Canifis | +| Vannaka | 40 | Edgeville Dungeon | +| Chaeldar | 70 | Zanaris | +| Nieve / Steve | 85 | Tree Gnome Stronghold | +| Duradel / Kuradal | 100 | Shilo Village | + +## Task Profiles (JSON) + +The plugin uses a per-task JSON profile system to configure task-specific settings like gear, combat style, prayer, location, cannon usage, and potions. + +### File Location + +Profiles are stored at: `~/.runelite/slayer-profiles.json` + +On first launch, the plugin generates a default file with 40+ preconfigured task profiles. You can edit this file to customize behavior for each task. + +### Profile Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `setup` | string | - | Inventory setup name to load from Microbot Inventory Setups | +| `style` | string | `"melee"` | Combat style: `melee`, `ranged`, `magic`, `burst`, `barrage` | +| `prayer` | string | `"none"` | Protection prayer: `pmelee`, `pmage`, `prange`, `none` | +| `cannon` | boolean | `false` | Whether to use cannon for this task | +| `goading` | boolean | `false` | Use goading potions (auto-enabled for burst/barrage) | +| `antipoison` | boolean | `false` | Bring antipoison potions | +| `antivenom` | boolean | `false` | Bring antivenom potions | +| `superAntifire` | boolean | `false` | Bring super antifire potions (for dragon tasks) | +| `variant` | string | - | Specific monster to target (e.g., `"kalphite soldier"` for kalphite tasks) | +| `location` | string | - | Preferred location key (e.g., `"catacombs abyssal demons"`) | +| `cannonLocation` | string | - | Location to use when cannoning (overrides `location`) | +| `useSpecial` | boolean | `false` | Use special attack | +| `specialThreshold` | int | `50` | Min spec energy before using special (0-100) | +| `minStackSize` | int | `3` | Min monsters to stack before casting burst/barrage | + +### Example Profile + +```json +{ + "dust devils": { + "setup": "burst", + "style": "burst", + "prayer": "pmage", + "cannon": false, + "goading": true, + "minStackSize": 4, + "location": "catacombs dust devils" + }, + "black demons": { + "setup": "melee", + "style": "melee", + "prayer": "pmelee", + "cannon": true, + "location": "catacombs black demons", + "cannonLocation": "chasm black demons" + }, + "frost dragons": { + "setup": "dragon", + "style": "melee", + "prayer": "pmelee", + "superAntifire": true, + "location": "grimstone frost dragons" + } +} +``` + +### Location Keys + +Location keys map to specific WorldPoints. Examples include: +- Catacombs: `catacombs hellhounds`, `catacombs dust devils`, `catacombs nechryael`, `catacombs abyssal demons` +- Slayer Tower: `slayer tower gargoyles`, `slayer tower abyssal demons`, `slayer tower aberrant spectres` +- Stronghold Cave: `stronghold hellhounds`, `stronghold fire giants`, `stronghold abyssal demons` +- Karuulm: `karuulm wyrms`, `karuulm drakes`, `karuulm hydras` +- Fremennik: `fremennik kurask`, `fremennik turoth`, `fremennik basilisk` +- Other: `chasm black demons`, `smoke devil dungeon`, `lunar isle suqahs`, `death plateau` + +See the `SlayerLocation` enum for the full list of 80+ supported location keys. + +## How It Works + +1. **Idle** - Checks if you have a slayer task. If yes, checks skip/block lists. If no task, walks to the selected slayer master. +2. **Getting Task** - Interacts with the slayer master and clicks through dialogue to receive an assignment. +3. **Skip/Block** - If the task is on your skip or block list and you have enough points, navigates the reward shop to skip (30 pts) or block (100 pts) the task. +4. **Detecting Task** - Reads the current task, loads the matching JSON profile, determines the task location and required gear. +5. **POH Restore** - If enabled, teleports to your POH and uses the rejuvenation pool. Handles spellbook swapping if needed. +6. **Banking** - Walks to a bank, loads the inventory setup from the profile, and withdraws food, potions, and cannonballs as needed. +7. **Traveling** - Uses the Microbot walker to navigate to the task location (from profile or auto-detected). +8. **Fighting** - Attacks task monsters, manages prayer, eats food, drinks potions, handles cannon, dodges AOE attacks, and loots drops. +9. **Task Complete** - Spends a few seconds looting remaining drops, then loops back to step 1 for the next task. + +## Requirements + +- Microbot client version 2.1.11 or higher +- Slayer skill unlocked +- Sufficient combat level for your chosen slayer master +- Food and prayer potions in your bank +- Microbot Inventory Setups plugin configured with gear setups matching your profile names + +## Tips + +- **Disable "Like a Boss"**: Turn off the "Like a Boss" slayer perk. Boss task amount input dialogues are not supported and will stall the plugin. +- **Inventory Setups**: Create named gear setups (e.g., "melee", "ranged", "burst") in the Microbot Inventory Setups plugin. Your JSON profile `setup` field should match these names. +- **Cannon Setups**: When cannon is enabled for a task, the plugin looks for a `{setup}-cannon` variant (e.g., "melee-cannon") to accommodate cannonballs in your inventory. +- **Start at Bank or POH**: Start the plugin while at a bank or your POH for the smoothest first-run experience. +- **Point Reserve**: Set your min points to skip/block higher than the cost (30/100) to keep a buffer for future skips. +- **Crash Detection**: With "Hop When Crashed" enabled, the plugin hops worlds after 20 seconds of finding no attackable targets. Configure your preferred world list or leave it empty for random members worlds. +- **Break Handler**: The plugin integrates with the Microbot break handler and only pauses during safe states (Idle, At Location) to avoid breaking mid-bank or mid-travel. +- **Profile Editing**: Edit `~/.runelite/slayer-profiles.json` while the client is closed, or restart the plugin to reload changes. + +## Known Limitations + +- **Boss tasks not supported**: Boss variants of slayer tasks are not handled. Disable "Like a Boss" in slayer rewards. +- **Konar not supported**: Konar quo Maten is excluded because her location-specific task constraints are not implemented. +- **No death recovery**: On death, the plugin logs out to pause your gravestone timer. You must recover your items manually. +- **Quest requirements**: Some task locations may require quest completion (e.g., Mourner Tunnels for Dark Beasts). Ensure you have the required access. +- **Spellbook requirements**: Burst/barrage tasks require the Ancient spellbook. The plugin can swap via POH lectern but needs a house with a lectern. + +## Disclaimer + +This plugin is intended for educational purposes and personal use only. Use of automation software may violate game rules. Use at your own risk. + +## Version History + +**1.0.0** - Initial release +- Full slayer loop automation (task, bank, travel, fight, loot) +- 8 supported slayer masters +- 40+ predefined cannon spots +- 80+ location keys +- Per-task JSON profile system +- Prayer flicking (lazy, perfect lazy, mixed) +- AOE projectile dodging +- Superior monster prioritization +- World hopping on crash detection +- POH pool restoration with spellbook swap +- Death safety (auto-logout) +- Break handler integration diff --git a/src/main/resources/slayer_icon.png b/src/main/resources/slayer_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..31a9fd443a195ef6023c29a3a27768348bb75c03 GIT binary patch literal 503 zcmV zm}FKgcHQX8vy{vzrATdg!0^M1>H5y&M@kq zWbA5cgV8|5&T!bvZZ(!bBnYG99_CdLg`0btYY_Q`?Va<&a*-_#LA+Ew#7!56x);E* zc@pDxkF`SFXN_{#IAd|B=f74s!8%BdR_beolfttUPm%$^jd{Q*i7^{ZjaKT5l30Y1 zAf6?2n<;Ny#t@7K0pPa?x9veT@AR|p*QImoGj`tTXWRB5+4x9{vEQt) zY@YH}U;wOxR5gu~*lH|+a)@yn$N=RmRLzVuP6GhII!IM>uJY^+01g2_1tACyP-VW~ ztN_3;2TB_eaM__QMY6a&SOsG>Gv(pn*ru5{kNK51t4iQnijx(z$TfYDR002ovPDHLkV1i?N[] debugPlugins = { - AIOFighterPlugin.class + SlayerPlugin.class, }; public static void main(String[] args) throws Exception