diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java
index 8f53d20259..f239ab1530 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java
@@ -132,12 +132,24 @@ default boolean avoidWilderness() {
return true;
}
+ @ConfigItem(
+ keyName = "avoidDangerousNpcs",
+ name = "Avoid dangerous NPCs",
+ description = "Route around tiles next to aggressive NPCs that attack you while passing
" +
+ "(e.g. undead trees at Draynor Manor). A penalty, not a block, so chokepoints still work.",
+ position = 1,
+ section = sectionSettings
+ )
+ default boolean avoidDangerousNpcs() {
+ return true;
+ }
+
@ConfigItem(
keyName = "useAgilityShortcuts",
name = "Use agility shortcuts",
description = "Whether to include agility shortcuts in the path.
" +
"You must also have the required agility level",
- position = 1,
+ position = 2,
section = sectionSettings
)
default boolean useAgilityShortcuts() {
@@ -149,7 +161,7 @@ default boolean useAgilityShortcuts() {
name = "Use grapple shortcuts",
description = "Whether to include crossbow grapple agility shortcuts in the path.
" +
"You must also have the required agility, ranged and strength levels",
- position = 2,
+ position = 3,
section = sectionSettings
)
default boolean useGrappleShortcuts() {
@@ -161,7 +173,7 @@ default boolean useGrappleShortcuts() {
name = "Use boats",
description = "Whether to include small boats in the path
" +
"(e.g. the boat to Fishing Platform)",
- position = 3,
+ position = 4,
section = sectionSettings
)
default boolean useBoats() {
@@ -172,7 +184,7 @@ default boolean useBoats() {
keyName = "useCanoes",
name = "Use canoes",
description = "Whether to include canoes in the path",
- position = 4,
+ position = 5,
section = sectionSettings
)
default boolean useCanoes() {
@@ -183,7 +195,7 @@ default boolean useCanoes() {
keyName = "useCharterShips",
name = "Use charter ships",
description = "Whether to include charter ships in the path",
- position = 5,
+ position = 6,
section = sectionSettings
)
default boolean useCharterShips() {
@@ -195,7 +207,7 @@ default boolean useCharterShips() {
name = "Use ships",
description = "Whether to include passenger ships in the path
" +
"(e.g. the customs ships to Karamja)",
- position = 6,
+ position = 7,
section = sectionSettings
)
default boolean useShips() {
@@ -207,7 +219,7 @@ default boolean useShips() {
name = "Use fairy rings",
description = "Whether to include fairy rings in the path.
" +
"You must also have completed the required quests or miniquests",
- position = 7,
+ position = 8,
section = sectionSettings
)
default boolean useFairyRings() {
@@ -218,7 +230,7 @@ default boolean useFairyRings() {
keyName = "useGnomeGliders",
name = "Use gnome gliders",
description = "Whether to include gnome gliders in the path",
- position = 8,
+ position = 9,
section = sectionSettings
)
default boolean useGnomeGliders() {
@@ -230,7 +242,7 @@ default boolean useGnomeGliders() {
name = "Use minecarts",
description = "Whether to include minecarts in the path
" +
"(e.g. the Keldagrim and Lovakengj minecart networks)",
- position = 9,
+ position = 10,
section = sectionSettings
)
default boolean useMinecarts() {
@@ -241,7 +253,7 @@ default boolean useMinecarts() {
keyName = "useSpiritTrees",
name = "Use spirit trees",
description = "Whether to include spirit trees in the path",
- position = 10,
+ position = 11,
section = sectionSettings
)
default boolean useSpiritTrees() {
@@ -253,7 +265,7 @@ default boolean useSpiritTrees() {
name = "Use teleportation items",
description = "Whether to include teleportation items from the player's inventory and equipment.
" +
"Options labelled (perm) only use permanent non-charge items.",
- position = 11,
+ position = 12,
section = sectionSettings
)
default TeleportationItem useTeleportationItems() {
@@ -265,7 +277,7 @@ default TeleportationItem useTeleportationItems() {
name = "Use teleportation levers",
description = "Whether to include teleportation levers in the path
" +
"(e.g. the lever from Edgeville to Wilderness)",
- position = 12,
+ position = 13,
section = sectionSettings
)
default boolean useTeleportationLevers() {
@@ -277,7 +289,7 @@ default boolean useTeleportationLevers() {
name = "Use teleportation portals",
description = "Whether to include teleportation portals in the path
" +
"(e.g. the portal from Ferox Enclave to Castle Wars)",
- position = 13,
+ position = 14,
section = sectionSettings
)
default boolean useTeleportationPortals() {
@@ -288,7 +300,7 @@ default boolean useTeleportationPortals() {
keyName = "useTeleportationSpells",
name = "Use teleportation spells",
description = "Whether to include teleportation spells in the path",
- position = 14,
+ position = 15,
section = sectionSettings
)
default boolean useTeleportationSpells() {
@@ -300,7 +312,7 @@ default boolean useTeleportationSpells() {
name = "Use teleportation to minigames",
description = "Whether to include teleportation to minigames/activities/grouping in the path
" +
"(e.g. the Nightmare Zone minigame teleport). These teleports share a 20 minute cooldown.",
- position = 15,
+ position = 16,
section = sectionSettings
)
default boolean useTeleportationMinigames() {
@@ -311,7 +323,7 @@ default boolean useTeleportationMinigames() {
keyName = "useWildernessObelisks",
name = "Use wilderness obelisks",
description = "Whether to include wilderness obelisks in the path",
- position = 16,
+ position = 17,
section = sectionSettings
)
default boolean useWildernessObelisks() {
@@ -322,7 +334,7 @@ default boolean useWildernessObelisks() {
keyName = "useNpcs",
name = "Use npcs",
description = "Whether to include npc transports in the path
(e.g. Tree gnome village maze or Lumbridge cellar)",
- position = 17,
+ position = 18,
section = sectionSettings
)
default boolean useNpcs() {
@@ -333,7 +345,7 @@ default boolean useNpcs() {
keyName = "useQuetzals",
name = "Use quetzals",
description = "Whether to include quetzals in the path.
",
- position = 18,
+ position = 19,
section = sectionSettings
)
default boolean useQuetzals() {
@@ -344,7 +356,7 @@ default boolean useQuetzals() {
keyName = "useMagicCarpets",
name = "Use Magic Carpets",
description = "Whether to include magic carpets in the path.
",
- position = 19,
+ position = 20,
section = sectionSettings
)
default boolean useMagicCarpets() {
@@ -355,7 +367,7 @@ default boolean useMagicCarpets() {
keyName = "useHotAirBalloons",
name = "Use Hot Air Balloons",
description = "Whether to include hot air balloons in the path.",
- position = 19,
+ position = 21,
section = sectionSettings
)
default boolean useHotAirBalloons() {
@@ -366,7 +378,7 @@ default boolean useHotAirBalloons() {
keyName = "useMagicMushtrees",
name = "Use Magic Mushtrees",
description = "Whether to include magic mushtrees in the path.",
- position = 19,
+ position = 22,
section = sectionSettings
)
default boolean useMagicMushtrees() {
@@ -377,7 +389,7 @@ default boolean useMagicMushtrees() {
keyName = "useSeasonalTransports",
name = "Use Seasonal Transports",
description = "Whether to include seasonal League transports (e.g. Map of Alacrity) in the path. League worlds only.",
- position = 19,
+ position = 23,
section = sectionSettings
)
default boolean useSeasonalTransports() {
@@ -388,7 +400,7 @@ default boolean useSeasonalTransports() {
keyName = "usePoh",
name = "Use Player-owned-house Teleports",
description = "Whether to include teleportation through the PoH",
- position = 20,
+ position = 24,
section = sectionSettings
)
default boolean usePoh() {
@@ -400,7 +412,7 @@ default boolean usePoh() {
name = "Cancel instead of recalculating",
description = "Whether the path should be cancelled rather than recalculated " +
"when the recalculate distance limit is exceeded",
- position = 21,
+ position = 25,
section = sectionSettings
)
default boolean cancelInstead() {
@@ -411,7 +423,7 @@ default boolean cancelInstead() {
keyName = "showTransportInfo",
name = "Show transport info",
description = "Whether to display transport destination hint info, e.g. which chat option and text to click",
- position = 22,
+ position = 26,
section = sectionSettings
)
default boolean showTransportInfo() {
@@ -423,7 +435,7 @@ default boolean showTransportInfo() {
name = "Teleport distance",
description = "Distance before using a teleport
" +
"(This is to avoid using teleports when you are to close",
- position = 23,
+ position = 27,
section = sectionSettings
)
default int distanceBeforeUsingTeleport() {
@@ -438,7 +450,7 @@ default int distanceBeforeUsingTeleport() {
keyName = "recalculateDistance",
name = "Recalculate distance",
description = "Distance from the path the player should be for it to be recalculated (-1 for never)",
- position = 24,
+ position = 28,
section = sectionSettings
)
default int recalculateDistance() {
@@ -453,7 +465,7 @@ default int recalculateDistance() {
keyName = "finishDistance",
name = "Finish distance",
description = "Distance from the target tile at which the path should be ended (-1 for never)",
- position = 25,
+ position = 29,
section = sectionSettings
)
default int reachedDistance() {
@@ -464,7 +476,7 @@ default int reachedDistance() {
keyName = "showTileCounter",
name = "Show tile counter",
description = "Whether to display the number of tiles travelled, number of tiles remaining or disable counting",
- position = 26,
+ position = 30,
section = sectionSettings
)
default TileCounter showTileCounter() {
@@ -475,7 +487,7 @@ default TileCounter showTileCounter() {
keyName = "tileCounterStep",
name = "Tile counter step",
description = "The number of tiles between the displayed tile counter numbers",
- position = 27,
+ position = 31,
section = sectionSettings
)
default int tileCounterStep()
@@ -495,7 +507,7 @@ default int tileCounterStep()
name = "Calculation cutoff",
description = "The cutoff threshold in number of ticks (0.6 seconds) of no progress being
" +
"made towards the path target before the calculation will be stopped",
- position = 28,
+ position = 32,
section = sectionSettings
)
default int calculationCutoff()
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java
index b35f09514a..cee0e1570e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java
@@ -160,6 +160,10 @@ private static int packedPointFromOrdinal(int startPacked, OrdinalDirection dire
private volatile long cachedRegionIdTime = 0;
private static final long REGION_CACHE_MS = 5000;
private static final int TOA_PUZZLE_REGION = 14162;
+ // Extra g-cost (in tile-distance units) for stepping onto a tile next to an aggressive-NPC
+ // hazard. High enough to strongly prefer a detour, but a penalty (not a block) so a true
+ // chokepoint is still traversable.
+ private static final int DANGEROUS_TILE_PENALTY = 100;
private int getCachedRegionId() {
long now = System.currentTimeMillis();
@@ -261,7 +265,18 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig
}
if (traversable[i]) {
- neighbors.add(new Node(neighborPacked, node));
+ if (config.isAvoidDangerousNpcs()
+ && config.isDangerousAdjacentTile(neighborPacked)
+ && !targets.contains(neighborPacked)) {
+ // Penalty (not a skip): the path keeps >=2 tiles from the hazard when a
+ // reasonable detour exists, but a chokepoint still routes through.
+ int penalizedCost = node.cost
+ + WorldPointUtil.distanceBetween(node.packedPosition, neighborPacked)
+ + DANGEROUS_TILE_PENALTY;
+ neighbors.add(new Node(neighborPacked, node, penalizedCost));
+ } else {
+ neighbors.add(new Node(neighborPacked, node));
+ }
} else if (Math.abs(d.x + d.y) == 1 && isBlocked(x + d.x, y + d.y, z)) {
// The transport starts from a blocked adjacent tile, e.g. fairy ring
// Only checks non-teleport transports (includes portals and levers, but not items and spells)
@@ -354,7 +369,16 @@ public List getReverseNeighbors(Node node, VisitedTiles visitedBackward, P
}
if (traversable[i]) {
- neighbors.add(new Node(prevPacked, node));
+ if (config.isAvoidDangerousNpcs() && config.isDangerousAdjacentTile(prevPacked)) {
+ // Mirror the forward danger penalty so the bidirectional search costs the same
+ // edge consistently from both ends and can't pick a hazard-adjacent meeting.
+ int penalizedCost = node.cost
+ + WorldPointUtil.distanceBetween(node.packedPosition, prevPacked)
+ + DANGEROUS_TILE_PENALTY;
+ neighbors.add(new Node(prevPacked, node, penalizedCost));
+ } else {
+ neighbors.add(new Node(prevPacked, node));
+ }
} else if (Math.abs(d.x + d.y) == 1
&& isBlocked(WorldPointUtil.unpackWorldX(prevPacked), WorldPointUtil.unpackWorldY(prevPacked), z)) {
int wx = WorldPointUtil.unpackWorldX(prevPacked);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java
index 918c52459c..211e1fdeef 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java
@@ -59,6 +59,9 @@ public class PathfinderConfig {
private static final WorldPoint SPIRIT_TREE_HOSIDIUS = new WorldPoint(1693, 3540, 0);
private static final WorldPoint SPIRIT_TREE_FARMING_GUILD = new WorldPoint(1251, 3750, 0);
private static final Set STATIC_BLOCKED_EDGES_PACKED = loadStaticBlockedEdgesFromResources();
+ // Tiles within 1 of an aggressive-NPC hazard tile (the melee-aggro ring). Stepping onto one
+ // gets a high pathfinding penalty when avoidDangerousNpcs is on, so paths keep >=2 tiles away.
+ private static final Set DANGEROUS_ADJACENT_TILES_PACKED = loadDangerousTilesFromResources();
/** Order matches {@link #spiritTreeDestinationToggle(int)} — add destinations in both places only here + switch. */
private static final WorldPoint[] SPIRIT_TREE_DESTINATIONS_ORDERED = {
@@ -102,6 +105,8 @@ public class PathfinderConfig {
@Getter
private volatile boolean avoidWilderness;
@Getter
+ private volatile boolean avoidDangerousNpcs;
+ @Getter
private volatile boolean useSpiritTrees;
private volatile boolean useAgilityShortcuts,
useGrappleShortcuts,
@@ -192,6 +197,7 @@ public CollisionMap getMap() {
public void refresh(WorldPoint target) {
calculationCutoffMillis = (long) config.calculationCutoff() * Constants.GAME_TICK_LENGTH;
avoidWilderness = ShortestPathPlugin.override("avoidWilderness", config.avoidWilderness());
+ avoidDangerousNpcs = ShortestPathPlugin.override("avoidDangerousNpcs", config.avoidDangerousNpcs());
useAgilityShortcuts = ShortestPathPlugin.override("useAgilityShortcuts", config.useAgilityShortcuts());
useGrappleShortcuts = ShortestPathPlugin.override("useGrappleShortcuts", config.useGrappleShortcuts());
useBoats = ShortestPathPlugin.override("useBoats", config.useBoats());
@@ -594,6 +600,43 @@ private static void addStaticEdge(Set edges, WorldPoint origin, WorldPoint
WorldPointUtil.packWorldPoint(destination)));
}
+ /** True if {@code packedPoint} is within 1 tile of an aggressive-NPC hazard tile. */
+ public boolean isDangerousAdjacentTile(int packedPoint) {
+ return DANGEROUS_ADJACENT_TILES_PACKED.contains(packedPoint);
+ }
+
+ private static Set loadDangerousTilesFromResources() {
+ Set tiles = new HashSet<>();
+ final String prefixComment = "#";
+ final String delimColumn = "\t";
+
+ try {
+ String s = new String(Util.readAllBytes(
+ ShortestPathPlugin.class.getResourceAsStream("dangerous_tiles.tsv")), StandardCharsets.UTF_8);
+ Scanner scanner = new Scanner(s);
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine();
+ if (line.startsWith(prefixComment) || line.isBlank()) {
+ continue;
+ }
+ WorldPoint hazard = parseBlockedEdgePoint(line.split(delimColumn)[0]);
+ // Penalize the hazard tile and all 8 neighbours (the melee-aggro ring) so the
+ // path keeps >=2 tiles away. Hazard tiles themselves are usually blocked anyway.
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ tiles.add(WorldPointUtil.packWorldPoint(
+ hazard.getX() + dx, hazard.getY() + dy, hazard.getPlane()));
+ }
+ }
+ }
+ scanner.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to load shortest-path dangerous tiles", e);
+ }
+
+ return Collections.unmodifiableSet(tiles);
+ }
+
private static boolean blocksWalkingEdgeWhenUnavailable(Transport transport) {
if (transport == null || transport.getOrigin() == null || transport.getDestination() == null) {
return false;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java
index c90ff191d9..750e5dd419 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java
@@ -1645,6 +1645,21 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part
path.get(i), RECOVERY_MINIMAP_REACH_EUCLIDEAN - 1,
wp -> inInstance || isKnownWalkableOrUnloaded(wp));
}
+ // Don't let recovery park the player on a tile next to an aggressive NPC
+ // (e.g. an undead tree). The planner avoids those via avoidDangerousNpcs,
+ // but this runtime fallback would otherwise strand us in melee. Step the
+ // target back along the path to the nearest non-hazard tile.
+ PathfinderConfig dangerCfg = ShortestPathPlugin.pathfinderConfig;
+ if (dangerCfg != null && dangerCfg.isAvoidDangerousNpcs() && recoverTarget != null
+ && dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(recoverTarget))) {
+ int safeIdx = recoverIdx;
+ while (safeIdx > indexOfStartPoint
+ && dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(path.get(safeIdx)))) {
+ safeIdx--;
+ }
+ recoverIdx = safeIdx;
+ recoverTarget = path.get(safeIdx);
+ }
boolean clicked = recoverTarget != null
&& !recoverTarget.equals(playerLoc)
&& Rs2Walker.walkMiniMap(recoverTarget);
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/dangerous_tiles.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/dangerous_tiles.tsv
new file mode 100644
index 0000000000..2149505b46
--- /dev/null
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/dangerous_tiles.tsv
@@ -0,0 +1,14 @@
+# Tile Display info
+# Tiles occupied by aggressive NPCs that attack you while you path next to them.
+# When the "Avoid dangerous NPCs" pathfinder option is on, a high step penalty is applied
+# to every tile within 1 of these (the melee-aggro ring), so paths keep >=2 tiles away
+# when a reasonable detour exists. It is a penalty, not a block: at a true chokepoint the
+# path still goes through. Each entry is "x y plane".
+3103 3347 0 Draynor Manor undead tree
+3107 3342 0 Draynor Manor undead tree
+3107 3344 0 Draynor Manor undead tree
+3108 3346 0 Draynor Manor undead tree
+3111 3339 0 Draynor Manor undead tree
+3111 3348 0 Draynor Manor undead tree
+3115 3344 0 Draynor Manor undead tree
+3120 3344 0 Draynor Manor undead tree