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