diff --git a/.run/Microbot Debug.run.xml b/.run/Microbot Debug.run.xml
new file mode 100644
index 0000000000..b6844aab78
--- /dev/null
+++ b/.run/Microbot Debug.run.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 086224e0e6..e5d1d1b3b0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,19 +1,223 @@
-# Repository Guidelines
+# agents.md
-## Project Structure & Module Organization
-Gradle drives builds through `build.gradle` plus helper scripts in `gradle/`. Plugin sources reside under `src/main/java/net/runelite/client/plugins/microbot/` with matching resources in `src/main/resources/`. Each plugin should also keep documentation inside `src/main/resources//docs/` (README, optional `assets/`, and `dependencies.txt`). Shared web assets live in `public/`, while IDE/debug helpers and tests sit in `src/test/java`, notably `net.runelite.client.Microbot` for RuneLiteDebug sessions.
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-## Build, Test, and Development Commands
-Use `./gradlew clean build` for a full compile, shading, and plugin detection report. During iteration, limit the scope via `./gradlew build -PpluginList=DailyTasksPlugin`. Execute unit tests with `./gradlew test`, and launch the client for manual verification through `./gradlew run --args='--debug'`.
+## Overview
-## Coding Style & Naming Conventions
-Java code uses four-space indentation, Lombok annotations where they reduce boilerplate, and `PluginDescriptor` metadata referencing constants from `PluginConstants`. Keep plugin versions in a `static final String version = "x.y.z"` field. Packages remain lowercase, while class names mirror plugin folders in UpperCamelCase (e.g., `PestControlPlugin`). Runtime assets stay under each plugin’s resources subtree, and documentation should reuse existing tag constants.
+Microbot Hub is a community plugin repository for the Microbot RuneLite client. It maintains a separation between core client functionality and community-contributed plugins, allowing rapid plugin development without affecting client stability. Each plugin is independently built, versioned, and packaged for GitHub Releases.
-## Testing Guidelines
-Place unit tests under `src/test/java` (or `src/test/generated_tests` for generated fixtures) with a `*Test` suffix. Tests should exercise automation logic headlessly, using deterministic mocks instead of live client calls. Add your plugin to `RuneLiteDebug.pluginsToDebug` in `src/test/java/net/runelite/client/Microbot.java` when interactive checks are necessary.
+## Build System Architecture
-## Commit & Pull Request Guidelines
-Follow the conventional subject style, e.g., `feat: add PestControlPlugin (#241)`. Keep subjects under ~72 characters and mention the plugin or subsystem touched. Pull requests must link related issues, summarize behavior changes, list the commands used for verification (`./gradlew clean build`, `./gradlew test`, etc.), and include screenshots or GIFs for UI overlays. Limit each PR to a single plugin or feature and call out dependency or configuration updates explicitly.
+The build system uses **Gradle with custom plugin discovery and packaging**:
-## Security & Configuration Tips
-Never commit credentials; load secrets from local `gradle.properties`. Prefer released dependency versions and justify additions inside each plugin’s `docs/README.md`. Use `PluginConstants.DEFAULT_PREFIX` and `DEFAULT_ENABLED`, and confirm `minClientVersion` against the Microbot client version logged during builds.
+- **Dynamic Plugin Discovery**: `build.gradle` scans `src/main/java/net/runelite/client/plugins/microbot/` for directories containing `*Plugin.java` files
+- **Per-Plugin Source Sets**: Each discovered plugin gets its own Gradle source set, compile task, and shadow JAR task
+- **Gradle Helper Scripts**: Core build logic lives in:
+ - `gradle/project-config.gradle` - centralized configuration (JDK version, paths, GitHub release URLs, client version)
+ - `gradle/plugin-utils.gradle` - plugin discovery, descriptor parsing, JAR creation, SHA256 hashing
+
+### Build Commands
+
+```bash
+# Build all plugins
+./gradlew clean build
+
+# Build specific plugin(s) only (much faster for iteration)
+./gradlew build -PpluginList=PestControlPlugin
+./gradlew build -PpluginList=PestControlPlugin,AutoMiningPlugin
+
+# Run tests (tests have access to all plugin source sets)
+./gradlew test
+
+# Generate plugins.json metadata file with SHA256 hashes (requires exact JDK 11)
+./gradlew generatePluginsJson
+
+# Copy plugin documentation to public/docs/
+./gradlew copyPluginDocs
+
+# Launch RuneLite debug session with plugins from Microbot.java
+./gradlew run --args='--debug'
+
+# Validate JDK version
+./gradlew validateJdkVersion
+```
+
+## Plugin Structure
+
+Each plugin lives in its own package under `src/main/java/net/runelite/client/plugins/microbot//`:
+
+```
+/
+├── Plugin.java # Main plugin class with @PluginDescriptor
+├── Script.java # Script logic extending Script class
+├── Config.java # Configuration interface (optional)
+├── Overlay.java # UI overlay (optional)
+└── Additional support classes
+```
+
+Matching resources under `src/main/resources/net/runelite/client/plugins/microbot//`:
+
+```
+/
+├── dependencies.txt # Maven coordinates (optional)
+└── docs/
+ ├── README.md # Plugin documentation
+ └── assets/ # Screenshots, icons, etc.
+```
+
+## Plugin Descriptor Anatomy
+
+Every plugin **must** have a `@PluginDescriptor` annotation with these **required** fields:
+
+- `name` - Display name (use `PluginConstants.DEFAULT_PREFIX` or create custom prefix)
+- `version` - Semantic version string (store in `static final String version` field)
+- `minClientVersion` - Minimum Microbot client version required
+
+Important **optional** fields:
+
+- `authors` - Array of author names
+- `description` - Brief description shown in plugin panel
+- `tags` - Array of tags for categorization
+- `iconUrl` - URL to icon image (shown in client hub)
+- `cardUrl` - URL to card image (shown on website)
+- `enabledByDefault` - Use `PluginConstants.DEFAULT_ENABLED` (currently `false`)
+- `isExternal` - Use `PluginConstants.IS_EXTERNAL` (currently `true`)
+
+Example:
+```java
+@PluginDescriptor(
+ name = PluginConstants.MOCROSOFT + "Pest Control",
+ description = "Supports all boats, portals, and shields.",
+ tags = {"pest control", "minigames"},
+ authors = { "Mocrosoft" },
+ version = PestControlPlugin.version,
+ minClientVersion = "1.9.6",
+ iconUrl = "https://chsami.github.io/Microbot-Hub/PestControlPlugin/assets/icon.png",
+ cardUrl = "https://chsami.github.io/Microbot-Hub/PestControlPlugin/assets/card.png",
+ enabledByDefault = PluginConstants.DEFAULT_ENABLED,
+ isExternal = PluginConstants.IS_EXTERNAL
+)
+@Slf4j
+public class PestControlPlugin extends Plugin {
+ static final String version = "2.2.7";
+ // ...
+}
+```
+
+## PluginConstants
+
+The `PluginConstants.java` file is **shared across all plugins** (included in each JAR during build). It contains:
+
+- Standardized plugin name prefixes (e.g., `DEFAULT_PREFIX`, `MOCROSOFT`, `BOLADO`)
+- Global defaults: `DEFAULT_ENABLED = false`, `IS_EXTERNAL = true`
+
+When creating a new plugin prefix, add it to `PluginConstants.java` for consistency.
+
+## Adding External Dependencies
+
+If a plugin needs additional libraries beyond the Microbot client:
+
+1. Create `src/main/resources/net/runelite/client/plugins/microbot//dependencies.txt`
+2. Add Maven coordinates, one per line:
+ ```
+ com.google.guava:guava:33.2.0-jre
+ org.apache.commons:commons-lang3:3.14.0
+ ```
+3. The build system automatically includes these in the plugin's shadow JAR
+
+## Testing and Debugging Plugins
+
+### Running Plugins in Debug Mode
+
+1. Edit `src/test/java/net/runelite/client/Microbot.java`
+2. Add your plugin class to the `debugPlugins` array:
+ ```java
+ private static final Class>[] debugPlugins = {
+ YourPlugin.class,
+ AutoLoginPlugin.class
+ };
+ ```
+3. Run `./gradlew run --args='--debug'` or use your IDE's run configuration
+
+### Running Tests
+
+- Tests live in `src/test/java/`
+- Test classes have access to all plugin source sets (configured in `build.gradle`)
+- Use `./gradlew test` to run all tests
+
+## Version Management
+
+- **Always increment the plugin version** when making changes (even small fixes)
+- Store version in a static field: `static final String version = "1.2.3";`
+- Follow semantic versioning: `MAJOR.MINOR.PATCH`
+- The version is used for JAR naming, GitHub release assets, and `plugins.json` generation
+
+## Git Workflow
+
+Based on recent commits:
+
+- Use conventional commit prefixes: `fix:`, `feat:`, `docs:`, etc.
+- Include PR references when applicable: `fix: description (#123)`
+- Work on feature branches, merge to `development`, create PRs to `main`
+- Current branch: `development`, main branch: `main`
+
+## Publishing Workflow
+
+1. Build plugins: `./gradlew build`
+2. Generate metadata: `./gradlew generatePluginsJson` (requires JDK 11 exactly)
+3. Copy documentation: `./gradlew copyPluginDocs`
+4. Upload `build/libs/-.jar` and updated `public/docs/plugins.json` as assets on the GitHub release tagged with `` (or `latest-release` for the stable tag): `https://github.com/chsami/Microbot-Hub/releases/download//-.jar`
+
+## Important Implementation Details
+
+- **Java Version**: JDK 11 (configured in `project-config.gradle` with `TARGET_JDK_VERSION = 11`, vendor `ADOPTIUM`)
+- **Microbot Client Dependency**: Defaults to the latest version resolved via `https://microbot.cloud/api/version/client`, falling back to `2.0.61` if lookup fails. Artifacts come from GitHub Releases (`https://github.com/chsami/Microbot/releases/download//microbot-.jar`). Override with `-PmicrobotClientVersion=` or `-PmicrobotClientVersion=latest`, or supply a local JAR for offline work via `-PmicrobotClientPath=/absolute/path/to/microbot-.jar`
+- **Plugin Release Tag**: `plugins.json` uses a stable release tag (`latest-release`) so download URLs stay constant: `https://github.com/chsami/Microbot-Hub/releases/download/latest-release/-.jar`. Override with `-PpluginsReleaseTag=` if needed.
+- **Shadow JAR Excludes**: Common exclusions defined in `plugin-utils.gradle` include `docs/**`, `dependencies.txt`, metadata files, and module-info
+- **Reproducible Builds**: JAR tasks disable file timestamps, use reproducible file order, and normalize file permissions to `0644`
+- **Descriptor Parsing**: Build system uses regex to extract plugin metadata from Java source files (see `getPluginDescriptorInfo` in `plugin-utils.gradle`)
+
+## Plugin Discovery Logic
+
+When you run `./gradlew build`:
+
+1. Scans `src/main/java/net/runelite/client/plugins/microbot/` for directories
+2. Finds directories containing a file matching `*Plugin.java`
+3. Creates a plugin object with: `name` (class name without .java), `sourceSetName` (directory name), `dir`, `javaFile`
+4. Filters by `-PpluginList` if provided
+5. For each plugin:
+ - Creates dedicated source set
+ - Configures compilation classpath with Microbot client
+ - Creates shadow JAR task with plugin-specific dependencies
+ - Parses `@PluginDescriptor` for metadata
+ - Computes SHA256 hash of JAR for `plugins.json`
+
+## Common Patterns
+
+- Plugins extending `SchedulablePlugin` implement `getStartCondition()` and `getStopCondition()` for scheduler integration
+- Use `@Inject` for dependency injection (configs, overlays, scripts)
+- Config classes use `@Provides` methods to register with `ConfigManager`
+- Overlays are registered in `startUp()`, unregistered in `shutDown()`
+- Use `@Subscribe` for event handling (ChatMessage, GameTick, etc.)
+
+## Threading
+
+Scripts run on a scheduled executor thread, but certain RuneLite API calls (widgets, game objects, etc.) must run on the client thread:
+
+```java
+// Use invoke() for client thread operations
+TrialInfo info = Microbot.getClientThread().invoke(() -> TrialInfo.getCurrent(client));
+
+// For void operations
+Microbot.getClientThread().invoke(() -> {
+ // client thread code here
+});
+```
+
+**Always use `Microbot.getClientThread().invoke()`** when accessing:
+- Widgets (`client.getWidget()`, `widget.isHidden()`)
+- Game objects that aren't cached
+- Player world view (`client.getLocalPlayer().getWorldView()`)
+- `BoatLocation.fromLocal()` - accesses player world view internally
+- `TrialInfo.getCurrent()` - accesses widgets internally
+- Any RuneLite API that throws "must be called on client thread"
diff --git a/CLAUDE.md b/CLAUDE.md
index f00bdb5f57..0b6ba95a5f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -199,3 +199,74 @@ When you run `./gradlew build`:
- Config classes use `@Provides` methods to register with `ConfigManager`
- Overlays are registered in `startUp()`, unregistered in `shutDown()`
- Use `@Subscribe` for event handling (ChatMessage, GameTick, etc.)
+
+## Threading
+
+Scripts run on a scheduled executor thread, but certain RuneLite API calls (widgets, game objects, etc.) must run on the client thread:
+
+```java
+// Use invoke() for client thread operations
+TrialInfo info = Microbot.getClientThread().invoke(() -> TrialInfo.getCurrent(client));
+
+// For void operations
+Microbot.getClientThread().invoke(() -> {
+ // client thread code here
+});
+```
+
+**Always use `Microbot.getClientThread().invoke()`** when accessing:
+- Widgets (`client.getWidget()`, `widget.isHidden()`)
+- Game objects that aren't cached
+- Player world view (`client.getLocalPlayer().getWorldView()`)
+- Varbits (`client.getVarbitValue()`)
+- `BoatLocation.fromLocal()` - accesses player world view internally
+- `TrialInfo.getCurrent()` - accesses widgets internally
+- `Rs2BoatCache.getLocalBoat()` - accesses player world view
+- `Rs2BoatModel.isNavigating()` - accesses varbits
+- `Rs2BoatModel.isMovingForward()` - accesses varbits
+- `Rs2BoatModel.getHeading()` - accesses varbits
+- Any RuneLite API that throws "must be called on client thread"
+
+## Event Subscription from Scripts
+
+**Important:** `@Subscribe` event handlers (`onGameTick`, `onClientTick`, `onVarbitChanged`, `onGameObjectSpawned`, etc.) **run on the client thread automatically**. You do NOT need to wrap client API calls in `Microbot.getClientThread().invoke()` inside these handlers.
+
+Scripts can subscribe to RuneLite events by injecting the EventBus:
+
+```java
+@Slf4j
+public class MyScript {
+ private final EventBus eventBus;
+
+ @Inject
+ public MyScript(EventBus eventBus) {
+ this.eventBus = eventBus;
+ }
+
+ public void register() {
+ eventBus.register(this);
+ }
+
+ public void unregister() {
+ eventBus.unregister(this);
+ }
+
+ @Subscribe
+ public void onGameTick(GameTick event) {
+ // Handle game tick
+ }
+}
+```
+
+In the plugin, call `register()` in `startUp()` and `unregister()` in `shutDown()`:
+
+```java
+@Override
+protected void startUp() {
+ myScript.register();
+}
+
+protected void shutDown() {
+ myScript.unregister();
+}
+```
diff --git a/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingOverlay.java
index 0674441261..1af628fe07 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingOverlay.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingOverlay.java
@@ -45,7 +45,7 @@ public Dimension render(Graphics2D graphics) {
.build());
panelComponent.getChildren().add(LineComponent.builder()
- .left("Bait: " + (Rs2Inventory.hasItem("fish chunks") ? String.valueOf(Rs2Inventory.get("fish chunks").getQuantity()) : "Not Present"))
+ .left("Bait: " + (Rs2Inventory.hasItem("fish chunks", "fish offcuts") ? String.valueOf(Rs2Inventory.get("fish chunks").getQuantity()) : "Not Present"))
.build());
panelComponent.getChildren().add(LineComponent.builder().build());
diff --git a/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingPlugin.java
index a6db7a50e2..a045473a82 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingPlugin.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingPlugin.java
@@ -27,7 +27,7 @@
isExternal = PluginConstants.IS_EXTERNAL
)
public class AerialFishingPlugin extends Plugin {
- public static final String version = "1.1.1";
+ public static final String version = "1.1.2";
@Inject
private Client client;
@Inject
diff --git a/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingScript.java b/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingScript.java
index 5205d14279..a5d2cea7a8 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingScript.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/aerialfishing/AerialFishingScript.java
@@ -36,7 +36,7 @@ public boolean run(AerialFishingConfig config) {
Rs2AntibanSettings.microBreakDurationLow = 1;
Rs2AntibanSettings.microBreakDurationHigh = 5;
mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
- if (!super.run() || !Microbot.isLoggedIn() || !Rs2Inventory.hasItem("fish chunks", "king worm") || (!Rs2Equipment.isWearing(ItemID.AERIAL_FISHING_GLOVES_NO_BIRD) && !Rs2Equipment.isWearing(ItemID.AERIAL_FISHING_GLOVES_BIRD))) {
+ if (!super.run() || !Microbot.isLoggedIn() || !Rs2Inventory.hasItem("fish chunks", "king worm", "fish offcuts") || (!Rs2Equipment.isWearing(ItemID.AERIAL_FISHING_GLOVES_NO_BIRD) && !Rs2Equipment.isWearing(ItemID.AERIAL_FISHING_GLOVES_BIRD))) {
return;
}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/MSailingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/MSailingPlugin.java
index 6baf7b68c6..fda10dc0dd 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/sailing/MSailingPlugin.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/MSailingPlugin.java
@@ -7,6 +7,9 @@
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.microbot.PluginConstants;
import net.runelite.client.plugins.microbot.sailing.features.salvaging.SalvagingHighlight;
+import net.runelite.client.plugins.microbot.sailing.features.trials.TrialsScript;
+import net.runelite.client.plugins.microbot.sailing.features.trials.debug.BoatPathOverlay;
+import net.runelite.client.plugins.microbot.sailing.features.trials.overlay.TrialRouteOverlay;
import net.runelite.client.ui.overlay.OverlayManager;
import javax.inject.Inject;
@@ -27,7 +30,7 @@
@Slf4j
public class MSailingPlugin extends Plugin {
- static final String version = "1.0.2";
+ static final String version = "2.0.1";
@Inject
private SailingConfig config;
@@ -45,19 +48,32 @@ SailingConfig provideConfig(ConfigManager configManager) {
@Inject
private SailingScript sailingScript;
+ @Inject
+ private TrialsScript trialsScript;
+ @Inject
+ private BoatPathOverlay boatPathOverlay;
+ @Inject
+ private TrialRouteOverlay trialRouteOverlay;
@Override
protected void startUp() throws AWTException {
if (overlayManager != null) {
overlayManager.add(sailingOverlay);
overlayManager.add(salvagingHighlight);
+ overlayManager.add(boatPathOverlay);
+ overlayManager.add(trialRouteOverlay);
}
+ trialsScript.register();
sailingScript.run();
}
protected void shutDown() {
sailingScript.shutdown();
+ trialsScript.shutdown();
+ trialsScript.unregister();
overlayManager.remove(sailingOverlay);
overlayManager.remove(salvagingHighlight);
+ overlayManager.remove(boatPathOverlay);
+ overlayManager.remove(trialRouteOverlay);
}
}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingConfig.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingConfig.java
index 80249dac91..6d91dbda98 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingConfig.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingConfig.java
@@ -5,6 +5,7 @@
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.ConfigSection;
+import net.runelite.client.plugins.microbot.sailing.features.trials.data.TrialRanks;
import java.awt.*;
@@ -26,6 +27,13 @@ public interface SailingConfig extends Config {
)
String highlightSection = "highlight";
+ @ConfigSection(
+ name = "Trials",
+ description = "Barracuda Trials settings",
+ position = 2
+ )
+ String trialsSection = "trials";
+
@ConfigItem(
keyName = "Salvgaging",
name = "Salvgaging",
@@ -160,4 +168,52 @@ default Color salvagingHighLevelWrecksColour()
{
return Color.RED;
}
+
+ @ConfigItem(
+ keyName = "trials",
+ name = "Enable Trials",
+ description = "Enable Barracuda Trials automation.",
+ position = 0,
+ section = trialsSection
+ )
+ default boolean trials()
+ {
+ return false;
+ }
+
+ @ConfigItem(
+ keyName = "trialsRank",
+ name = "Target Rank",
+ description = "The rank route to follow during trials.",
+ position = 1,
+ section = trialsSection
+ )
+ default TrialRanks trialsRank()
+ {
+ return TrialRanks.Swordfish;
+ }
+
+ @ConfigItem(
+ keyName = "showTrialRoute",
+ name = "Show Route Overlay",
+ description = "Show the trial route path on screen.",
+ position = 2,
+ section = trialsSection
+ )
+ default boolean showTrialRoute()
+ {
+ return true;
+ }
+
+ @ConfigItem(
+ keyName = "autoNavigate",
+ name = "Auto Navigate",
+ description = "Automatically navigate the boat along the route.",
+ position = 3,
+ section = trialsSection
+ )
+ default boolean autoNavigate()
+ {
+ return false;
+ }
}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingScript.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingScript.java
index 98fddb87f8..522b10436a 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingScript.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/SailingScript.java
@@ -4,6 +4,7 @@
import net.runelite.client.plugins.microbot.Microbot;
import net.runelite.client.plugins.microbot.Script;
import net.runelite.client.plugins.microbot.sailing.features.salvaging.SalvagingScript;
+import net.runelite.client.plugins.microbot.sailing.features.trials.TrialsScript;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
@@ -13,11 +14,13 @@ public class SailingScript extends Script {
private final SailingConfig config;
private final SalvagingScript salvagingFeature;
+ private final TrialsScript trialsFeature;
@Inject
- public SailingScript(SailingConfig config, SalvagingScript salvagingFeature) {
+ public SailingScript(SailingConfig config, SalvagingScript salvagingFeature, TrialsScript trialsFeature) {
this.config = config;
this.salvagingFeature = salvagingFeature;
+ this.trialsFeature = trialsFeature;
}
public boolean run() {
@@ -26,21 +29,20 @@ public boolean run() {
try {
if (!Microbot.isLoggedIn()) return;
if (!super.run()) return;
- long startTime = System.currentTimeMillis();
if (config.salvaging()) {
salvagingFeature.run(config);
}
+ if (config.trials()) {
+ trialsFeature.run(config);
+ }
- long endTime = System.currentTimeMillis();
- long totalTime = endTime - startTime;
- log.info("Total time for loop {}ms", totalTime);
} catch (Exception ex) {
log.trace("Exception in main loop: ", ex);
}
- }, 0, 1000, TimeUnit.MILLISECONDS);
+ }, 0, 100, TimeUnit.MILLISECONDS);
return true;
}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/BoatLocation.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/BoatLocation.java
new file mode 100644
index 0000000000..aa3c67d4df
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/BoatLocation.java
@@ -0,0 +1,24 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials;
+
+import net.runelite.api.Client;
+import net.runelite.api.WorldEntity;
+import net.runelite.api.WorldView;
+import net.runelite.api.coords.LocalPoint;
+import net.runelite.api.coords.WorldPoint;
+
+public class BoatLocation {
+ public static WorldPoint fromLocal(Client client, LocalPoint local) {
+ if (local == null) {
+ return null;
+ }
+
+ WorldView wv = client.getLocalPlayer().getWorldView();
+ int wvid = wv.getId();
+ boolean isOnBoat = wvid != -1;
+ if (isOnBoat) {
+ WorldEntity we = client.getTopLevelWorldView().worldEntities().byIndex(wvid);
+ return WorldPoint.fromLocalInstance(client, we.getLocalLocation());
+ }
+ return WorldPoint.fromLocalInstance(client, local);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/TrialsScript.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/TrialsScript.java
new file mode 100644
index 0000000000..c10a761718
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/TrialsScript.java
@@ -0,0 +1,924 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials;
+
+import com.google.common.base.Strings;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.*;
+import net.runelite.api.coords.WorldPoint;
+import net.runelite.api.events.*;
+import net.runelite.api.gameval.InterfaceID;
+import net.runelite.api.gameval.ObjectID;
+import net.runelite.api.gameval.VarbitID;
+import net.runelite.api.widgets.Widget;
+import net.runelite.client.eventbus.EventBus;
+import net.runelite.client.eventbus.Subscribe;
+import net.runelite.client.events.ConfigChanged;
+import net.runelite.client.plugins.microbot.Microbot;
+import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache;
+import net.runelite.client.plugins.microbot.sailing.SailingConfig;
+import net.runelite.client.plugins.microbot.sailing.features.trials.data.*;
+import net.runelite.client.plugins.microbot.sailing.features.trials.debug.BoatPathHelper;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.*;
+
+@Slf4j
+@Singleton
+public class TrialsScript {
+
+ private final Client client;
+ private final Rs2BoatCache boatCache;
+ private final EventBus eventBus;
+
+ private int currentWaypointIndex = 0;
+ private TrialRoute activeRoute = null;
+
+ private final Set TRIAL_CRATE_ANIMS = Set.of(8867);
+ private final Set SPEED_BOOST_ANIMS = Set.of(13159, 13160, 13163);
+ private final Set DECORATION_ANIMS = Set.of(1071, 13537, 13538, 13539);
+
+ private static final int VISIT_TOLERANCE = 15;
+
+ private static final int WIND_MOTE_INACTIVE_SPRITE_ID = 7076;
+ private static final int WIND_MOTE_ACTIVE_SPRITE_ID = 7075;
+
+
+ private TrialInfo currentTrial = null;
+
+
+ private int lastVisitedIndex = -1;
+
+
+ private int toadsThrown = 0;
+
+ // Number of consecutive game ticks where TrialInfo.getCurrent(client) returned null
+ // Used to allow a grace period before clearing currentTrial during transient nulls
+ private int nullTrialConsecutiveTicks = 0;
+
+ private final Set TRIAL_BOAT_GAMEOBJECT_IDS = Set.of(
+ ObjectID.SAILING_BT_TEMPOR_TANTRUM_NORTH_LOC_PARENT,
+ ObjectID.SAILING_BT_TEMPOR_TANTRUM_SOUTH_LOC_PARENT,
+ ObjectID.SAILING_BT_JUBBLY_JIVE_TOAD_SUPPLIES_PARENT);
+
+ private final Map> toadFlagsById = new HashMap<>();
+
+ private final Map trialCratesById = new HashMap<>();
+
+ private final Map> trialBoostsById = new HashMap<>();
+
+ private GameObject sailGameObject = null;
+
+
+ private final Set obstacleWorldPoints = new HashSet<>();
+
+
+ private final Map trialBoatsById = new HashMap<>();
+
+
+ private int isInTrial;
+
+ private int boardedBoat;
+
+
+ private Directions currentHeadingDirection = Directions.North;
+
+
+ private Directions requestedHeadingDirection = currentHeadingDirection;
+
+
+ private Widget windMoteButtonWidget;
+
+
+ private double minCratePickupDistance;
+
+
+ private double maxCratePickupDistance;
+
+ private int boatSpawnedAngle;
+ private boolean needsTrim;
+ private int windMoteReleasedTick;
+ private Directions hoveredHeadingDirection;
+ private int boatSpawnedFineX;
+ private int boatSpawnedFineZ;
+ private int boatBaseSpeed;
+ private int boatSpeedCap;
+ private int boatSpeedBoostDuration;
+ private int boatAcceleration;
+
+ @Inject
+ public TrialsScript(Client client, Rs2BoatCache boatCache, EventBus eventBus) {
+ this.client = client;
+ this.boatCache = boatCache;
+ this.eventBus = eventBus;
+ }
+
+ public void register() {
+ eventBus.register(this);
+ }
+
+ public void unregister() {
+ eventBus.unregister(this);
+ }
+
+ public void run(SailingConfig config) {
+ try {
+ TrialInfo info = Microbot.getClientThread().invoke(() -> TrialInfo.getCurrent(client));
+
+ if (info == null) {
+ resetState();
+ return;
+ }
+
+ TrialRoute route = findRoute(info.Location, info.Rank);
+ if (route == null) {
+ log.warn("No route found for location={} rank={}", info.Location, info.Rank);
+ return;
+ }
+
+ if (!route.equals(activeRoute)) {
+ activeRoute = route;
+ currentWaypointIndex = 0;
+ }
+
+ List routePoints = route.getInterpolatedPoints();
+ if (routePoints == null || routePoints.isEmpty()) {
+ return;
+ }
+
+ if (currentWaypointIndex >= routePoints.size()) {
+ currentWaypointIndex = 0;
+ }
+
+ WorldPoint boatPos = getBoatPosition();
+ if (boatPos == null) {
+ return;
+ }
+
+ WorldPoint target = routePoints.get(currentWaypointIndex);
+ int distance = boatPos.distanceTo(target);
+
+ if (distance <= 5) {
+ lastVisitedIndex = currentWaypointIndex;
+ currentWaypointIndex = (currentWaypointIndex + 1) % routePoints.size();
+ target = routePoints.get(currentWaypointIndex);
+ }
+
+ final WorldPoint hintTarget = target;
+ Microbot.getClientThread().invoke(() -> client.setHintArrow(hintTarget));
+
+ if (config.autoNavigate()) {
+ navigateToWaypoint(target);
+ }
+
+ } catch (Exception ex) {
+ log.error("Error in trials script", ex);
+ }
+ }
+
+ private TrialRoute findRoute(TrialLocations location, TrialRanks rank) {
+ for (TrialRoute route : TrialRoute.AllTrialRoutes) {
+ if (route.Location == location && route.Rank == rank) {
+ return route;
+ }
+ }
+ return null;
+ }
+
+ private WorldPoint getBoatPosition() {
+ return Microbot.getClientThread().invoke(() -> {
+ var localPlayer = client.getLocalPlayer();
+ if (localPlayer == null) {
+ return null;
+ }
+ return BoatLocation.fromLocal(client, localPlayer.getLocalLocation());
+ });
+ }
+
+ private void navigateToWaypoint(WorldPoint target) {
+ var boat = Microbot.getClientThread().invoke(() -> boatCache.getLocalBoat());
+ if (boat == null) {
+ log.info("navigateToWaypoint: No boat found in cache");
+ return;
+ }
+
+ boat.sailTo(target);
+ }
+
+ public void shutdown() {
+ resetState();
+ }
+
+ private void resetState() {
+ currentWaypointIndex = 0;
+ activeRoute = null;
+ lastVisitedIndex = -1;
+ Microbot.getClientThread().invoke(() -> client.clearHintArrow());
+ }
+
+ @Subscribe
+ public void onClientTick(ClientTick clientTick) {
+ var localPlayer = client.getLocalPlayer();
+ if (localPlayer == null) {
+ return;
+ }
+
+ var tick = client.getTickCount();
+ var position = BoatLocation.fromLocal(client, localPlayer.getLocalLocation());
+ if (tick <= 0 || position == null) {
+ return;
+ }
+
+ if (BoatPathHelper.HasTickData(tick)) {
+ //log.info("Adding visited point for tick {}: {}", tick, position);
+ BoatPathHelper.AddVisitedPoint(tick, position);
+ } else {
+ BoatPathHelper.StartNewTick(tick, position, currentHeadingDirection);
+ }
+ }
+
+ @Subscribe
+ public void onGameTick(GameTick tick) {
+ if (client == null || client.getLocalPlayer() == null) {
+ return;
+ }
+
+ updateFromVarbits();
+ updateCurrentTrial();
+ updateCurrentHeading();
+
+ updateWindMoteButtonWidget();
+
+ final var player = client.getLocalPlayer();
+ var boatLocation = BoatLocation.fromLocal(client, player.getLocalLocation());
+
+ if (boatLocation == null)
+ return;
+
+ var active = getActiveTrialRoute();
+ if (active != null) {
+ markNextWaypointVisited(boatLocation, active, VISIT_TOLERANCE);
+ }
+ }
+
+ @Subscribe
+ public void onVarbitChanged(VarbitChanged event) {
+ if (event.getVarbitId() == VarbitID.SAILING_BOAT_SPAWNED_ANGLE) {
+ boatSpawnedAngle = event.getValue();
+ updateCurrentHeadingFromVarbit(boatSpawnedAngle);
+ }
+
+ if (event.getVarbitId() == VarbitID.SAILING_BT_IN_TRIAL) {
+ updateToadsThrown(currentTrial);
+ }
+
+ trackCratePickups(event);
+ }
+
+ private void updateCurrentHeadingFromVarbit(int value) {
+ var ordinal = value / 128;
+ var directions = Directions.values();
+ if (ordinal < 0 || ordinal >= directions.length) {
+ return;
+ }
+ currentHeadingDirection = directions[ordinal];
+ }
+
+ @Subscribe
+ public void onGameObjectSpawned(GameObjectSpawned event) {
+ var obj = event.getGameObject();
+ if (obj == null) {
+ return;
+ }
+ var id = obj.getId();
+
+ var isToadFlag = ToadFlagGameObject.All.stream().anyMatch(t -> t.GameObjectIds.contains(id));
+ if (isToadFlag) {
+ toadFlagsById.computeIfAbsent(id, k -> new ArrayList<>()).add(obj);
+ }
+
+ var isTrialBoat = TRIAL_BOAT_GAMEOBJECT_IDS.contains(id);
+ if (isTrialBoat) {
+ trialBoatsById.put(id, obj);
+ //log.info("Tracked trial boat gameobject id {} at {} - {}", id, obj.getWorldLocation(), BoatLocation.fromLocal(client, obj.getLocalLocation()));
+
+ }
+
+ var isObstacle = ObstacleTracking.OBSTACLE_GAMEOBJECT_IDS.contains(id);
+ if (isObstacle) {
+ // Add world points for all tiles covered by this obstacle's footprint
+ try {
+ var worldView = client.getTopLevelWorldView();
+ var scene = worldView != null ? worldView.getScene() : null;
+ if (scene != null) {
+ var min = obj.getSceneMinLocation();
+ var max = obj.getSceneMaxLocation();
+ if (min != null && max != null) {
+ var plane = worldView.getPlane();
+ for (var x = min.getX(); x <= max.getX(); x++) {
+ for (var y = min.getY(); y <= max.getY(); y++) {
+ WorldPoint wp = WorldPoint.fromScene(worldView, x, y, plane);
+ obstacleWorldPoints.add(wp);
+ }
+ }
+ } else {
+ obstacleWorldPoints.add(obj.getWorldLocation());
+ }
+ } else {
+ obstacleWorldPoints.add(obj.getWorldLocation());
+ }
+ } catch (Exception ex) {
+ obstacleWorldPoints.add(obj.getWorldLocation());
+ }
+ if (currentTrial != null) {
+ removeGameObjectFromScene(obj);
+ }
+ }
+
+ var isSail = AllSails.GAMEOBJECT_IDS.contains(id);
+ if (isSail) {
+ sailGameObject = obj;
+ }
+
+ var renderable = obj.getRenderable();
+ if (renderable != null) {
+ if (renderable instanceof DynamicObject) {
+ var dynObj = (DynamicObject) renderable;
+ var anim = dynObj.getAnimation();
+ var animId = anim != null ? anim.getId() : -1;
+ if (TRIAL_CRATE_ANIMS.contains(animId)) {
+ trialCratesById.put(id, obj);
+ } else if (SPEED_BOOST_ANIMS.contains(animId)) {
+ trialBoostsById.computeIfAbsent(id, k -> new ArrayList<>()).add(obj);
+ } else if (DECORATION_ANIMS.contains(animId)) {
+ removeGameObjectFromScene(obj);
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ public void onGameObjectDespawned(GameObjectDespawned event) {
+ var obj = event.getGameObject();
+ if (obj == null)
+ return;
+
+ var id = obj.getId();
+
+ var flagList = toadFlagsById.get(id);
+ if (flagList != null) {
+ flagList.removeIf(x -> x == null || x == obj);
+ if (flagList.isEmpty()) {
+ toadFlagsById.remove(id);
+ }
+ }
+
+ if (trialBoatsById.get(id) == obj) {
+ trialBoatsById.remove(id);
+ }
+
+ if (trialCratesById.get(id) == obj) {
+ trialCratesById.remove(id);
+ }
+
+ var boostList = trialBoostsById.get(id);
+ if (boostList != null) {
+ boostList.removeIf(x -> x == null || x == obj);
+ if (boostList.isEmpty()) {
+ trialBoostsById.remove(id);
+ }
+ }
+
+ if (sailGameObject == obj) {
+ sailGameObject = null;
+ }
+ }
+
+ @Subscribe
+ public void onWorldViewUnloaded(WorldViewUnloaded event) {
+ for (var toadFlagList : toadFlagsById.values()) {
+ toadFlagList.removeIf(obj -> event.getWorldView() == obj.getWorldView());
+ }
+ toadFlagsById.entrySet().removeIf(entry -> entry.getValue().isEmpty());
+
+ for (var boat : trialBoatsById.values()) {
+ if (event.getWorldView() == boat.getWorldView()) {
+ trialBoatsById.remove(boat.getId());
+ }
+ }
+
+ for (var crate : trialCratesById.values()) {
+ if (event.getWorldView() == crate.getWorldView()) {
+ trialCratesById.remove(crate.getId());
+ }
+ }
+
+ for (var boostList : trialBoostsById.values()) {
+ boostList.removeIf(obj -> event.getWorldView() == obj.getWorldView());
+ }
+ trialBoostsById.entrySet().removeIf(entry -> entry.getValue().isEmpty());
+
+ if (sailGameObject != null && event.getWorldView() == sailGameObject.getWorldView()) {
+ sailGameObject = null;
+ }
+ }
+
+ @Subscribe
+ public void onGameStateChanged(GameStateChanged event) {
+ if (event.getGameState() == GameState.LOADING) {
+ // on region changes the tiles and gameobjects get set to null
+ reset();
+ }
+ }
+
+
+ @Subscribe
+ public void onConfigChanged(ConfigChanged event) {
+ if (event == null) {
+ return;
+ }
+
+ if (event.getGroup().equals(SailingConfig.configGroup)) {
+ if (event.getKey().equals("trials") && event.getNewValue().equals("false")) {
+ resetState();
+ }
+ return;
+ }
+ }
+
+ @Subscribe
+ public void onChatMessage(ChatMessage e) {
+ if (e.getType() != ChatMessageType.GAMEMESSAGE && e.getType() != ChatMessageType.SPAM) {
+ return;
+ }
+
+ var msg = e.getMessage().toLowerCase();
+ if (msg == null || msg.isEmpty()) {
+ return;
+ }
+
+ String WIND_MOTE_RELEASED_TEXT = "you release the wind mote for a burst of speed";
+ if (msg.contains(WIND_MOTE_RELEASED_TEXT)) {
+ windMoteReleasedTick = client.getTickCount();
+ }
+ String TRIM_AVAILABLE_TEXT = "you feel a gust of wind.";
+ String TRIM_SUCCESS_TEXT = "you trim the sails";
+ String TRIM_FAIL_TEXT = "the wind dies down";
+ if (msg.contains(TRIM_AVAILABLE_TEXT)) {
+ needsTrim = true;
+ } else if (msg.contains(TRIM_SUCCESS_TEXT) || msg.contains(TRIM_FAIL_TEXT)) {
+ needsTrim = false;
+ }
+ }
+
+ private void updateCurrentTrial() {
+ var newTrialInfo = TrialInfo.getCurrent(client);
+ if (newTrialInfo != null) {
+ nullTrialConsecutiveTicks = 0;
+
+ // If the trial changed (location/rank/reset time), reset route state
+ if (currentTrial == null || currentTrial.Location != newTrialInfo.Location || currentTrial.Rank != newTrialInfo.Rank || newTrialInfo.CurrentTimeSeconds < currentTrial.CurrentTimeSeconds) {
+ resetRouteData();
+ }
+
+ updateToadsThrown(newTrialInfo);
+ currentTrial = newTrialInfo;
+ } else {
+ if (currentTrial != null) {
+ nullTrialConsecutiveTicks += 1;
+ if (nullTrialConsecutiveTicks >= 18) {
+ resetRouteData();
+ currentTrial = null;
+ }
+ } else {
+ nullTrialConsecutiveTicks = 0;
+ }
+ }
+ }
+
+
+ private void resetRouteData() {
+ lastVisitedIndex = -1;
+ toadsThrown = 0;
+ }
+
+ private void reset() {
+ requestedHeadingDirection = currentHeadingDirection;
+ }
+
+ private void updateToadsThrown(TrialInfo newTrialInfo) {
+ if (currentTrial == null || isInTrial == 0 || newTrialInfo.CurrentTimeSeconds < currentTrial.CurrentTimeSeconds || newTrialInfo.CurrentTimeSeconds == 0) {
+ toadsThrown = 0;
+ return;
+ }
+ if (newTrialInfo.ToadCount < currentTrial.ToadCount) {
+ toadsThrown += 1;
+ }
+ }
+
+ public void markNextWaypointVisited(final WorldPoint player, final TrialRoute route, final int tolerance) {
+ if (player == null || route == null) {
+ return;
+ }
+ var points = route.getInterpolatedPoints();
+ if (points == null || points.isEmpty()) {
+ return;
+ }
+ var nextIdx = lastVisitedIndex + 1;
+ if (nextIdx >= points.size()) {
+ return;
+ }
+ var target = points.get(nextIdx);
+ if (target == null) {
+ return;
+ }
+ var dist = Math.hypot(player.getX() - target.getX(), player.getY() - target.getY());
+ if (dist <= tolerance) {
+ lastVisitedIndex = nextIdx;
+ }
+ }
+
+ public List getNextIndicesAfterLastVisited(final TrialRoute route, final int limit) {
+ if (route == null || limit <= 0) {
+ return Collections.emptyList();
+ }
+ var points = route.getInterpolatedPoints();
+ if (points == null || points.isEmpty()) {
+ return Collections.emptyList();
+ }
+ var start = Math.max(0, lastVisitedIndex);
+ if (start >= points.size()) {
+ return Collections.emptyList();
+ }
+ var out = new ArrayList(limit);
+ var nextPortal = route.PortalDirections.stream()
+ .filter(x -> route.getInterpolatedIndex(x.Index) >= lastVisitedIndex)
+ .min((a, b) -> Integer.compare(route.getInterpolatedIndex(a.Index), route.getInterpolatedIndex(b.Index)))
+ .orElse(null);
+ for (var i = start; i < points.size() && out.size() < limit; i++) {
+ if (nextPortal != null && i > route.getInterpolatedIndex(nextPortal.Index)) {
+ break;
+ }
+ out.add(i);
+ }
+ return out;
+ }
+
+ public List getVisibleLineForRoute(final WorldPoint player, final TrialRoute route, final int limit) {
+ if (player == null || route == null || lastVisitedIndex == -1) {
+ return Collections.emptyList();
+ }
+
+ final List nextIdx = getNextIndicesAfterLastVisited(route, limit);
+ if (nextIdx.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ var points = route.getInterpolatedPoints();
+ List out = new ArrayList<>();
+ for (var idx : nextIdx) {
+ if (idx >= 0 && idx < points.size()) {
+ out.add(points.get(idx));
+ }
+ }
+ return out;
+ }
+
+ public PortalDirection getVisiblePortalDirection(TrialRoute route) {
+ var portalDirection = route.PortalDirections.stream()
+ .filter(x -> {
+ int interpolatedIdx = route.getInterpolatedIndex(x.Index);
+ return interpolatedIdx - 1 == lastVisitedIndex || interpolatedIdx == lastVisitedIndex || interpolatedIdx + 1 == lastVisitedIndex;
+ })
+ .min((a, b) -> Integer.compare(route.getInterpolatedIndex(a.Index), route.getInterpolatedIndex(b.Index)))
+ .orElse(null);
+
+ return portalDirection;
+ }
+
+ public List getToadFlagGameObjectsForIds(Set ids) {
+ var out = new ArrayList();
+ if (ids == null || ids.isEmpty()) {
+ return out;
+ }
+ for (var id : ids) {
+ var list = toadFlagsById.get(id);
+ if (list != null && !list.isEmpty()) {
+ out.addAll(list);
+ }
+ }
+ return out;
+ }
+
+ public TrialRoute getActiveTrialRoute() {
+ if (currentTrial == null)
+ return null;
+
+ for (var route : TrialRoute.AllTrialRoutes) {
+ if (route == null) {
+ continue;
+ }
+
+ if (route.Location == currentTrial.Location && route.Rank == currentTrial.Rank) {
+ return route;
+ }
+ }
+ return null;
+ }
+
+ public List getVisibleActiveLineForPlayer(final WorldPoint player, final int limit) {
+ var route = getActiveTrialRoute();
+ if (route == null) {
+ return Collections.emptyList();
+ }
+
+ return getVisibleLineForRoute(player, route, limit);
+ }
+
+ public List getNextUnvisitedIndicesForActiveRoute(final int limit) {
+ var route = getActiveTrialRoute();
+ if (route == null) {
+ return Collections.emptyList();
+ }
+ return getNextIndicesAfterLastVisited(route, limit);
+ }
+
+ public int getHighlightedToadFlagIndex() {
+ var route = getActiveTrialRoute();
+ if (route == null || currentTrial == null) {
+ return 0;
+ }
+ return getHighlightedToadFlagIndex(route);
+ }
+
+ private int getHighlightedToadFlagIndex(TrialRoute route) {
+ return toadsThrown < route.ToadOrder.size() ? toadsThrown : 0;
+ }
+
+ public List getToadFlagToHighlight() {
+ if (currentTrial == null || currentTrial.Location != TrialLocations.JubblyJive || currentTrial.ToadCount <= 0) {
+ return Collections.emptyList();
+ }
+
+ var route = getActiveTrialRoute();
+ if (route == null || route.ToadOrder == null || route.ToadOrder.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ var nextToadIdx = getHighlightedToadFlagIndex(route);
+ if (nextToadIdx >= 0 && nextToadIdx < route.ToadOrder.size()) {
+ var nextToadColor = route.ToadOrder.get(nextToadIdx);
+ var nextToadGameObject = ToadFlagGameObject.getByColor(nextToadColor);
+ var cached = getToadFlagGameObjectsForIds(nextToadGameObject.GameObjectIds);
+ if (!cached.isEmpty()) {
+ return cached;
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ public Collection getTrialBoatsToHighlight() {
+ var route = getActiveTrialRoute();
+ if (route == null || currentTrial == null || trialBoatsById.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ if (route.Location == TrialLocations.JubblyJive && !currentTrial.HasToads) {
+ return trialBoatsById.values();
+ }
+
+ if (route.Location == TrialLocations.TemporTantrum) {
+ if (currentTrial.HasRum) {
+ var boat = trialBoatsById.get(ObjectID.SAILING_BT_TEMPOR_TANTRUM_NORTH_LOC_PARENT);
+ if (boat != null) {
+ return List.of(boat);
+ }
+ } else {
+ var boat = trialBoatsById.get(ObjectID.SAILING_BT_TEMPOR_TANTRUM_SOUTH_LOC_PARENT);
+ if (boat != null) {
+ return List.of(boat);
+ }
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ private void handleHeadingClicks(MenuOptionClicked event) {
+ if (event.getMenuAction() != MenuAction.SET_HEADING) {
+ return;
+ }
+ requestedHeadingDirection = Directions.values()[event.getId()];
+ }
+
+ private void updateCurrentHeading() {
+ if (currentHeadingDirection == null) {
+ currentHeadingDirection = Directions.South;
+ }
+
+ if (requestedHeadingDirection == null) {
+ requestedHeadingDirection = currentHeadingDirection;
+ return;
+ }
+
+ if (currentHeadingDirection == requestedHeadingDirection) {
+ return;
+ }
+
+ var all = Directions.values();
+ var n = all.length;
+ var currentIndex = currentHeadingDirection.ordinal();
+ var targetIndex = requestedHeadingDirection.ordinal();
+
+ var forwardSteps = (targetIndex - currentIndex + n) % n;
+ var backwardSteps = (currentIndex - targetIndex + n) % n;
+
+ if (forwardSteps == 0) {
+ return;
+ }
+
+ if (forwardSteps <= backwardSteps) {
+ currentIndex = (currentIndex + 1) % n;
+ } else {
+ currentIndex = (currentIndex - 1 + n) % n;
+ }
+
+ currentHeadingDirection = all[currentIndex];
+ }
+
+ private void manageHeadingHovers(PostMenuSort event) {
+ var entries = client.getMenuEntries();
+ var headingEntry = Arrays.stream(entries)
+ .filter(e -> e.getOption().equals("Set heading"))
+ .findFirst().orElse(null);
+ if (headingEntry != null) {
+ hoveredHeadingDirection = Directions.values()[headingEntry.getIdentifier()];
+ }
+ }
+
+ private void updateFromVarbits() {
+ //todo check VarbitID.SAILING_BOAT_TIME_TILL_TRIM and VarbitID.SAILING_BOAT_TIME_TRIM_WINDOW to see if they're working in the future
+ boatSpawnedAngle = client.getVarbitValue(VarbitID.SAILING_BOAT_SPAWNED_ANGLE);
+ boatSpawnedFineX = client.getVarbitValue(VarbitID.SAILING_BOAT_SPAWNED_FINEX);
+ boatSpawnedFineZ = client.getVarbitValue(VarbitID.SAILING_BOAT_SPAWNED_FINEZ);
+ boatBaseSpeed = client.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_BASESPEED);
+ boatSpeedCap = client.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_SPEEDCAP);
+ boatSpeedBoostDuration = client.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_SPEEDBOOST_DURATION);
+ boatAcceleration = client.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_ACCELERATION);
+ isInTrial = client.getVarbitValue(VarbitID.SAILING_BT_IN_TRIAL);
+ boardedBoat = client.getVarbitValue(VarbitID.SAILING_BOARDED_BOAT);
+ }
+
+ private void removeGameObjectFromScene(GameObject gameObject) {
+ if (gameObject != null) {
+ var renderable = gameObject == null ? null : gameObject.getRenderable();
+ if (renderable != null) {
+ var model = renderable instanceof Model ? (Model) renderable : renderable.getModel();
+ if (model != null) {
+ var scene = client.getTopLevelWorldView().getScene();
+ if (scene != null) {
+ scene.removeGameObject(gameObject);
+ }
+ var playerWv = client.getLocalPlayer().getWorldView();
+ var playerScene = playerWv != null ? playerWv.getScene() : null;
+ if (playerScene != null) {
+ playerScene.removeGameObject(gameObject);
+ }
+ }
+ }
+ }
+ }
+
+ private void updateWindMoteButtonWidget() {
+ if (boardedBoat == 0) {
+ windMoteButtonWidget = null;
+ return;
+ }
+
+ if (windMoteButtonWidget != null && !windMoteButtonWidget.isHidden()) {
+ return;
+ }
+
+ var widget = client.getWidget(InterfaceID.SailingSidepanel.FACILITIES_ROWS);
+ if (widget == null) {
+ //log.info("updateWindMoteButtonWidget: FACILITIES_ROWS widget is null");
+ return;
+ }
+
+ var facilityChildren = widget.getChildren();
+ Widget button = null;
+ if (facilityChildren != null) {
+ for (var childWidget : facilityChildren) {
+ if (childWidget != null && (childWidget.getSpriteId() == WIND_MOTE_INACTIVE_SPRITE_ID || childWidget.getSpriteId() == WIND_MOTE_ACTIVE_SPRITE_ID)) {
+ button = childWidget;
+ break;
+ }
+ }
+ }
+ if (button != null) {
+ if (windMoteButtonWidget == null) {
+ windMoteButtonWidget = button;
+ }
+ }
+ }
+
+ private void trackCratePickups(VarbitChanged event) {
+ if (event.getVarbitId() >= VarbitID.SAILING_BT_OBJECTIVE0 && event.getVarbitId() <= VarbitID.SAILING_BT_OBJECTIVE95) {
+ var closestCrate = getClosestTrialCrate();
+ if (closestCrate != null) {
+ var player = client.getLocalPlayer();
+ if (player != null) {
+ var playerPoint = BoatLocation.fromLocal(client, player.getLocalLocation());
+ if (playerPoint != null) {
+ var cratePoint = closestCrate.getWorldLocation();
+ double lastCratePickupDistance = Math.hypot(Math.abs(playerPoint.getX() - cratePoint.getX()), Math.abs(playerPoint.getY() - cratePoint.getY()));
+ log.info("Picked up crate from distance: {}", lastCratePickupDistance);
+ if (minCratePickupDistance == 0 || lastCratePickupDistance < minCratePickupDistance) {
+ minCratePickupDistance = lastCratePickupDistance;
+ }
+ if (lastCratePickupDistance > maxCratePickupDistance) {
+ maxCratePickupDistance = lastCratePickupDistance;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private GameObject getClosestTrialCrate() {
+ GameObject closest = null;
+ var closestDist = Double.MAX_VALUE;
+
+ var player = client.getLocalPlayer();
+ if (player == null) {
+ return null;
+ }
+ var playerPoint = BoatLocation.fromLocal(client, player.getLocalLocation());
+ if (playerPoint == null) {
+ return null;
+ }
+
+ for (var crateEntry : trialCratesById.entrySet()) {
+ var crate = crateEntry.getValue();
+ if (crate == null) {
+ continue;
+ }
+ var cratePoint = crate.getWorldLocation();
+ var dist = playerPoint.distanceTo(cratePoint);
+ if (dist < closestDist) {
+ closestDist = dist;
+ closest = crate;
+ }
+ }
+ return closest;
+ }
+
+ private void logCrateAndBoostSpawns(GameObjectSpawned event) {
+ var gameObject = event.getGameObject();
+ if (gameObject == null) {
+ return;
+ }
+
+ var renderable = gameObject.getRenderable();
+ if (!(renderable instanceof net.runelite.api.DynamicObject)) {
+ return; // not an animating dynamic object
+ }
+
+ var dyn = (net.runelite.api.DynamicObject) renderable;
+ var anim = dyn.getAnimation();
+ if (anim == null) {
+ return;
+ }
+
+ final var animId = anim.getId();
+ final var isCrateAnim = TRIAL_CRATE_ANIMS.contains(animId);
+ final var isSpeedAnim = SPEED_BOOST_ANIMS.contains(animId);
+
+ if (!isCrateAnim && !isSpeedAnim) {
+ return; // ignore unrelated animations
+ }
+
+ var wp = gameObject.getWorldLocation();
+
+ var objectComposition = client.getObjectDefinition(gameObject.getId());
+ if (objectComposition.getImpostorIds() == null) {
+ var name = objectComposition.getName();
+ if (Strings.isNullOrEmpty(name) || name.equals("null")) {
+ return;
+ }
+ }
+
+ var minLocation = gameObject.getSceneMinLocation();
+ var poly = gameObject.getCanvasTilePoly();
+
+ var type = isCrateAnim ? "CRATE" : "SPEED BOOST";
+ if (wp != null) {
+ if (isCrateAnim) {
+ log.info("[SPAWN] {} -> GameObject id={} world={} (hash={}) minLocation={} poly={}", type, animId, gameObject.getId(), wp, gameObject.getHash(), minLocation, poly);
+ }
+
+ } else {
+ log.info("[SPAWN] {} -> GameObject id={} (no world point available)", type, gameObject.getId());
+ }
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/AllSails.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/AllSails.java
new file mode 100644
index 0000000000..0b2123ee95
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/AllSails.java
@@ -0,0 +1,30 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+import net.runelite.api.gameval.ObjectID;
+
+import java.util.List;
+
+public class AllSails {
+ public static final List GAMEOBJECT_IDS = List.of(
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_1X3_WOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_2X5_WOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_WOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_1X3_OAK,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_2X5_OAK,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_OAK,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_1X3_TEAK,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_2X5_TEAK,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_TEAK,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_MAHOGANY,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_MAHOGANY,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_MAHOGANY,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_1X3_CAMPHOR,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_2X5_CAMPHOR,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_CAMPHOR,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_1X3_IRONWOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_2X5_IRONWOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_IRONWOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_1X3_ROSEWOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_2X5_ROSEWOOD,
+ ObjectID.SAILING_BOAT_SAIL_KANDARIN_3X8_ROSEWOOD);
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/Directions.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/Directions.java
new file mode 100644
index 0000000000..f727aadbb6
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/Directions.java
@@ -0,0 +1,20 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public enum Directions {
+ South,
+ SouthSouthWest,
+ SouthWest,
+ WestSouthWest,
+ West,
+ WestNorthWest,
+ NorthWest,
+ NorthNorthWest,
+ North,
+ NorthNorthEast,
+ NorthEast,
+ EastNorthEast,
+ East,
+ EastSouthEast,
+ SouthEast,
+ SouthSouthEast
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObjectiveInfo.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObjectiveInfo.java
new file mode 100644
index 0000000000..fbb99b543c
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObjectiveInfo.java
@@ -0,0 +1,11 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public class ObjectiveInfo {
+ public int Collected;
+ public int TotalNeeded;
+
+ public ObjectiveInfo() {
+ Collected = 0;
+ TotalNeeded = 0;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObstacleTracking.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObstacleTracking.java
new file mode 100644
index 0000000000..3562d475a5
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObstacleTracking.java
@@ -0,0 +1,32 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+import java.util.Set;
+
+public class ObstacleTracking {
+ public static final Set OBSTACLE_GAMEOBJECT_IDS = Set.of(
+ //jubbly jive obstacles
+ 60359,
+ 60360,
+ 60361,
+ 60362,
+ 60363,
+ 60438,
+ 60440,
+ 60441,
+ 60442,
+ 60443,
+
+ //gwenith glide obstacles
+ 59109,
+ 59116,
+ 59110,
+ 59108,
+ 59094,
+ 59095,
+ 59098,
+ 59115,
+ 60415,
+ 58978
+ //
+ );
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalColors.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalColors.java
new file mode 100644
index 0000000000..3dbfd92794
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalColors.java
@@ -0,0 +1,12 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public enum PortalColors {
+ White,
+ Blue,
+ Green,
+ Yellow,
+ Red,
+ Black,
+ Cyan,
+ Pink
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalDirection.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalDirection.java
new file mode 100644
index 0000000000..a945cd2ce7
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalDirection.java
@@ -0,0 +1,16 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public class PortalDirection {
+ public int Index;
+ public PortalColors Color;
+ public Directions BoatDirection;
+ public Directions FirstMovementDirection;
+
+ public PortalDirection(int index, PortalColors color, Directions boatDirection, Directions firstMovementDirection) {
+ Index = index;
+ Color = color;
+ BoatDirection = boatDirection;
+ FirstMovementDirection = firstMovementDirection;
+ }
+
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagColors.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagColors.java
new file mode 100644
index 0000000000..9664105ff1
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagColors.java
@@ -0,0 +1,12 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public enum ToadFlagColors {
+ Green,
+ Yellow,
+ Red,
+ Blue,
+ Orange,
+ Teal,
+ Pink,
+ White
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagGameObject.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagGameObject.java
new file mode 100644
index 0000000000..8d09c4310f
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagGameObject.java
@@ -0,0 +1,42 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+import java.util.List;
+import java.util.Set;
+
+public class ToadFlagGameObject {
+ public Set GameObjectIds;
+ public ToadFlagColors Color;
+
+ public ToadFlagGameObject(Set gameObjectIds, ToadFlagColors color) {
+ GameObjectIds = gameObjectIds;
+ Color = color;
+ }
+
+ public static ToadFlagGameObject Green = new ToadFlagGameObject(Set.of(59121, 59124), ToadFlagColors.Green);
+ public static ToadFlagGameObject Yellow = new ToadFlagGameObject(Set.of(59127, 59130), ToadFlagColors.Yellow);
+ public static ToadFlagGameObject Red = new ToadFlagGameObject(Set.of(59133, 59136), ToadFlagColors.Red);
+ public static ToadFlagGameObject Blue = new ToadFlagGameObject(Set.of(59139, 59142), ToadFlagColors.Blue);
+ public static ToadFlagGameObject Orange = new ToadFlagGameObject(Set.of(59145, 59148), ToadFlagColors.Orange);
+ public static ToadFlagGameObject Teal = new ToadFlagGameObject(Set.of(59151, 59154), ToadFlagColors.Teal);
+ public static ToadFlagGameObject Pink = new ToadFlagGameObject(Set.of(59157, 59160), ToadFlagColors.Pink);
+ public static ToadFlagGameObject White = new ToadFlagGameObject(Set.of(59163, 59166), ToadFlagColors.White);
+
+ public static List All = List.of(
+ Green,
+ Yellow,
+ Red,
+ Blue,
+ Orange,
+ Teal,
+ Pink,
+ White);
+
+ public static ToadFlagGameObject getByColor(ToadFlagColors color) {
+ for (var toadGameObject : All) {
+ if (toadGameObject.Color == color) {
+ return toadGameObject;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialInfo.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialInfo.java
new file mode 100644
index 0000000000..02a006de46
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialInfo.java
@@ -0,0 +1,158 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+import com.google.common.base.Strings;
+import net.runelite.api.Client;
+import net.runelite.api.gameval.InterfaceID.SailingBtHud;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+public class TrialInfo {
+
+ public TrialLocations Location;
+ public TrialRanks Rank;
+
+ public int CurrentTimeSeconds;
+ public int GoalTimeSeconds;
+
+ public int CollectedPrimaryObjectives;
+ public int TotalPrimaryObjectivesNeeded;
+
+ public int CollectedCrates;
+ public int TotalCratesNeeded;
+
+ public boolean HasRum;
+ public boolean HasToads;
+ public int ToadCount;
+
+ public static TrialInfo getCurrent(Client client) {
+ var trialWidget = client.getWidget(SailingBtHud.BARRACUDA_TRIALS);
+ if (trialWidget == null || trialWidget.isHidden()) {
+ return null;
+ }
+
+ var info = new TrialInfo();
+ var locationText = Objects.requireNonNull(Objects.requireNonNull(client.getWidget(SailingBtHud.BT_TITLE)).getChild(9)).getText();
+ info.Location = parseLocation(locationText);
+
+ var rankSprite = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_RANK_GFX)).getSpriteId();
+ info.Rank = parseRank(rankSprite);
+
+ var currentTimeSecondsString = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_CURRENT_TIME)).getText();
+ info.CurrentTimeSeconds = parseTimeSeconds(currentTimeSecondsString);
+
+ var goalTimeSecondsString = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_RANK_TIME)).getText();
+ info.GoalTimeSeconds = parseTimeSeconds(goalTimeSecondsString);
+
+ var primaryObjectiveText = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_TRACKER_PROGRESS)).getText();
+ var crateText = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_OPTIONAL_PROGRESS)).getText();
+
+ var primaryObjectiveInfo = parseObjectiveText(primaryObjectiveText);
+ info.CollectedPrimaryObjectives = primaryObjectiveInfo.Collected;
+ info.TotalPrimaryObjectivesNeeded = primaryObjectiveInfo.TotalNeeded;
+
+ var crateInfo = parseObjectiveText(crateText);
+ info.CollectedCrates = crateInfo.Collected;
+ info.TotalCratesNeeded = crateInfo.TotalNeeded;
+
+ var partialGfxSpriteId = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_PARTIAL_GFX)).getSpriteId();
+ info.HasRum = HasRum(info.Location, partialGfxSpriteId);
+ info.HasToads = HasToads(info.Location, partialGfxSpriteId);
+
+ var partialText = Objects.requireNonNull(client.getWidget(SailingBtHud.BT_PARTIAL_TEXT)).getText();
+ info.ToadCount = ToadCount(info.Location, partialText);
+
+ return info;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Location=%s, Rank=%s, PrimaryObjectives=%d/%d, Crates=%d/%d, HasRum=%b, HasFrogs=%b", Location.toString(), Rank.toString(), CollectedPrimaryObjectives, TotalPrimaryObjectivesNeeded, CollectedCrates, TotalCratesNeeded, HasRum, HasToads);
+ }
+
+ private static int parseTimeSeconds(String timeText) {
+ if (Strings.isNullOrEmpty(timeText)) {
+ return 0;
+ }
+
+ var pattern = Pattern.compile("(\\d+):(\\d+)");
+ var matcher = pattern.matcher(timeText);
+ if (matcher.find() && matcher.groupCount() == 2) {
+ var minutes = Integer.parseInt(matcher.group(1));
+ var seconds = Integer.parseInt(matcher.group(2));
+ return minutes * 60 + seconds;
+ }
+ return 0;
+ }
+
+ private static boolean HasToads(TrialLocations location, int spriteId) {
+ if (Objects.requireNonNull(location) == TrialLocations.JubblyJive) {
+ return spriteId == 7024;
+ }
+ return false;
+ }
+
+ private static int ToadCount(TrialLocations location, String text) {
+ if (Objects.requireNonNull(location) == TrialLocations.JubblyJive) {
+ var pattern = Pattern.compile("(\\d+)");
+ var matcher = pattern.matcher(text);
+ if (matcher.find() && matcher.groupCount() == 1) {
+ return Integer.parseInt(matcher.group(1));
+ }
+ return 0;
+ }
+ return 0;
+ }
+
+ private static boolean HasRum(TrialLocations location, int spriteId) {
+ if (Objects.requireNonNull(location) == TrialLocations.TemporTantrum) {
+ return spriteId == 7022;
+ }
+ return false;
+ }
+
+ private static ObjectiveInfo parseObjectiveText(String text) {
+ var info = new ObjectiveInfo();
+ if (Strings.isNullOrEmpty(text)) {
+ return info;
+ }
+
+ var pattern = Pattern.compile("(\\d+) / (\\d+).*?");
+ var matcher = pattern.matcher(text);
+ if (matcher.find() && matcher.groupCount() == 2) {
+ info.Collected = Integer.parseInt(matcher.group(1));
+ info.TotalNeeded = Integer.parseInt(matcher.group(2));
+ }
+ return info;
+ }
+
+ private static TrialRanks parseRank(int spriteId) {
+ switch (spriteId) {
+ case 7026:
+ return TrialRanks.Unranked;// todo check
+ case 7027:
+ return TrialRanks.Swordfish;
+ case 7028:
+ return TrialRanks.Shark;
+ case 7029:
+ return TrialRanks.Marlin;
+ }
+ return TrialRanks.Unknown;
+ }
+
+ private static TrialLocations parseLocation(String text) {
+ if (Strings.isNullOrEmpty(text)) {
+ return TrialLocations.Unknown;
+ }
+
+ switch (text) {
+ case "Gwenith Glide":
+ return TrialLocations.GwenithGlide;
+ case "Jubbly Jive":
+ return TrialLocations.JubblyJive;
+ case "Tempor Tantrum":
+ return TrialLocations.TemporTantrum;
+ }
+ return TrialLocations.Unknown;
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialLocations.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialLocations.java
new file mode 100644
index 0000000000..77b2aea3e9
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialLocations.java
@@ -0,0 +1,9 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public enum TrialLocations {
+ TemporTantrum,
+ JubblyJive,
+ GwenithGlide,
+
+ Unknown
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRanks.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRanks.java
new file mode 100644
index 0000000000..35f37234c3
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRanks.java
@@ -0,0 +1,10 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+public enum TrialRanks {
+ Unranked,
+ Swordfish,
+ Shark,
+ Marlin,
+
+ Unknown,
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRoute.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRoute.java
new file mode 100644
index 0000000000..f51b98eea3
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRoute.java
@@ -0,0 +1,892 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.data;
+
+import net.runelite.api.coords.WorldPoint;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class TrialRoute {
+ public TrialLocations Location;
+ public TrialRanks Rank;
+ public List Points;
+ public List ToadOrder;
+ public List WindMoteIndices;
+ public List PortalDirections;
+ private List interpolatedPoints;
+ private Map originalToInterpolatedIndexMap;
+ private boolean interpolationApplied = false;
+
+ private static final double DEFAULT_SPACING = 5.0;
+
+ public TrialRoute(TrialLocations location, TrialRanks rank, List points) {
+ Location = location;
+ Rank = rank;
+ Points = points == null ? new ArrayList<>() : new ArrayList<>(points);
+ ToadOrder = Collections.emptyList();
+ WindMoteIndices = Collections.emptyList();
+ PortalDirections = Collections.emptyList();
+ }
+
+ public TrialRoute(TrialLocations location, TrialRanks rank, List points, List toadOrder, List windMoteIndices, List portalDirections) {
+ this(location, rank, points);
+ ToadOrder = toadOrder == null ? Collections.emptyList() : toadOrder;
+ WindMoteIndices = windMoteIndices == null ? Collections.emptyList() : windMoteIndices;
+ PortalDirections = portalDirections == null ? Collections.emptyList() : portalDirections;
+ }
+
+ public List getInterpolatedPoints() {
+ if (!interpolationApplied) {
+ originalToInterpolatedIndexMap = new HashMap<>();
+ interpolatedPoints = interpolateRoute(Points, DEFAULT_SPACING, originalToInterpolatedIndexMap);
+ interpolationApplied = true;
+ }
+ return interpolatedPoints;
+ }
+
+ public int getInterpolatedIndex(int originalIndex) {
+ if (!interpolationApplied) {
+ getInterpolatedPoints();
+ }
+ return originalToInterpolatedIndexMap.getOrDefault(originalIndex, originalIndex);
+ }
+
+ public static List interpolateRoute(List originalPoints, double spacing, Map indexMapping) {
+ if (originalPoints == null || originalPoints.size() < 2) {
+ if (indexMapping != null && originalPoints != null) {
+ for (int i = 0; i < originalPoints.size(); i++) {
+ indexMapping.put(i, i);
+ }
+ }
+ return originalPoints == null ? new ArrayList<>() : new ArrayList<>(originalPoints);
+ }
+
+ List result = new ArrayList<>();
+ result.add(originalPoints.get(0));
+ if (indexMapping != null) {
+ indexMapping.put(0, 0);
+ }
+
+ for (int i = 0; i < originalPoints.size() - 1; i++) {
+ WorldPoint current = originalPoints.get(i);
+ WorldPoint next = originalPoints.get(i + 1);
+
+ List segment = interpolateSegment(current, next, spacing);
+ for (int j = 1; j < segment.size(); j++) {
+ result.add(segment.get(j));
+ }
+
+ if (indexMapping != null) {
+ indexMapping.put(i + 1, result.size() - 1);
+ }
+ }
+
+ return result;
+ }
+
+ private static List interpolateSegment(WorldPoint from, WorldPoint to, double spacing) {
+ List points = new ArrayList<>();
+ points.add(from);
+
+ double dx = to.getX() - from.getX();
+ double dy = to.getY() - from.getY();
+ double distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= spacing) {
+ points.add(to);
+ return points;
+ }
+
+ int numSegments = (int) Math.ceil(distance / spacing);
+ double stepX = dx / numSegments;
+ double stepY = dy / numSegments;
+
+ for (int i = 1; i < numSegments; i++) {
+ int x = from.getX() + (int) Math.round(stepX * i);
+ int y = from.getY() + (int) Math.round(stepY * i);
+ points.add(new WorldPoint(x, y, from.getPlane()));
+ }
+
+ points.add(to);
+ return points;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ TrialRoute that = (TrialRoute) o;
+ return Location == that.Location && Rank == that.Rank;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Location, Rank);
+ }
+
+ private static final List TemporTantrumSwordfishBestLine = new ArrayList<>(
+ List.of(
+ new WorldPoint(3035, 2922, 0), // start
+ new WorldPoint(3025, 2911, 0),
+ new WorldPoint(3017, 2900, 0),
+ new WorldPoint(2996, 2896, 0),
+ new WorldPoint(2994, 2882, 0),
+ new WorldPoint(2979, 2866, 0),
+ new WorldPoint(2983, 2839, 0),
+ new WorldPoint(2979, 2827, 0),
+ new WorldPoint(2990, 2809, 0),
+ new WorldPoint(3001, 2787, 0),
+ new WorldPoint(3013, 2769, 0),
+ new WorldPoint(3022, 2762, 0),
+ new WorldPoint(3039, 2760, 0),
+ new WorldPoint(3056, 2763, 0),
+ new WorldPoint(3054, 2763, 0),
+ new WorldPoint(3057, 2792, 0),
+ new WorldPoint(3065, 2811, 0),
+ new WorldPoint(3078, 2827, 0),
+ new WorldPoint(3078, 2864, 0),
+ new WorldPoint(3084, 2875, 0),
+ new WorldPoint(3091, 2887, 0),
+ new WorldPoint(3072, 2916, 0),
+ new WorldPoint(3052, 2920, 0),
+ new WorldPoint(3035, 2922, 0) // end
+ ));
+
+ private static final List TemporTantrumSharkBestLine = new ArrayList<>(
+ List.of(
+ new WorldPoint(3035, 2922, 0), // start
+ new WorldPoint(3017, 2898, 0),
+ new WorldPoint(3017, 2889, 0),
+ new WorldPoint(3001, 2869, 0),
+ new WorldPoint(3002, 2858, 0),
+ new WorldPoint(3004, 2827, 0),
+ new WorldPoint(3009, 2816, 0),
+ new WorldPoint(3019, 2814, 0),
+ new WorldPoint(3027, 2798, 0),
+ new WorldPoint(3039, 2778, 0),
+ new WorldPoint(3045, 2777, 0),
+ new WorldPoint(3057, 2792, 0),
+ new WorldPoint(3069, 2814, 0),
+ new WorldPoint(3076, 2825, 0),
+ new WorldPoint(3082, 2873, 0),
+ new WorldPoint(3076, 2883, 0),
+ new WorldPoint(3077, 2896, 0),
+ new WorldPoint(3060, 2906, 0),
+ new WorldPoint(3040, 2921, 0),
+ new WorldPoint(3027, 2913, 0),
+ new WorldPoint(3013, 2910, 0),
+ new WorldPoint(2994, 2896, 0),
+ new WorldPoint(2994, 2882, 0),
+ new WorldPoint(2977, 2865, 0),
+ new WorldPoint(2982, 2847, 0),
+ new WorldPoint(2979, 2830, 0),
+ new WorldPoint(2991, 2806, 0),
+ new WorldPoint(3014, 2763, 0),
+ new WorldPoint(3038, 2758, 0),
+ new WorldPoint(3054, 2761, 0),
+ new WorldPoint(3066, 2768, 0),
+ new WorldPoint(3075, 2776, 0),
+ new WorldPoint(3084, 2801, 0),
+ new WorldPoint(3081, 2813, 0),
+ new WorldPoint(3094, 2828, 0),
+ new WorldPoint(3093, 2843, 0),
+ new WorldPoint(3093, 2864, 0),
+ new WorldPoint(3100, 2872, 0),
+ new WorldPoint(3092, 2884, 0),
+ new WorldPoint(3073, 2916, 0),
+ new WorldPoint(3053, 2921, 0),
+ new WorldPoint(3035, 2922, 0) // end
+ ));
+
+ private static final List TemporTantrumMarlinBestLine = new ArrayList<>(
+ List.of(
+ // Trial Route:
+ /*0*/new WorldPoint(3035, 2922, 0),
+ /*1*/new WorldPoint(3017, 2898, 0),
+ /*2*/new WorldPoint(3017, 2889, 0),
+ /*3*/new WorldPoint(3001, 2869, 0),
+ /*4*/new WorldPoint(3002, 2858, 0),
+ /*5*/new WorldPoint(3004, 2827, 0),
+ /*6*/new WorldPoint(3009, 2816, 0),
+ /*7*/new WorldPoint(3019, 2814, 0),
+ /*8*/new WorldPoint(3030, 2815, 0),
+ /*9*/new WorldPoint(3027, 2798, 0),
+ /*10*/new WorldPoint(3039, 2778, 0),
+ /*11*/new WorldPoint(3045, 2777, 0),
+ /*12*/new WorldPoint(3057, 2792, 0),
+ /*13*/new WorldPoint(3069, 2814, 0),
+ /*14*/new WorldPoint(3076, 2825, 0),
+ /*15*/new WorldPoint(3078, 2863, 0),
+ /*16*/new WorldPoint(3082, 2873, 0),
+ /*17*/new WorldPoint(3073, 2875, 0),
+ /*18*/new WorldPoint(3060, 2882, 0),
+ /*19*/new WorldPoint(3060, 2906, 0),
+ /*20*/new WorldPoint(3035, 2917, 0),
+ /*21*/new WorldPoint(3027, 2913, 0),
+ /*22*/new WorldPoint(3013, 2910, 0),
+ /*23*/new WorldPoint(2994, 2896, 0),
+ /*24*/new WorldPoint(2994, 2882, 0),
+ /*25*/new WorldPoint(2977, 2865, 0),
+ /*26*/new WorldPoint(2982, 2847, 0),
+ /*27*/new WorldPoint(2979, 2830, 0),
+ /*28*/new WorldPoint(2991, 2806, 0),
+ /*29*/new WorldPoint(3016, 2776, 0),
+ /*30*/new WorldPoint(3038, 2771, 0),
+ /*31*/new WorldPoint(3045, 2776, 0),
+ /*32*/new WorldPoint(3066, 2768, 0),
+ /*33*/new WorldPoint(3075, 2776, 0),
+ /*34*/new WorldPoint(3084, 2801, 0),
+ /*35*/new WorldPoint(3081, 2813, 0),
+ /*36*/new WorldPoint(3094, 2828, 0),
+ /*37*/new WorldPoint(3093, 2843, 0),
+ /*38*/new WorldPoint(3093, 2864, 0),
+ /*39*/new WorldPoint(3100, 2872, 0),
+ /*40*/new WorldPoint(3092, 2884, 0),
+ /*41*/new WorldPoint(3073, 2916, 0),
+ /*42*/new WorldPoint(3053, 2921, 0),
+ /*43*/new WorldPoint(3035, 2922, 0),
+ /*44*/new WorldPoint(3012, 2910, 0),
+ /*45*/new WorldPoint(2993, 2895, 0),
+ /*46*/new WorldPoint(2979, 2883, 0),
+ /*47*/new WorldPoint(2962, 2882, 0),
+ /*48*/new WorldPoint(2956, 2872, 0),
+ /*49*/new WorldPoint(2965, 2865, 0),
+ /*50*/new WorldPoint(2966, 2851, 0),
+ /*51*/new WorldPoint(2958, 2840, 0),
+ /*52*/new WorldPoint(2953, 2810, 0),
+ /*53*/new WorldPoint(2968, 2794, 0),
+ /*54*/new WorldPoint(2983, 2787, 0),
+ /*55*/new WorldPoint(2987, 2777, 0),
+ /*56*/new WorldPoint(3004, 2768, 0),
+ /*57*/new WorldPoint(3039, 2758, 0),
+ /*58*/new WorldPoint(3056, 2761, 0),
+ /*59*/new WorldPoint(3068, 2766, 0),
+ /*60*/new WorldPoint(3090, 2764, 0),
+ /*61*/new WorldPoint(3098, 2774, 0),
+ /*62*/new WorldPoint(3103, 2797, 0),
+ /*63*/new WorldPoint(3110, 2825, 0),
+ /*64*/new WorldPoint(3118, 2836, 0),
+ /*65*/new WorldPoint(3117, 2850, 0),
+ /*66*/new WorldPoint(3121, 2864, 0),
+ /*67*/new WorldPoint(3103, 2878, 0),
+ /*68*/new WorldPoint(3082, 2900, 0),
+ /*69*/new WorldPoint(3072, 2917, 0),
+ /*70*/new WorldPoint(3059, 2921, 0),
+ /*71*/new WorldPoint(3035, 2922, 0)
+ // End of trial route
+ ));
+
+ private static final List JubblySwordfishBestLine = new ArrayList<>(
+ List.of(
+ new WorldPoint(2436, 3018, 0),
+ new WorldPoint(2423, 3012, 0),
+ new WorldPoint(2413, 3015, 0),
+ new WorldPoint(2396, 3010, 0),
+ new WorldPoint(2373, 3008, 0),
+ new WorldPoint(2357, 2991, 0),
+ new WorldPoint(2353, 2979, 0),
+ new WorldPoint(2342, 2974, 0),
+ new WorldPoint(2323, 2976, 0),
+ new WorldPoint(2309, 2974, 0),
+ new WorldPoint(2285, 2980, 0),
+ new WorldPoint(2267, 2990, 0),
+ new WorldPoint(2251, 2995, 0),
+ new WorldPoint(2239, 3005, 0),
+ new WorldPoint(2239, 3016, 0),
+ new WorldPoint(2252, 3025, 0),
+ new WorldPoint(2261, 3021, 0),
+ new WorldPoint(2281, 2999, 0),
+ new WorldPoint(2298, 3002, 0),
+ new WorldPoint(2300, 3014, 0),
+ new WorldPoint(2311, 3021, 0),
+ new WorldPoint(2352, 3004, 0),
+ new WorldPoint(2360, 2999, 0),
+ new WorldPoint(2358, 2969, 0),
+ new WorldPoint(2358, 2960, 0),
+ new WorldPoint(2374, 2940, 0),
+ new WorldPoint(2428, 2939, 0),
+ new WorldPoint(2435, 2949, 0),
+ new WorldPoint(2436, 2985, 0),
+ new WorldPoint(2437, 2990, 0),
+ new WorldPoint(2433, 3005, 0),
+ new WorldPoint(2436, 3018, 0)//end
+ ));
+
+ private static final List JubblySwordfishToadOrder = List.of(
+ ToadFlagColors.Orange,
+ ToadFlagColors.Teal,
+ ToadFlagColors.Pink,
+ ToadFlagColors.White//end
+ );
+
+ private static final List JubblySharkBestLine = new ArrayList<>(
+ List.of(
+ new WorldPoint(2436, 3018, 0),
+ new WorldPoint(2422, 3012, 0),
+ new WorldPoint(2413, 3016, 0),
+ new WorldPoint(2402, 3017, 0),
+ new WorldPoint(2395, 3010, 0),
+ new WorldPoint(2378, 3008, 0),
+ new WorldPoint(2362, 2998, 0),
+ new WorldPoint(2351, 2979, 0),
+ new WorldPoint(2340, 2973, 0),
+ new WorldPoint(2330, 2974, 0),
+ new WorldPoint(2299, 2975, 0),
+ new WorldPoint(2276, 2984, 0),
+ new WorldPoint(2263, 2992, 0),
+ new WorldPoint(2250, 2993, 0),
+ new WorldPoint(2239, 3007, 0), // collect toad
+ new WorldPoint(2240, 3016, 0),
+ new WorldPoint(2250, 3023, 0),
+ new WorldPoint(2253, 3025, 0),
+ new WorldPoint(2261, 3021, 0),
+ new WorldPoint(2278, 3001, 0),
+ new WorldPoint(2295, 3000, 0), // click yellow outcrop
+ new WorldPoint(2299, 3007, 0),
+ new WorldPoint(2302, 3017, 0), // click red outcrop
+ new WorldPoint(2310, 3021, 0),
+ new WorldPoint(2329, 3016, 0),
+ new WorldPoint(2339, 3004, 0),
+ new WorldPoint(2345, 2990, 0),
+ new WorldPoint(2359, 2974, 0),
+ new WorldPoint(2358, 2965, 0),
+ new WorldPoint(2365, 2948, 0), // click yellow outcrop
+ new WorldPoint(2373, 2939, 0),
+ new WorldPoint(2386, 2940, 0),
+ new WorldPoint(2399, 2939, 0),
+ new WorldPoint(2420, 2938, 0), // click green outcrop
+ new WorldPoint(2426, 2936, 0),
+ new WorldPoint(2434, 2949, 0),
+ new WorldPoint(2434, 2969, 0),
+ new WorldPoint(2438, 2989, 0),
+ new WorldPoint(2438, 2989, 0), // click pink outcrop
+ new WorldPoint(2434, 2998, 0),
+ new WorldPoint(2432, 3021, 0),
+ new WorldPoint(2413, 3026, 0), // click white outcrop
+ new WorldPoint(2402, 3021, 0),
+ new WorldPoint(2394, 3020, 0),
+ new WorldPoint(2382, 3025, 0),
+ new WorldPoint(2370, 3022, 0),
+ new WorldPoint(2357, 3025, 0),
+ new WorldPoint(2340, 3031, 0),
+ new WorldPoint(2333, 3028, 0),
+ new WorldPoint(2327, 3016, 0),
+ new WorldPoint(2339, 3006, 0),
+ new WorldPoint(2353, 3005, 0), // click blue outcrop
+ new WorldPoint(2379, 2993, 0),
+ new WorldPoint(2384, 2985, 0),
+ new WorldPoint(2379, 2974, 0),
+ new WorldPoint(2388, 2959, 0), // click orange outcrop
+ new WorldPoint(2403, 2951, 0),
+ new WorldPoint(2413, 2955, 0),
+ new WorldPoint(2420, 2959, 0), // click teal outcrop
+ new WorldPoint(2424, 2974, 0),
+ new WorldPoint(2418, 2988, 0), // click pink outcrop
+ new WorldPoint(2414, 2993, 0),
+ new WorldPoint(2417, 3003, 0), //click white outcrop
+ new WorldPoint(2436, 3023, 0) // end
+ ));
+
+ private static final List JubblySharkToadOrder = List.of(
+ ToadFlagColors.Yellow,
+ ToadFlagColors.Red,
+ ToadFlagColors.Orange,
+ ToadFlagColors.Teal,
+ ToadFlagColors.Pink,
+ ToadFlagColors.White,
+ ToadFlagColors.Blue,
+ ToadFlagColors.Orange,
+ ToadFlagColors.Teal,
+ ToadFlagColors.Pink,
+ ToadFlagColors.White //fin
+ );
+
+ private static final List JubblyMarlinBestLine = new ArrayList<>(
+ List.of(
+ /*0*/new WorldPoint(2436, 3018, 0),
+ /*1*/new WorldPoint(2424, 3025, 0),
+ /*2*/new WorldPoint(2412, 3026, 0),
+ /*3*/new WorldPoint(2405, 3023, 0),
+ /*4*/new WorldPoint(2400, 3011, 0),
+ /*5*/new WorldPoint(2396, 3009, 0),
+ /*6*/new WorldPoint(2373, 3009, 0),
+ /*7*/new WorldPoint(2350, 2977, 0),
+ /*8*/new WorldPoint(2332, 2974, 0),
+ /*9*/new WorldPoint(2303, 2975, 0),
+ /*10*/new WorldPoint(2281, 2980, 0),
+ /*11*/new WorldPoint(2265, 2991, 0),
+ /*12*/new WorldPoint(2251, 2994, 0),
+ /*13*/new WorldPoint(2248, 3000, 0),
+ /*14*/new WorldPoint(2268, 3013, 0),
+ /*15*/new WorldPoint(2280, 3000, 0),
+ /*16*/new WorldPoint(2298, 3001, 0),
+ /*17*/new WorldPoint(2302, 3017, 0),
+ /*18*/new WorldPoint(2316, 3023, 0),
+ /*19*/new WorldPoint(2350, 2981, 0),
+ /*20*/new WorldPoint(2359, 2958, 0),
+ /*21*/new WorldPoint(2375, 2936, 0),
+ /*22*/new WorldPoint(2387, 2940, 0),
+ /*23*/new WorldPoint(2420, 2939, 0),
+ /*24*/new WorldPoint(2434, 2942, 0),
+ /*25*/new WorldPoint(2435, 2967, 0),
+ /*26*/new WorldPoint(2435, 2986, 0),
+ /*27*/new WorldPoint(2437, 2991, 0),
+ /*28*/new WorldPoint(2433, 3004, 0),
+ /*29*/new WorldPoint(2435, 3010, 0),
+ /*30*/new WorldPoint(2422, 3012, 0),
+ /*31*/new WorldPoint(2415, 3000, 0),
+ /*32*/new WorldPoint(2414, 2990, 0),
+ /*33*/new WorldPoint(2423, 2978, 0),
+ /*34*/new WorldPoint(2421, 2964, 0),
+ /*35*/new WorldPoint(2417, 2957, 0),
+ /*36*/new WorldPoint(2405, 2950, 0),
+ /*37*/new WorldPoint(2390, 2957, 0),
+ /*38*/new WorldPoint(2380, 2974, 0),
+ /*39*/new WorldPoint(2384, 2985, 0),
+ /*40*/new WorldPoint(2384, 2989, 0),
+ /*41*/new WorldPoint(2369, 2997, 0),
+ /*42*/new WorldPoint(2359, 2991, 0),
+ /*43*/new WorldPoint(2350, 2977, 0),
+ /*44*/new WorldPoint(2340, 2974, 0),
+ /*45*/new WorldPoint(2305, 2974, 0),
+ /*46*/new WorldPoint(2288, 2980, 0),
+ /*47*/new WorldPoint(2278, 2981, 0),
+ /*48*/new WorldPoint(2268, 2990, 0),
+ /*49*/new WorldPoint(2256, 2992, 0),
+ /*50*/new WorldPoint(2238, 3006, 0),
+ /*51*/new WorldPoint(2242, 3020, 0),
+ /*52*/new WorldPoint(2251, 3025, 0),
+ /*53*/new WorldPoint(2257, 3024, 0),
+ /*54*/new WorldPoint(2280, 2999, 0),
+ /*55*/new WorldPoint(2292, 2997, 0), //wind mote HERE
+ /*56*/new WorldPoint(2312, 2987, 0),
+ /*57*/new WorldPoint(2324, 2984, 0),
+ /*58*/new WorldPoint(2333, 2977, 0),
+ /*59*/new WorldPoint(2335, 2954, 0),
+ /*60*/new WorldPoint(2345, 2931, 0),
+ /*61*/new WorldPoint(2365, 2928, 0),
+ /*62*/new WorldPoint(2378, 2942, 0),
+ /*63*/new WorldPoint(2395, 2939, 0),
+ /*64*/new WorldPoint(2400, 2927, 0),
+ /*65*/new WorldPoint(2417, 2924, 0),
+ /*66*/new WorldPoint(2427, 2921, 0),
+ /*67*/new WorldPoint(2442, 2927, 0),
+ /*68*/new WorldPoint(2454, 2930, 0),
+ /*69*/new WorldPoint(2469, 2953, 0),
+ /*70*/new WorldPoint(2447, 2974, 0),
+ /*71*/new WorldPoint(2447, 2986, 0), //wind mote HERE
+ /*72*/new WorldPoint(2445, 3009, 0),
+ /*73*/new WorldPoint(2437, 3010, 0),
+ /*74*/new WorldPoint(2434, 3007, 0),
+ /*75*/new WorldPoint(2403, 3017, 0),
+ /*76*/new WorldPoint(2395, 3020, 0),
+ /*77*/new WorldPoint(2387, 3020, 0),
+ /*78*/new WorldPoint(2378, 3026, 0),
+ /*79*/new WorldPoint(2371, 3022, 0),
+ /*80*/new WorldPoint(2355, 3023, 0),
+ /*81*/new WorldPoint(2343, 3031, 0),
+ /*82*/new WorldPoint(2329, 3030, 0),
+ /*83*/new WorldPoint(2313, 3045, 0),
+ /*84*/new WorldPoint(2304, 3038, 0),
+ /*85*/new WorldPoint(2313, 3025, 0),
+ /*86*/new WorldPoint(2341, 3007, 0),
+ /*87*/new WorldPoint(2355, 3005, 0),
+ /*88*/new WorldPoint(2361, 3000, 0),
+ /*89*/new WorldPoint(2390, 2986, 0),
+ /*90*/new WorldPoint(2418, 2961, 0),
+ /*91*/new WorldPoint(2430, 2953, 0), //shoot teal
+ /*92*/new WorldPoint(2435, 2965, 0),
+ /*93*/new WorldPoint(2433, 3000, 0),
+ new WorldPoint(2436, 3023, 0) // end
+ ));
+
+ private static final List JubblyMarlinToadOrder = List.of(
+ ToadFlagColors.Yellow,
+ ToadFlagColors.Red,
+ ToadFlagColors.Orange,
+ ToadFlagColors.Teal,
+ ToadFlagColors.Pink,
+ ToadFlagColors.White,
+ ToadFlagColors.Teal,
+ ToadFlagColors.Orange,
+ ToadFlagColors.Blue,
+ ToadFlagColors.Yellow,
+ ToadFlagColors.Orange,
+ ToadFlagColors.White,
+ ToadFlagColors.Pink,
+ ToadFlagColors.Red,
+ ToadFlagColors.Blue,
+ ToadFlagColors.Teal,
+ ToadFlagColors.Pink,
+ ToadFlagColors.White//end
+ );
+
+ private static final List JubblyMarlinWindMoteIndices = List.of(13, 55, 71, 89, 90);
+
+ private static final List GwenithGlideSwordfishBestLine = new ArrayList<>(
+ List.of(
+ new WorldPoint(2257, 3459, 0),
+ new WorldPoint(2255, 3469, 0),
+ new WorldPoint(2260, 3474, 0),
+ new WorldPoint(2271, 3477, 0),
+ new WorldPoint(2274, 3487, 0),
+ new WorldPoint(2260, 3494, 0), //white portal
+ new WorldPoint(2093, 3233, 0), //after portal
+ new WorldPoint(2103, 3230, 0),
+ new WorldPoint(2111, 3234, 0),
+ new WorldPoint(2118, 3231, 0),
+ new WorldPoint(2128, 3233, 0),
+ new WorldPoint(2130, 3253, 0),
+ new WorldPoint(2133, 3263, 0),
+ new WorldPoint(2127, 3275, 0),
+ new WorldPoint(2121, 3278, 0),
+ new WorldPoint(2121, 3289, 0),
+ new WorldPoint(2131, 3297, 0),
+ new WorldPoint(2148, 3297, 0),
+ new WorldPoint(2157, 3293, 0), //white portal
+ new WorldPoint(2260, 3509, 0),
+ new WorldPoint(2266, 3518, 0),
+ new WorldPoint(2263, 3531, 0),
+ new WorldPoint(2250, 3542, 0),
+ new WorldPoint(2252, 3558, 0),
+ new WorldPoint(2254, 3571, 0),
+ new WorldPoint(2242, 3574, 0), //blue portal
+ new WorldPoint(2088, 3215, 0),
+ new WorldPoint(2110, 3214, 0),
+ new WorldPoint(2115, 3206, 0),
+ new WorldPoint(2132, 3193, 0),
+ new WorldPoint(2141, 3220, 0),
+ new WorldPoint(2139, 3230, 0),
+ new WorldPoint(2141, 3243, 0),
+ new WorldPoint(2153, 3246, 0), //blue portal
+ new WorldPoint(2203, 3574, 0),
+ new WorldPoint(2191, 3567, 0),
+ new WorldPoint(2194, 3547, 0),
+ new WorldPoint(2201, 3535, 0),
+ new WorldPoint(2198, 3514, 0), //green portal
+ new WorldPoint(2105, 3140, 0),
+ new WorldPoint(2092, 3145, 0),
+ new WorldPoint(2078, 3158, 0),
+ new WorldPoint(2070, 3161, 0),
+ new WorldPoint(2069, 3175, 0),
+ new WorldPoint(2058, 3185, 0),
+ new WorldPoint(2073, 3210, 0),
+ new WorldPoint(2100, 3205, 0),
+ new WorldPoint(2128, 3172, 0)//end
+ ));
+
+ private static final List GwenithGlideSwordfishPortalDirections = List.of(
+ new PortalDirection(5, PortalColors.White, Directions.East, Directions.SouthEast),
+ new PortalDirection(18, PortalColors.White, Directions.North, Directions.NorthEast),
+ new PortalDirection(25, PortalColors.Blue, Directions.East, Directions.East),
+ new PortalDirection(33, PortalColors.Blue, Directions.West, Directions.SouthWest),
+ new PortalDirection(38, PortalColors.Green, Directions.West, Directions.NorthWest),
+ new PortalDirection(47, PortalColors.Green, Directions.South, Directions.SouthWest),
+ new PortalDirection(52, PortalColors.Yellow, Directions.West, Directions.West),
+ new PortalDirection(62, PortalColors.Yellow, Directions.West, Directions.West),
+ new PortalDirection(71, PortalColors.Red, Directions.West, Directions.SouthWest)//
+ //
+ );
+
+ private static final List GwenithGlideSharkBestLine = new ArrayList<>(
+ List.of(
+ new WorldPoint(2257, 3459, 0),
+ new WorldPoint(2255, 3469, 0),
+ new WorldPoint(2260, 3474, 0),
+ new WorldPoint(2271, 3477, 0),
+ new WorldPoint(2274, 3487, 0),
+ new WorldPoint(2260, 3494, 0), //white portal
+ new WorldPoint(2093, 3233, 0), //after portal
+ new WorldPoint(2103, 3230, 0),
+ new WorldPoint(2111, 3234, 0),
+ new WorldPoint(2118, 3231, 0),
+ new WorldPoint(2128, 3233, 0),
+ new WorldPoint(2130, 3253, 0),
+ new WorldPoint(2133, 3263, 0),
+ new WorldPoint(2127, 3275, 0),
+ new WorldPoint(2121, 3278, 0),
+ new WorldPoint(2121, 3289, 0),
+ new WorldPoint(2131, 3297, 0),
+ new WorldPoint(2148, 3297, 0),
+ new WorldPoint(2157, 3293, 0), //white portal
+ new WorldPoint(2260, 3509, 0),
+ new WorldPoint(2266, 3518, 0),
+ new WorldPoint(2263, 3531, 0),
+ new WorldPoint(2250, 3542, 0),
+ new WorldPoint(2252, 3558, 0),
+ new WorldPoint(2254, 3571, 0),
+ new WorldPoint(2242, 3574, 0), //blue portal
+ new WorldPoint(2088, 3215, 0),
+ new WorldPoint(2110, 3214, 0),
+ new WorldPoint(2115, 3206, 0),
+ new WorldPoint(2132, 3193, 0),
+ new WorldPoint(2141, 3220, 0),
+ new WorldPoint(2139, 3230, 0),
+ new WorldPoint(2141, 3243, 0),
+ new WorldPoint(2153, 3246, 0), //blue portal
+ new WorldPoint(2203, 3574, 0),
+ new WorldPoint(2191, 3567, 0),
+ new WorldPoint(2194, 3547, 0),
+ new WorldPoint(2201, 3535, 0),
+ new WorldPoint(2198, 3514, 0), //green portal
+ new WorldPoint(2105, 3140, 0),
+ new WorldPoint(2092, 3145, 0),
+ new WorldPoint(2078, 3158, 0),
+ new WorldPoint(2070, 3161, 0),
+ new WorldPoint(2069, 3175, 0),
+ new WorldPoint(2058, 3185, 0),
+ new WorldPoint(2073, 3210, 0),
+ new WorldPoint(2100, 3205, 0),
+ new WorldPoint(2128, 3172, 0), //green portal, end swordfish
+ new WorldPoint(2198, 3497, 0),
+ new WorldPoint(2192, 3480, 0),
+ new WorldPoint(2177, 3474, 0),
+ new WorldPoint(2171, 3465, 0),
+ new WorldPoint(2158, 3464, 0), //yellow portal
+ new WorldPoint(2115, 3373, 0),
+ new WorldPoint(2100, 3372, 0),
+ new WorldPoint(2087, 3377, 0),
+ new WorldPoint(2079, 3389, 0),
+ new WorldPoint(2094, 3397, 0),
+ new WorldPoint(2104, 3406, 0),
+ new WorldPoint(2085, 3413, 0),
+ new WorldPoint(2078, 3423, 0),
+ new WorldPoint(2098, 3437, 0),
+ new WorldPoint(2116, 3439, 0), //yellow portal
+ new WorldPoint(2143, 3464, 0),
+ new WorldPoint(2110, 3464, 0),
+ new WorldPoint(2105, 3481, 0),
+ new WorldPoint(2106, 3493, 0),
+ new WorldPoint(2125, 3495, 0),
+ new WorldPoint(2135, 3480, 0), //secret crate!
+ new WorldPoint(2151, 3490, 0),
+ new WorldPoint(2149, 3503, 0),
+ new WorldPoint(2160, 3508, 0), //red portal
+ new WorldPoint(2248, 3634, 0),
+ new WorldPoint(2240, 3628, 0),
+ new WorldPoint(2231, 3617, 0),
+ new WorldPoint(2229, 3608, 0),
+ new WorldPoint(2229, 3599, 0),
+ new WorldPoint(2216, 3593, 0),
+ new WorldPoint(2190, 3597, 0),
+ new WorldPoint(2167, 3589, 0),
+ new WorldPoint(2141, 3597, 0),
+ new WorldPoint(2123, 3598, 0),
+ new WorldPoint(2100, 3583, 0),
+ new WorldPoint(2100, 3583, 0),
+ new WorldPoint(2104, 3574, 0),
+
+ new WorldPoint(0, 0, 0)//end swordfish
+ ));
+
+ private static final List GwenithGlideSharkPortalDirections = List.of(
+ new PortalDirection(5, PortalColors.White, Directions.East, Directions.SouthEast),
+ new PortalDirection(18, PortalColors.White, Directions.North, Directions.NorthEast),
+ new PortalDirection(25, PortalColors.Blue, Directions.East, Directions.East),
+ new PortalDirection(33, PortalColors.Blue, Directions.West, Directions.SouthWest),
+ new PortalDirection(38, PortalColors.Green, Directions.West, Directions.NorthWest),
+ new PortalDirection(47, PortalColors.Green, Directions.South, Directions.SouthWest),
+ new PortalDirection(52, PortalColors.Yellow, Directions.West, Directions.West),
+ new PortalDirection(62, PortalColors.Yellow, Directions.West, Directions.West),
+ new PortalDirection(71, PortalColors.Red, Directions.West, Directions.SouthWest)//
+ );
+
+ private static final List GwenithGlideMarlinBestLine = new ArrayList<>(
+ List.of(
+ // Trial Route:
+ /*0*/new WorldPoint(2257, 3459, 0),
+ /*1*/new WorldPoint(2259, 3472, 0),
+ /*2*/new WorldPoint(2269, 3476, 0),
+ /*3*/new WorldPoint(2272, 3487, 0),
+ /*4*/new WorldPoint(2260, 3496, 0),
+ /*5*/new WorldPoint(2093, 3233, 0),
+ /*6*/new WorldPoint(2097, 3231, 0),
+ /*7*/new WorldPoint(2104, 3230, 0),
+ /*8*/new WorldPoint(2117, 3232, 0),
+ /*9*/new WorldPoint(2130, 3254, 0),
+ /*10*/new WorldPoint(2132, 3269, 0),
+ /*11*/new WorldPoint(2143, 3291, 0),
+ /*12*/new WorldPoint(2157, 3293, 0),
+ /*13*/new WorldPoint(2260, 3509, 0),
+ /*14*/new WorldPoint(2267, 3519, 0),
+ /*15*/new WorldPoint(2263, 3531, 0),
+ /*16*/new WorldPoint(2250, 3542, 0),
+ /*17*/new WorldPoint(2251, 3559, 0),
+ /*19*/new WorldPoint(2242, 3574, 0),
+ /*20*/new WorldPoint(2084, 3215, 0),
+ /*21*/new WorldPoint(2107, 3214, 0),
+ /*22*/new WorldPoint(2110, 3212, 0),
+ /*23*/new WorldPoint(2136, 3212, 0),
+ /*24*/new WorldPoint(2140, 3214, 0),
+ /*25*/new WorldPoint(2138, 3228, 0),
+ /*26*/new WorldPoint(2133, 3232, 0),
+ /*27*/new WorldPoint(2138, 3237, 0),
+ /*28*/new WorldPoint(2143, 3243, 0),
+ /*29*/new WorldPoint(2153, 3247, 0),
+ /*30*/new WorldPoint(2203, 3574, 0),
+ /*31*/new WorldPoint(2192, 3568, 0),
+ /*32*/new WorldPoint(2192, 3548, 0),
+ /*33*/new WorldPoint(2202, 3534, 0),
+ /*34*/new WorldPoint(2198, 3513, 0),
+ /*35*/new WorldPoint(2105, 3140, 0),
+ /*36*/new WorldPoint(2092, 3145, 0),
+ /*37*/new WorldPoint(2078, 3158, 0),
+ /*38*/new WorldPoint(2070, 3161, 0),
+ /*39*/new WorldPoint(2069, 3175, 0),
+ /*40*/new WorldPoint(2058, 3185, 0),
+ /*41*/new WorldPoint(2073, 3210, 0),
+ /*42*/new WorldPoint(2100, 3205, 0),
+ /*44*/new WorldPoint(2115, 3189, 0),
+ /*45*/new WorldPoint(2133, 3191, 0),
+ /*43*/new WorldPoint(2128, 3172, 0),
+ /*46*/new WorldPoint(2197, 3490, 0),
+ /*47*/new WorldPoint(2192, 3480, 0),
+ /*48*/new WorldPoint(2176, 3475, 0),
+ /*49*/new WorldPoint(2170, 3465, 0),
+ /*50*/new WorldPoint(2158, 3464, 0),
+ /*51*/new WorldPoint(2117, 3372, 0),
+ /*52*/new WorldPoint(2089, 3374, 0),
+ /*53*/new WorldPoint(2079, 3388, 0),
+ /*54*/new WorldPoint(2083, 3394, 0),
+ /*55*/new WorldPoint(2097, 3396, 0),
+ /*56*/new WorldPoint(2105, 3407, 0),
+ /*57*/new WorldPoint(2108, 3410, 0),
+ /*58*/new WorldPoint(2113, 3414, 0),
+ /*59*/new WorldPoint(2115, 3423, 0),
+ /*60*/new WorldPoint(2110, 3439, 0),
+ /*61*/new WorldPoint(2117, 3439, 0),
+ /*62*/new WorldPoint(2146, 3464, 0),
+ /*63*/new WorldPoint(2111, 3464, 0),
+ /*64*/new WorldPoint(2105, 3471, 0),
+ /*65*/new WorldPoint(2107, 3492, 0),
+ /*66*/new WorldPoint(2115, 3496, 0),
+ /*67*/new WorldPoint(2126, 3495, 0),
+ /*68*/new WorldPoint(2134, 3483, 0),
+ /*69*/new WorldPoint(2144, 3483, 0),
+ /*70*/new WorldPoint(2149, 3493, 0),
+ /*71*/new WorldPoint(2149, 3502, 0),
+ /*72*/new WorldPoint(2160, 3508, 0),
+ /*73*/new WorldPoint(2250, 3633, 0),
+ /*74*/new WorldPoint(2241, 3629, 0),
+ /*75*/new WorldPoint(2232, 3617, 0),
+ /*76*/new WorldPoint(2230, 3609, 0),
+ /*77*/new WorldPoint(2228, 3600, 0),
+ /*78*/new WorldPoint(2220, 3593, 0),
+ /*79*/new WorldPoint(2192, 3599, 0),
+ /*80*/new WorldPoint(2166, 3589, 0),
+ /*81*/new WorldPoint(2151, 3597, 0),
+ /*82*/new WorldPoint(2125, 3598, 0),
+ /*83*/new WorldPoint(2109, 3592, 0),
+ /*84*/new WorldPoint(2099, 3582, 0),
+ /*85*/new WorldPoint(2104, 3574, 0),
+ /*86*/new WorldPoint(2174, 3508, 0),
+ /*87*/new WorldPoint(2189, 3508, 0),
+ /*88*/new WorldPoint(2208, 3508, 0),
+ /*89*/new WorldPoint(2220, 3516, 0),
+ /*90*/new WorldPoint(2220, 3526, 0),
+ /*91*/new WorldPoint(2216, 3538, 0),
+ /*92*/new WorldPoint(2222, 3547, 0),
+ /*93*/new WorldPoint(2224, 3570, 0),
+ /*94*/new WorldPoint(2218, 3580, 0),
+ /*95*/new WorldPoint(2208, 3584, 0),
+ /*96*/new WorldPoint(2108, 3560, 0),
+ /*97*/new WorldPoint(2097, 3558, 0),
+ /*98*/new WorldPoint(2080, 3552, 0),
+ /*99*/new WorldPoint(2072, 3540, 0),
+ /*100*/new WorldPoint(2083, 3529, 0),
+ /*101*/new WorldPoint(2083, 3504, 0),
+ /*102*/new WorldPoint(2085, 3472, 0),
+ /*103*/new WorldPoint(2095, 3445, 0),
+ /*104*/new WorldPoint(2095, 3438, 0),
+ /*105*/new WorldPoint(2103, 3433, 0),
+ /*106*/new WorldPoint(2105, 3425, 0),
+ /*107*/new WorldPoint(2193, 3584, 0),
+ /*108*/new WorldPoint(2177, 3579, 0),
+ /*109*/new WorldPoint(2175, 3562, 0),
+ /*110*/new WorldPoint(2179, 3544, 0),
+ /*111*/new WorldPoint(2174, 3537, 0),
+ /*112*/new WorldPoint(2162, 3545, 0),
+ /*113*/new WorldPoint(2153, 3574, 0),
+ /*114*/new WorldPoint(2143, 3582, 0),
+ /*115*/new WorldPoint(2137, 3256, 0),
+ /*116*/new WorldPoint(2130, 3276, 0),
+ /*117*/new WorldPoint(2130, 3283, 0),
+ /*118*/new WorldPoint(2124, 3289, 0),
+ /*119*/new WorldPoint(2127, 3296, 0),
+ /*120*/new WorldPoint(2131, 3318, 0),
+ /*121*/new WorldPoint(2141, 3336, 0),
+ /*122*/new WorldPoint(2151, 3346, 0),
+ /*123*/new WorldPoint(2146, 3367, 0),
+ /*124*/new WorldPoint(2128, 3380, 0),
+ /*125*/new WorldPoint(2121, 3364, 0),
+ /*126*/new WorldPoint(2126, 3356, 0),
+ /*127*/new WorldPoint(2129, 3582, 0),
+ /*128*/new WorldPoint(2122, 3582, 0),
+ /*129*/new WorldPoint(2119, 3569, 0),
+ /*130*/new WorldPoint(2124, 3539, 0),
+ /*131*/new WorldPoint(2121, 3527, 0),
+ /*132*/new WorldPoint(2119, 3519, 0),
+ /*133*/new WorldPoint(2130, 3512, 0),
+ /*134*/new WorldPoint(2140, 3517, 0),
+ /*135*/new WorldPoint(2162, 3519, 0),
+ /*136*/new WorldPoint(2171, 3523, 0),
+ /*137*/new WorldPoint(2104, 3413, 0),
+ /*138*/new WorldPoint(2094, 3417, 0),
+ /*139*/new WorldPoint(2085, 3414, 0),
+ /*140*/new WorldPoint(2080, 3426, 0),
+ /*141*/new WorldPoint(2081, 3439, 0),
+ /*142*/new WorldPoint(2083, 3446, 0),
+ /*143*/new WorldPoint(2082, 3454, 0),
+ /*144*/new WorldPoint(2082, 3492, 0),
+ /*145*/new WorldPoint(2095, 3505, 0),
+ /*146*/new WorldPoint(2105, 3520, 0),
+ /*147*/new WorldPoint(2095, 3529, 0),
+ /*148*/new WorldPoint(2091, 3541, 0),
+ /*149*/new WorldPoint(2106, 3543, 0)
+ // End of trial route
+
+ ));
+
+ private static final List GwenithGlideMarlinPortalDirections = List.of(
+ new PortalDirection(4, PortalColors.White, Directions.East, Directions.EastSouthEast),
+ new PortalDirection(12, PortalColors.White, Directions.North, Directions.NorthNorthEast),
+ new PortalDirection(18, PortalColors.Blue, Directions.East, Directions.East),
+ new PortalDirection(28, PortalColors.Blue, Directions.West, Directions.WestSouthWest),
+ new PortalDirection(33, PortalColors.Green, Directions.West, Directions.WestNorthWest),
+ new PortalDirection(44, PortalColors.Green, Directions.South, Directions.SouthSouthWest),
+ new PortalDirection(49, PortalColors.Yellow, Directions.West, Directions.West),
+ new PortalDirection(60, PortalColors.Yellow, Directions.West, Directions.West),
+ new PortalDirection(71, PortalColors.Red, Directions.West, Directions.SouthWest),
+ new PortalDirection(84, PortalColors.Red, Directions.East, Directions.East),
+ new PortalDirection(86, PortalColors.Green, Directions.East, Directions.NorthEast),
+ new PortalDirection(94, PortalColors.Black, Directions.West, Directions.West),
+ new PortalDirection(105, PortalColors.Black, Directions.West, Directions.West),
+ new PortalDirection(113, PortalColors.Cyan, Directions.North, Directions.NorthNorthWest),
+ new PortalDirection(125, PortalColors.Cyan, Directions.West, Directions.South),
+ new PortalDirection(135, PortalColors.Pink, Directions.West, Directions.WestNorthWest)//
+ );
+
+ public static final List AllTrialRoutes = new ArrayList(
+ List.of(
+ new TrialRoute(TrialLocations.TemporTantrum, TrialRanks.Swordfish, TemporTantrumSwordfishBestLine),
+ new TrialRoute(TrialLocations.TemporTantrum, TrialRanks.Shark, TemporTantrumSharkBestLine),
+ new TrialRoute(TrialLocations.TemporTantrum, TrialRanks.Marlin, TemporTantrumMarlinBestLine),
+ new TrialRoute(TrialLocations.JubblyJive, TrialRanks.Swordfish, JubblySwordfishBestLine, JubblySwordfishToadOrder, Collections.emptyList(), null),
+ new TrialRoute(TrialLocations.JubblyJive, TrialRanks.Shark, JubblySharkBestLine, JubblySharkToadOrder, Collections.emptyList(), null),
+ new TrialRoute(TrialLocations.JubblyJive, TrialRanks.Marlin, JubblyMarlinBestLine, JubblyMarlinToadOrder, JubblyMarlinWindMoteIndices, null),
+ new TrialRoute(TrialLocations.GwenithGlide, TrialRanks.Swordfish, GwenithGlideSwordfishBestLine, null, null, GwenithGlideSwordfishPortalDirections),
+ new TrialRoute(TrialLocations.GwenithGlide, TrialRanks.Shark, GwenithGlideSharkBestLine, null, null, GwenithGlideSharkPortalDirections),
+ new TrialRoute(TrialLocations.GwenithGlide, TrialRanks.Marlin, GwenithGlideMarlinBestLine, null, null, GwenithGlideMarlinPortalDirections)//
+ ));
+
+ public static final void AddGwenithGlideRoutes() {
+ AllTrialRoutes.add(new TrialRoute(TrialLocations.GwenithGlide, TrialRanks.Swordfish, GwenithGlideSwordfishBestLine, null, null, GwenithGlideSwordfishPortalDirections));
+ AllTrialRoutes.add(new TrialRoute(TrialLocations.GwenithGlide, TrialRanks.Shark, GwenithGlideSharkBestLine, null, null, GwenithGlideSharkPortalDirections));
+ AllTrialRoutes.add(new TrialRoute(TrialLocations.GwenithGlide, TrialRanks.Marlin, GwenithGlideMarlinBestLine, null, null, GwenithGlideMarlinPortalDirections));
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathHelper.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathHelper.java
new file mode 100644
index 0000000000..150b46535a
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathHelper.java
@@ -0,0 +1,35 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.debug;
+
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.coords.WorldPoint;
+import net.runelite.client.plugins.microbot.sailing.features.trials.data.Directions;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Slf4j
+public class BoatPathHelper {
+ private static final Map tickDataMap = new HashMap<>();
+
+ public static boolean HasTickData(int tick) {
+ return tickDataMap.containsKey(tick);
+ }
+
+ public static void StartNewTick(int tick, WorldPoint startPosition, Directions startHeading) {
+ //log.info("Starting new tick {}: position {}, heading {}", tick, startPosition, startHeading);
+ tickDataMap.put(tick, new TickMovementData(tick, startPosition, startHeading, new java.util.HashSet<>(Set.of(startPosition))));
+ }
+
+ public static void AddVisitedPoint(int tick, WorldPoint point) {
+ TickMovementData data = tickDataMap.get(tick);
+ if (data != null) {
+ data.PointsVisited.add(point);
+ }
+ }
+
+ public static TickMovementData GetTickData(int tick) {
+ return tickDataMap.get(tick);
+ }
+
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathOverlay.java
new file mode 100644
index 0000000000..678cdaedfc
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathOverlay.java
@@ -0,0 +1,160 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.debug;
+
+import com.google.inject.Inject;
+import net.runelite.api.Client;
+import net.runelite.api.GameState;
+import net.runelite.api.Perspective;
+import net.runelite.api.coords.WorldPoint;
+import net.runelite.client.plugins.microbot.sailing.features.trials.overlay.WorldPerspective;
+import net.runelite.client.ui.overlay.Overlay;
+import net.runelite.client.ui.overlay.OverlayLayer;
+import net.runelite.client.ui.overlay.OverlayPosition;
+
+import java.awt.*;
+import java.util.Set;
+
+public class BoatPathOverlay extends Overlay {
+ private Client client;
+
+ @Inject
+ public BoatPathOverlay(Client client) {
+ super();
+ setPosition(OverlayPosition.DYNAMIC);
+ setLayer(OverlayLayer.ABOVE_WIDGETS);
+ this.client = client;
+ }
+
+ private static final Set TickColors = Set.of(
+ Color.RED,
+ Color.GREEN,
+ Color.BLUE,
+ Color.YELLOW,
+ Color.ORANGE,
+ Color.CYAN);
+
+ public BoatPathOverlay() {
+ super();
+ }
+
+ @Override
+ public Dimension render(Graphics2D graphics) {
+ if (client == null || graphics == null) {
+ return null;
+ }
+
+ if (client.getGameState() != GameState.LOGGED_IN || client.getTickCount() <= 0 || TickColors.isEmpty()) {
+ return null;
+ }
+
+ var tickColorOrder = TickColors.toArray(new Color[0]);
+
+ var currentTick = client.getTickCount();
+ var ticksToRender = 6;
+
+ for (var offset = 0; offset < ticksToRender; offset++) {
+ var targetTick = currentTick - offset;
+ if (targetTick < 0) {
+ continue;
+ }
+
+ var tickData = BoatPathHelper.GetTickData(targetTick);
+ if (tickData == null || tickData.PointsVisited == null || tickData.PointsVisited.isEmpty()) {
+ continue;
+ }
+
+ var outlineColor = tickColorOrder[offset % tickColorOrder.length];
+ var insetEvenTicks = (tickData.Tick % 2) == 0;
+ drawVisitedTileOutlines(graphics, tickData, outlineColor, insetEvenTicks);
+ }
+
+ return null;
+ }
+
+ private void drawVisitedTileOutlines(Graphics2D graphics, TickMovementData tickData, Color outlineColor, boolean insetOutline) {
+ if (tickData == null || tickData.PointsVisited == null || tickData.PointsVisited.isEmpty()) {
+ return;
+ }
+
+ var previousStroke = graphics.getStroke();
+ graphics.setStroke(new BasicStroke(2f));
+ graphics.setColor(outlineColor);
+
+ for (var visitedPoint : tickData.PointsVisited) {
+ if (visitedPoint == null) {
+ continue;
+ }
+
+ var tilePolygon = getCanvasPolygonForWorldPoint(visitedPoint);
+ if (tilePolygon != null) {
+ if (insetOutline) {
+ var insetPolygon = insetPolygon(tilePolygon);
+ if (insetPolygon != null) {
+ tilePolygon = insetPolygon;
+ }
+ }
+ graphics.draw(tilePolygon);
+ }
+ }
+
+ graphics.setStroke(previousStroke);
+ }
+
+ private Polygon getCanvasPolygonForWorldPoint(WorldPoint worldPoint) {
+ if (worldPoint == null) {
+ return null;
+ }
+
+ var localPoints = WorldPerspective.getInstanceLocalPointFromReal(client, worldPoint);
+ if (localPoints.isEmpty()) {
+ return null;
+ }
+
+ for (var localPoint : localPoints) {
+ if (localPoint == null) {
+ continue;
+ }
+ var polygon = Perspective.getCanvasTilePoly(client, localPoint);
+ if (polygon != null) {
+ return polygon;
+ }
+ }
+
+ return null;
+ }
+
+ private Polygon insetPolygon(Polygon polygon) {
+ if (polygon == null) {
+ return polygon;
+ }
+
+ var centerX = 0;
+ var centerY = 0;
+ var nPoints = polygon.npoints;
+ if (nPoints == 0) {
+ return polygon;
+ }
+
+ for (var i = 0; i < nPoints; i++) {
+ centerX += polygon.xpoints[i];
+ centerY += polygon.ypoints[i];
+ }
+ centerX /= nPoints;
+ centerY /= nPoints;
+
+ var insetPoly = new Polygon();
+ for (var i = 0; i < nPoints; i++) {
+ var dx = polygon.xpoints[i] - centerX;
+ var dy = polygon.ypoints[i] - centerY;
+ var distance = Math.hypot(dx, dy);
+ if (distance == 0) {
+ continue;
+ }
+ var scale = Math.max((distance - 3) / distance, 0);
+ var newX = (int) Math.round(centerX + dx * scale);
+ var newY = (int) Math.round(centerY + dy * scale);
+ insetPoly.addPoint(newX, newY);
+ }
+
+ return insetPoly.npoints > 0 ? insetPoly : polygon;
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/TickMovementData.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/TickMovementData.java
new file mode 100644
index 0000000000..10256e0bd6
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/TickMovementData.java
@@ -0,0 +1,20 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.debug;
+
+import net.runelite.api.coords.WorldPoint;
+import net.runelite.client.plugins.microbot.sailing.features.trials.data.Directions;
+
+import java.util.Set;
+
+public class TickMovementData {
+ public int Tick;
+ public WorldPoint StartPosition;
+ public Directions StartHeading;
+ public Set PointsVisited;
+
+ public TickMovementData(int tick, WorldPoint startPosition, Directions startHeading, Set pointsVisited) {
+ Tick = tick;
+ StartPosition = startPosition;
+ StartHeading = startHeading;
+ PointsVisited = pointsVisited;
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/TrialRouteOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/TrialRouteOverlay.java
new file mode 100644
index 0000000000..f1ef0f5824
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/TrialRouteOverlay.java
@@ -0,0 +1,221 @@
+package net.runelite.client.plugins.microbot.sailing.features.trials.overlay;
+
+import net.runelite.api.Client;
+import net.runelite.api.Perspective;
+import net.runelite.api.Point;
+import net.runelite.api.coords.WorldPoint;
+import net.runelite.client.plugins.microbot.sailing.SailingConfig;
+import net.runelite.client.plugins.microbot.sailing.features.trials.BoatLocation;
+import net.runelite.client.plugins.microbot.sailing.features.trials.TrialsScript;
+import net.runelite.client.plugins.microbot.sailing.features.trials.data.PortalDirection;
+import net.runelite.client.plugins.microbot.sailing.features.trials.data.TrialRoute;
+import net.runelite.client.ui.overlay.Overlay;
+import net.runelite.client.ui.overlay.OverlayLayer;
+import net.runelite.client.ui.overlay.OverlayPosition;
+
+import javax.inject.Inject;
+import java.awt.*;
+import java.util.List;
+
+public class TrialRouteOverlay extends Overlay {
+
+ private static final int MAX_WAYPOINTS_TO_RENDER = 15;
+ private static final Color LINE_COLOR = new Color(0, 255, 128, 180);
+ private static final Color NEXT_WAYPOINT_COLOR = new Color(255, 255, 0, 220);
+ private static final Color WAYPOINT_COLOR = new Color(0, 200, 255, 150);
+ private static final Color PORTAL_COLOR = new Color(255, 100, 255, 200);
+
+ private final Client client;
+ private final SailingConfig config;
+ private final TrialsScript trialsScript;
+
+ @Inject
+ public TrialRouteOverlay(Client client, SailingConfig config, TrialsScript trialsScript) {
+ super();
+ setPosition(OverlayPosition.DYNAMIC);
+ setLayer(OverlayLayer.ABOVE_SCENE);
+ this.client = client;
+ this.config = config;
+ this.trialsScript = trialsScript;
+ }
+
+ @Override
+ public Dimension render(Graphics2D graphics) {
+ if (client == null || graphics == null) {
+ return null;
+ }
+
+ if (!config.showTrialRoute()) {
+ return null;
+ }
+
+ var player = client.getLocalPlayer();
+ if (player == null) {
+ return null;
+ }
+
+ var boatLocation = BoatLocation.fromLocal(client, player.getLocalLocation());
+ if (boatLocation == null) {
+ return null;
+ }
+
+ var route = trialsScript.getActiveTrialRoute();
+ if (route == null) {
+ return null;
+ }
+ var routePoints = route.getInterpolatedPoints();
+ if (routePoints == null || routePoints.isEmpty()) {
+ return null;
+ }
+
+ var visiblePoints = trialsScript.getVisibleActiveLineForPlayer(boatLocation, MAX_WAYPOINTS_TO_RENDER);
+ if (visiblePoints == null || visiblePoints.isEmpty()) {
+ return null;
+ }
+
+ var visibleIndices = trialsScript.getNextUnvisitedIndicesForActiveRoute(MAX_WAYPOINTS_TO_RENDER);
+ var portalDirection = trialsScript.getVisiblePortalDirection(route);
+
+ drawRouteLine(graphics, visiblePoints);
+ drawWaypoints(graphics, visiblePoints, visibleIndices, route, portalDirection);
+
+ return null;
+ }
+
+ private void drawRouteLine(Graphics2D graphics, List points) {
+ if (points.size() < 2) {
+ return;
+ }
+
+ graphics.setColor(LINE_COLOR);
+ graphics.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+
+ Point previousScreenPoint = null;
+ for (var worldPoint : points) {
+ var screenPoint = worldPointToScreen(worldPoint);
+ if (screenPoint == null) {
+ previousScreenPoint = null;
+ continue;
+ }
+
+ if (previousScreenPoint != null) {
+ graphics.drawLine(
+ previousScreenPoint.getX(), previousScreenPoint.getY(),
+ screenPoint.getX(), screenPoint.getY()
+ );
+ }
+ previousScreenPoint = screenPoint;
+ }
+ }
+
+ private void drawWaypoints(Graphics2D graphics, List points, List indices, TrialRoute route, PortalDirection portalDirection) {
+ var previousStroke = graphics.getStroke();
+ graphics.setStroke(new BasicStroke(2f));
+
+ for (int i = 0; i < points.size(); i++) {
+ var worldPoint = points.get(i);
+ var polygon = getCanvasPolygonForWorldPoint(worldPoint);
+ if (polygon == null) {
+ continue;
+ }
+
+ boolean isNextWaypoint = (i == 0);
+ boolean isPortalWaypoint = false;
+
+ if (i < indices.size()) {
+ int routeIndex = indices.get(i);
+ isPortalWaypoint = isPortalIndex(routeIndex, route);
+ }
+
+ if (isPortalWaypoint) {
+ graphics.setColor(PORTAL_COLOR);
+ graphics.fill(polygon);
+ graphics.setColor(Color.WHITE);
+ graphics.draw(polygon);
+
+ if (portalDirection != null) {
+ drawPortalLabel(graphics, polygon, portalDirection);
+ }
+ } else if (isNextWaypoint) {
+ graphics.setColor(NEXT_WAYPOINT_COLOR);
+ graphics.fill(polygon);
+ graphics.setColor(Color.WHITE);
+ graphics.draw(polygon);
+ } else {
+ graphics.setColor(WAYPOINT_COLOR);
+ graphics.draw(polygon);
+ }
+ }
+
+ graphics.setStroke(previousStroke);
+ }
+
+ private boolean isPortalIndex(int index, TrialRoute route) {
+ if (route.PortalDirections == null || route.PortalDirections.isEmpty()) {
+ return false;
+ }
+ for (var portal : route.PortalDirections) {
+ if (route.getInterpolatedIndex(portal.Index) == index) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void drawPortalLabel(Graphics2D graphics, Polygon polygon, PortalDirection portalDirection) {
+ var bounds = polygon.getBounds();
+ var textX = bounds.x + bounds.width + 5;
+ var textY = bounds.y + (bounds.height / 2) + 5;
+
+ var label = portalDirection.Color.name() + " → " + portalDirection.FirstMovementDirection.name();
+
+ graphics.setColor(Color.BLACK);
+ graphics.drawString(label, textX + 1, textY + 1);
+ graphics.setColor(PORTAL_COLOR);
+ graphics.drawString(label, textX, textY);
+ }
+
+ private Point worldPointToScreen(WorldPoint worldPoint) {
+ if (worldPoint == null) {
+ return null;
+ }
+
+ var localPoints = WorldPerspective.getInstanceLocalPointFromReal(client, worldPoint);
+ if (localPoints.isEmpty()) {
+ return null;
+ }
+
+ for (var localPoint : localPoints) {
+ if (localPoint == null) {
+ continue;
+ }
+ var screenPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane());
+ if (screenPoint != null) {
+ return screenPoint;
+ }
+ }
+ return null;
+ }
+
+ private Polygon getCanvasPolygonForWorldPoint(WorldPoint worldPoint) {
+ if (worldPoint == null) {
+ return null;
+ }
+
+ var localPoints = WorldPerspective.getInstanceLocalPointFromReal(client, worldPoint);
+ if (localPoints.isEmpty()) {
+ return null;
+ }
+
+ for (var localPoint : localPoints) {
+ if (localPoint == null) {
+ continue;
+ }
+ var polygon = Perspective.getCanvasTilePoly(client, localPoint);
+ if (polygon != null) {
+ return polygon;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/WorldPerspective.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/WorldPerspective.java
new file mode 100644
index 0000000000..632757536a
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/WorldPerspective.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (c) 2021, Zoinkwiz
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.plugins.microbot.sailing.features.trials.overlay;
+
+import net.runelite.api.Client;
+import net.runelite.api.Point;
+import net.runelite.api.WorldView;
+import net.runelite.api.coords.LocalPoint;
+import net.runelite.api.coords.WorldPoint;
+import net.runelite.api.gameval.InterfaceID;
+import net.runelite.api.gameval.VarbitID;
+import net.runelite.api.widgets.Widget;
+
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static net.runelite.api.Constants.CHUNK_SIZE;
+
+public class WorldPerspective {
+ private final static int SW = 0;
+ private final static int NW = 3;
+ private final static int NE = 2;
+ private final static int SE = 1;
+
+ public static Collection toLocalInstanceFromReal(Client client, WorldPoint worldPoint) {
+ if (!client.isInInstancedRegion()) {
+ return Collections.singleton(worldPoint);
+ }
+
+ if (worldPoint == null)
+ return Collections.singleton(null);
+
+ List worldPoints = new ArrayList<>();
+
+ int[][][] instanceTemplateChunks = client.getInstanceTemplateChunks();
+ for (int z = 0; z < instanceTemplateChunks.length; ++z) {
+ for (int x = 0; x < instanceTemplateChunks[z].length; ++x) {
+ for (int y = 0; y < instanceTemplateChunks[z][x].length; ++y) {
+ int chunkData = instanceTemplateChunks[z][x][y];
+ int rotation = chunkData >> 1 & 0x3;
+ int templateChunkY = (chunkData >> 3 & 0x7FF) * CHUNK_SIZE;
+ int templateChunkX = (chunkData >> 14 & 0x3FF) * CHUNK_SIZE;
+ if (worldPoint.getX() >= templateChunkX && worldPoint.getX() < templateChunkX + CHUNK_SIZE
+ && worldPoint.getY() >= templateChunkY && worldPoint.getY() < templateChunkY + CHUNK_SIZE) {
+ WorldPoint p = new WorldPoint(
+ client.getBaseX() + x * CHUNK_SIZE + (worldPoint.getX() & (CHUNK_SIZE - 1)),
+ client.getBaseY() + y * CHUNK_SIZE + (worldPoint.getY() & (CHUNK_SIZE - 1)),
+ z);
+ p = rotate(p, rotation);
+ if (p.isInScene(client)) {
+ worldPoints.add(p);
+ }
+ }
+ }
+ }
+ }
+ return worldPoints;
+ }
+
+ private static WorldPoint rotate(WorldPoint point, int rotation) {
+ int chunkX = point.getX() & -CHUNK_SIZE;
+ int chunkY = point.getY() & -CHUNK_SIZE;
+ int x = point.getX() & (CHUNK_SIZE - 1);
+ int y = point.getY() & (CHUNK_SIZE - 1);
+ switch (rotation) {
+ case 1:
+ return new WorldPoint(chunkX + y, chunkY + (CHUNK_SIZE - 1 - x), point.getPlane());
+ case 2:
+ return new WorldPoint(chunkX + (CHUNK_SIZE - 1 - x), chunkY + (CHUNK_SIZE - 1 - y), point.getPlane());
+ case 3:
+ return new WorldPoint(chunkX + (CHUNK_SIZE - 1 - y), chunkY + x, point.getPlane());
+ }
+ return point;
+ }
+
+ public static List getInstanceLocalPointFromReal(Client client, WorldPoint wp) {
+ List instanceWorldPoint = new ArrayList<>(WorldPerspective.toLocalInstanceFromReal(client, wp));
+
+ List localPoints = new ArrayList<>();
+ for (WorldPoint worldPoint : instanceWorldPoint) {
+ LocalPoint lp = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint);
+ if (lp != null) {
+ localPoints.add(lp);
+ }
+ }
+
+ return localPoints;
+ }
+
+ public static WorldPoint getInstanceWorldPointFromReal(Client client, WorldPoint wp) {
+ if (wp == null)
+ return null;
+ Collection points = WorldPerspective.toLocalInstanceFromReal(client, wp);
+
+ if (points.isEmpty())
+ return null;
+
+ // If multiple instance candidates are returned, prefer the one that
+ // matches the client's current plane. This reduces mismatches where a
+ // real-world point could map to multiple instance planes and the
+ // overlay ends up rendering a point on a different plane than the
+ // player (causing apparent 'missing' points).
+ int clientPlane = client.getPlane();
+ WorldPoint fallback = null;
+ for (WorldPoint point : points) {
+ if (point == null)
+ continue;
+
+ // Prefer a mapping that is on the same plane as the client
+ if (point.getPlane() == clientPlane) {
+ if (point.isInScene(client)) {
+ return point;
+ }
+ // keep as preferred candidate if it's not yet chosen
+ fallback = point;
+ } else if (fallback == null) {
+ // keep any first non-null candidate as a fallback
+ fallback = point;
+ }
+ }
+
+ return fallback;
+ }
+
+ /**
+ * Like getInstanceWorldPointFromReal(Client, WorldPoint) but prefer a candidate
+ * which maps into the supplied WorldView. This mirrors how DevTools renders
+ * and allows mapping using the player's worldview for more accurate
+ * on-screen coordinates.
+ */
+ public static WorldPoint getInstanceWorldPointFromReal(Client client, WorldView worldView, WorldPoint wp) {
+ if (wp == null)
+ return null;
+
+ Collection points = WorldPerspective.toLocalInstanceFromReal(client, wp);
+
+ if (points.isEmpty())
+ return null;
+
+ WorldPoint fallback = null;
+
+ // First, prefer a mapping that produces a non-null LocalPoint from the
+ // provided WorldView (i.e., it maps into the player's visible worldview)
+ if (worldView != null) {
+ for (WorldPoint point : points) {
+ if (point == null) {
+ continue;
+ }
+
+ LocalPoint lp = LocalPoint.fromWorld(worldView, point);
+ if (lp != null) {
+ // If it maps into that worldview it's a good match
+ return point;
+ }
+ if (fallback == null) {
+ fallback = point;
+ }
+ }
+ }
+
+ // Fall back to the regular selection logic if worldview-based selection
+ // didn't return an immediate result.
+ int clientPlane = client.getPlane();
+ for (WorldPoint point : points) {
+ if (point == null)
+ continue;
+
+ if (point.getPlane() == clientPlane) {
+ if (point.isInScene(client)) {
+ return point;
+ }
+ fallback = point;
+ } else if (fallback == null) {
+ fallback = point;
+ }
+ }
+
+ return fallback;
+ }
+
+ public static WorldPoint getRealWorldPointFromLocal(Client client, WorldPoint wp) {
+ LocalPoint lp = LocalPoint.fromWorld(client.getTopLevelWorldView(), wp);
+ if (lp == null)
+ return null;
+
+ return WorldPoint.fromLocalInstance(client, lp);
+ }
+
+ public static Rectangle getWorldMapClipArea(Client client) {
+ Widget widget = client.getWidget(InterfaceID.Worldmap.MAP_CONTAINER);
+ if (widget == null) {
+ return null;
+ }
+
+ return widget.getBounds();
+ }
+
+ public static Point mapWorldPointToGraphicsPoint(Client client, WorldPoint worldPoint) {
+ var worldMap = client.getWorldMap();
+ if (worldPoint == null)
+ return null;
+ if (!worldMap.getWorldMapData().surfaceContainsPosition(worldPoint.getX(), worldPoint.getY())) {
+ return null;
+ }
+
+ float pixelsPerTile = worldMap.getWorldMapZoom();
+
+ Widget map = client.getWidget(InterfaceID.Worldmap.MAP_CONTAINER);
+ if (map != null) {
+ Rectangle worldMapRect = map.getBounds();
+
+ int widthInTiles = (int) Math.ceil(worldMapRect.getWidth() / pixelsPerTile);
+ int heightInTiles = (int) Math.ceil(worldMapRect.getHeight() / pixelsPerTile);
+
+ var worldMapPosition = worldMap.getWorldMapPosition();
+
+ int yTileMax = worldMapPosition.getY() - heightInTiles / 2;
+ int yTileOffset = (yTileMax - worldPoint.getY() - 1) * -1;
+ int xTileOffset = worldPoint.getX() + widthInTiles / 2 - worldMapPosition.getX();
+
+ int xGraphDiff = ((int) (xTileOffset * pixelsPerTile));
+ int yGraphDiff = (int) (yTileOffset * pixelsPerTile);
+
+ yGraphDiff -= pixelsPerTile - Math.ceil(pixelsPerTile / 2);
+ xGraphDiff += pixelsPerTile - Math.ceil(pixelsPerTile / 2);
+
+ yGraphDiff = worldMapRect.height - yGraphDiff;
+ yGraphDiff += (int) worldMapRect.getY();
+ xGraphDiff += (int) worldMapRect.getX();
+
+ return new Point(xGraphDiff, yGraphDiff);
+ }
+ return null;
+ }
+
+ public static List worldToCanvasWithOffset(Client client, WorldPoint worldPoint, int zOffset) {
+ List canvasPoints = new ArrayList<>();
+
+ if (worldPoint == null) {
+ return canvasPoints;
+ }
+
+ Collection instances = WorldPerspective.toLocalInstanceFromReal(client, worldPoint);
+ for (WorldPoint wp : instances) {
+ if (wp == null) {
+ continue;
+ }
+
+ LocalPoint lp = LocalPoint.fromWorld(client.getTopLevelWorldView(), wp);
+ if (lp == null) {
+ continue;
+ }
+
+ Point canvas = net.runelite.api.Perspective.localToCanvas(client, lp, wp.getPlane(), zOffset);
+ if (canvas != null) {
+ canvasPoints.add(canvas);
+ }
+ }
+
+ return canvasPoints;
+ }
+
+ public static Point getMinimapPoint(Client client, WorldPoint start, WorldPoint destination) {
+ var worldMapData = client.getWorldMap().getWorldMapData();
+ if (worldMapData.surfaceContainsPosition(start.getX(), start.getY()) != worldMapData
+ .surfaceContainsPosition(destination.getX(), destination.getY())) {
+ return null;
+ }
+
+ int x = (destination.getX() - start.getX());
+ int y = (destination.getY() - start.getY());
+
+ float maxDistance = Math.max(Math.abs(x), Math.abs(y));
+ x = x * 100;
+ y = y * 100;
+ x /= maxDistance;
+ y /= maxDistance;
+
+ Widget minimapDrawWidget;
+ if (client.isResized()) {
+ if (client.getVarbitValue(VarbitID.RESIZABLE_STONE_ARRANGEMENT) == 1) {
+ minimapDrawWidget = client.getWidget(InterfaceID.ToplevelPreEoc.MINIMAP);
+ } else {
+ minimapDrawWidget = client.getWidget(InterfaceID.ToplevelOsrsStretch.MINIMAP);
+ }
+ } else {
+ minimapDrawWidget = client.getWidget(InterfaceID.Toplevel.MINIMAP);
+ }
+
+ if (minimapDrawWidget == null) {
+ return null;
+ }
+
+ final int angle = client.getCameraYawTarget() & 0x7FF;
+
+ final int sin = net.runelite.api.Perspective.SINE[angle];
+ final int cos = net.runelite.api.Perspective.COSINE[angle];
+
+ final int xx = y * sin + cos * x >> 16;
+ final int yy = sin * x - y * cos >> 16;
+
+ Point loc = minimapDrawWidget.getCanvasLocation();
+ int miniMapX = loc.getX() + xx + minimapDrawWidget.getWidth() / 2;
+ int miniMapY = minimapDrawWidget.getHeight() / 2 + loc.getY() + yy;
+ return new Point(miniMapX, miniMapY);
+ }
+
+ public static Polygon getZonePoly(Client client, Zone zone) {
+ Polygon areaPoly = new Polygon();
+ if (zone == null)
+ return areaPoly;
+
+ for (int x = zone.getMinX(); x < zone.getMaxX(); x++) {
+ addToPoly(client, areaPoly, new WorldPoint(x, zone.getMaxY(), zone.getMinWorldPoint().getPlane()), NW);
+ }
+
+ addToPoly(client, areaPoly, new WorldPoint(zone.getMaxX(), zone.getMaxY(), zone.getMinWorldPoint().getPlane()), NW, NE, SE);
+
+ for (int y = zone.getMaxY() - 1; y > zone.getMinY(); y--) {
+ addToPoly(client, areaPoly, new WorldPoint(zone.getMaxX(), y, zone.getMinWorldPoint().getPlane()), SE);
+ }
+
+ addToPoly(client, areaPoly, new WorldPoint(zone.getMaxX(), zone.getMinY(), zone.getMinWorldPoint().getPlane()), SE, SW);
+
+ for (int x = zone.getMaxX() - 1; x > zone.getMinX(); x--) {
+ addToPoly(client, areaPoly, new WorldPoint(x, zone.getMinY(), zone.getMinWorldPoint().getPlane()), SW);
+ }
+
+ addToPoly(client, areaPoly, new WorldPoint(zone.getMinX(), zone.getMinY(), zone.getMinWorldPoint().getPlane()), SW, NW);
+
+ for (int y = zone.getMinY() + 1; y < zone.getMaxY(); y++) {
+ addToPoly(client, areaPoly, new WorldPoint(zone.getMinX(), y, zone.getMinWorldPoint().getPlane()), NW);
+ }
+
+ return areaPoly;
+ }
+
+ private static void addToPoly(Client client, Polygon areaPoly, WorldPoint wp, int... points) {
+ LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), wp);
+ if (localPoint == null)
+ return;
+
+ Polygon poly = net.runelite.api.Perspective.getCanvasTilePoly(client, localPoint);
+ if (poly != null) {
+ for (int point : points) {
+ areaPoly.addPoint(poly.xpoints[point], poly.ypoints[point]);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/Zone.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/Zone.java
new file mode 100644
index 0000000000..0220dbcca3
--- /dev/null
+++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/Zone.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2019, Trevor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.plugins.microbot.sailing.features.trials.overlay;
+
+import lombok.Getter;
+import net.runelite.api.coords.WorldPoint;
+
+import static net.runelite.api.Constants.REGION_SIZE;
+
+public class Zone {
+ @Getter
+ private final int minX;
+ @Getter
+ private final int maxX;
+ @Getter
+ private final int minY;
+ @Getter
+ private final int maxY;
+ private int minPlane = 0;
+ private int maxPlane = 2;
+
+ // The first plane of the "Overworld"
+ public Zone() {
+ minX = 1152;
+ maxX = 3903;
+ minY = 2496;
+ maxY = 4159;
+ maxPlane = 0;
+ }
+
+ public Zone(WorldPoint p1, WorldPoint p2) {
+ assert (p1 != null);
+ assert (p2 != null);
+ minX = Math.min(p1.getX(), p2.getX());
+ maxX = Math.max(p1.getX(), p2.getX());
+ minY = Math.min(p1.getY(), p2.getY());
+ maxY = Math.max(p1.getY(), p2.getY());
+ minPlane = Math.min(p1.getPlane(), p2.getPlane());
+ maxPlane = Math.max(p1.getPlane(), p2.getPlane());
+ }
+
+ public Zone(WorldPoint p) {
+ assert (p != null);
+ minX = p.getX();
+ maxX = p.getX();
+ minY = p.getY();
+ maxY = p.getY();
+ minPlane = p.getPlane();
+ maxPlane = p.getPlane();
+ }
+
+ public Zone(int regionID) {
+ minX = ((regionID >> 8) & 0xFF) << 6;
+ maxX = minX + REGION_SIZE;
+ minY = (regionID & 0xFF) << 6;
+ maxY = minY + REGION_SIZE;
+ }
+
+ public Zone(int regionID, int plane) {
+ this(regionID);
+ minPlane = plane;
+ maxPlane = plane;
+ }
+
+ public boolean contains(WorldPoint worldPoint) {
+ return minX <= worldPoint.getX() && worldPoint.getX() <= maxX && minY <= worldPoint.getY()
+ && worldPoint.getY() <= maxY && minPlane <= worldPoint.getPlane()
+ && worldPoint.getPlane() <= maxPlane;
+ }
+
+ public WorldPoint getMinWorldPoint() {
+ return new WorldPoint(minX, minY, minPlane);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemConfig.java b/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemConfig.java
index e4fcef1a84..1b83c59cdf 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemConfig.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemConfig.java
@@ -4,6 +4,8 @@
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.ConfigInformation;
+import net.runelite.client.config.ConfigSection;
+import net.runelite.client.config.Range;
@ConfigGroup("valeTotems")
@ConfigInformation(
@@ -45,7 +47,12 @@
"• Ensure desired logs and knife/fletching knife are in bank
" +
"• Equip desired gear before starting (bot doesn't wield/unwield)
" +
"• Recommended: Graceful gear for reduced run energy drain
" +
- "• Bot handles all navigation and banking automatically!"
+ "• Bot handles all navigation and banking automatically!
" +
+
+ "⚡ STAMINA SUPPORT:
" +
+ "• Enable 'Use Stamina Potions' to automatically drink stamina potions
" +
+ "• Bot will withdraw potions from bank and drink when run energy is low
" +
+ "• Stamina potions are prioritized over energy potions"
)
public interface ValeTotemConfig extends Config {
@@ -79,6 +86,36 @@ default int collectOfferingsFrequency() {
return 5;
}
+ @ConfigSection(
+ name = "Stamina",
+ description = "Stamina potion settings",
+ position = 10
+ )
+ String staminaSection = "stamina";
+
+ @ConfigItem(
+ keyName = "useStaminaPotions",
+ name = "Use Stamina Potions",
+ description = "Automatically drink stamina potions when run energy is low",
+ position = 11,
+ section = staminaSection
+ )
+ default boolean useStaminaPotions() {
+ return false;
+ }
+
+ @ConfigItem(
+ keyName = "staminaThreshold",
+ name = "Drink At Energy %",
+ description = "Drink stamina potion when run energy falls below this percentage",
+ position = 12,
+ section = staminaSection
+ )
+ @Range(min = 10, max = 90)
+ default int staminaThreshold() {
+ return 50;
+ }
+
enum LogType {
OAK("Oak Logs", 20),
WILLOW("Willow Logs", 35),
diff --git a/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemPlugin.java
index 5af506d3f1..4a795bed6a 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemPlugin.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/valetotems/ValeTotemPlugin.java
@@ -27,7 +27,7 @@
)
@Slf4j
public class ValeTotemPlugin extends Plugin {
- static final String version = "1.0.5";
+ static final String version = "1.0.6";
@Inject
private ValeTotemConfig config;
diff --git a/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/BankingHandler.java b/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/BankingHandler.java
index 81f5fd32c7..afa1cd6c05 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/BankingHandler.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/BankingHandler.java
@@ -12,9 +12,13 @@
import net.runelite.client.plugins.microbot.util.bank.Rs2Bank;
import net.runelite.client.plugins.microbot.util.camera.Rs2Camera;
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.misc.Rs2Potion;
import net.runelite.client.plugins.microbot.util.player.Rs2Player;
import net.runelite.client.plugins.microbot.util.walker.Rs2Walker;
+import java.util.List;
+
import static net.runelite.client.plugins.microbot.util.Global.sleep;
import static net.runelite.client.plugins.microbot.util.Global.sleepGaussian;
@@ -310,7 +314,7 @@ private static boolean performStandardBankingOperations(GameSession gameSession)
return false;
}
- // Step 2: Deposit all except knife
+ // Step 2: Deposit all except knife (and stamina potion if we have one)
if (!depositAllExceptKnife()) {
Microbot.log("Failed to deposit items for standard route");
return false;
@@ -329,6 +333,11 @@ private static boolean performStandardBankingOperations(GameSession gameSession)
return false;
}
+ // Step 5: Handle stamina potion withdrawal if enabled
+ if (!handleStaminaPotionWithdrawal()) {
+ Microbot.log("Warning: Failed to handle stamina potions - continuing without");
+ }
+
return true;
} catch (Exception e) {
Microbot.log("Error during standard banking operations: " + e.getMessage());
@@ -378,6 +387,11 @@ private static boolean performExtendedRouteBankingOperations(GameSession gameSes
return false;
}
+ // Step 6: Handle stamina potion withdrawal if enabled
+ if (!handleStaminaPotionWithdrawal()) {
+ Microbot.log("Warning: Failed to handle stamina potions - continuing without");
+ }
+
Microbot.log("Extended route banking operations completed successfully");
return true;
@@ -735,6 +749,61 @@ private static boolean ensureKnifeAndLogBasketInInventory(GameSession gameSessio
}
}
+ private static boolean handleStaminaPotionWithdrawal() {
+ if (config == null || !config.useStaminaPotions() || !Rs2Bank.isOpen()) {
+ return true;
+ }
+
+ if (Rs2Inventory.hasItem(Rs2Potion.getStaminaPotion()) ||
+ Rs2Inventory.hasItem(Rs2Potion.getRestoreEnergyPotionsVariants().toArray(String[]::new))) {
+ return true;
+ }
+
+ InventoryUtils.depositEmptyVials();
+
+ int currentEnergy = Rs2Player.getRunEnergy();
+ int threshold = config.staminaThreshold();
+ boolean needsDrink = currentEnergy <= threshold && !Rs2Player.hasStaminaBuffActive();
+
+ String potionToWithdraw = findBestPotionInBank();
+ if (potionToWithdraw == null) {
+ Microbot.log("No stamina or energy potions available in bank");
+ return true;
+ }
+
+ if (needsDrink) {
+ Microbot.log("Run energy low - withdrawing and drinking potion");
+ Rs2Bank.withdrawOne(potionToWithdraw);
+ Rs2Inventory.waitForInventoryChanges(1800);
+ if (Rs2Inventory.hasItem(potionToWithdraw)) {
+ Rs2Inventory.interact(potionToWithdraw, "drink");
+ Rs2Inventory.waitForInventoryChanges(1800);
+ InventoryUtils.depositEmptyVials();
+ }
+ } else {
+ Microbot.log("Withdrawing potion for navigation");
+ Rs2Bank.withdrawOne(potionToWithdraw);
+ Rs2Inventory.waitForInventoryChanges(1800);
+ }
+
+ return true;
+ }
+
+ private static String findBestPotionInBank() {
+ if (Rs2Bank.hasItem(Rs2Potion.getStaminaPotion())) {
+ return Rs2Potion.getStaminaPotion();
+ }
+
+ List energyVariants = Rs2Potion.getRestoreEnergyPotionsVariants();
+ Rs2ItemModel energyPotion = Rs2Bank.bankItems().stream()
+ .filter(item -> energyVariants.stream()
+ .anyMatch(variant -> item.getName().toLowerCase().contains(variant.toLowerCase())))
+ .findFirst()
+ .orElse(null);
+
+ return energyPotion != null ? energyPotion.getName() : null;
+ }
+
/**
* Perform the log basket filling operation according to user specifications:
* 1. Take inventory full of logs
diff --git a/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/NavigationHandler.java b/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/NavigationHandler.java
index b3cc3d5fac..8fd3737694 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/NavigationHandler.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/valetotems/handlers/NavigationHandler.java
@@ -105,14 +105,25 @@ private static void checkPathfinderSettings() {
/**
* Custom walking method that only enables run energy if we have more than 10% energy
+ * Also handles stamina potion drinking when energy is low
* @param worldPoint the target location
* @return true if the walk command was successfully issued
*/
private static boolean walkWithRunEnergyCheck(WorldPoint worldPoint) {
- // Check current run energy before toggling run on
int currentRunEnergy = Rs2Player.getRunEnergy();
- boolean shouldToggleRun = currentRunEnergy > 10;
+ if (InventoryUtils.isStaminaSupportEnabled()) {
+ int threshold = InventoryUtils.getStaminaThreshold();
+ if (currentRunEnergy < threshold && !Rs2Player.hasStaminaBuffActive()) {
+ if (InventoryUtils.drinkStaminaOrEnergyPotion()) {
+ System.out.println("Drank stamina/energy potion - run energy was at " + currentRunEnergy + "%");
+ InventoryUtils.dropEmptyVials();
+ currentRunEnergy = Rs2Player.getRunEnergy();
+ }
+ }
+ }
+
+ boolean shouldToggleRun = currentRunEnergy > 10;
return Rs2Walker.walkFastCanvas(worldPoint, shouldToggleRun);
}
@@ -138,7 +149,7 @@ public static boolean navigateToTotem(TotemLocation totemLocation, net.runelite.
}
System.out.println("Navigating to " + totemLocation.getDescription());
-
+
// Clear processed ent trails for this new navigation run
clearProcessedEntTrails();
@@ -309,14 +320,23 @@ public static boolean navigateToBank() {
try {
WorldPoint bankLocation = BankLocation.PLAYER_STANDING_TILE.getLocation();
- // Check if already at bank
if (CoordinateUtils.isAtBankingPosition()) {
return true;
}
System.out.println("Returning to bank");
- // Simple blocking walk to bank - no need for concurrent actions while banking
+ if (InventoryUtils.isStaminaSupportEnabled()) {
+ int currentEnergy = Rs2Player.getRunEnergy();
+ int threshold = InventoryUtils.getStaminaThreshold();
+ if (currentEnergy < threshold && !Rs2Player.hasStaminaBuffActive()) {
+ if (InventoryUtils.drinkStaminaOrEnergyPotion()) {
+ System.out.println("Drank potion before returning to bank - energy was at " + currentEnergy + "%");
+ InventoryUtils.dropEmptyVials();
+ }
+ }
+ }
+
boolean walked = Rs2Walker.walkTo(bankLocation);
if (walked) {
return CoordinateUtils.isAtBankingPosition();
diff --git a/src/main/java/net/runelite/client/plugins/microbot/valetotems/utils/InventoryUtils.java b/src/main/java/net/runelite/client/plugins/microbot/valetotems/utils/InventoryUtils.java
index 127b96b102..1fb8340f3b 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/valetotems/utils/InventoryUtils.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/valetotems/utils/InventoryUtils.java
@@ -7,10 +7,14 @@
import net.runelite.client.plugins.microbot.util.dialogues.Rs2Dialogue;
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.misc.Rs2Potion;
+import net.runelite.client.plugins.microbot.util.player.Rs2Player;
import static net.runelite.client.plugins.microbot.util.Global.sleep;
import static net.runelite.client.plugins.microbot.util.Global.sleepGaussian;
import static net.runelite.client.plugins.microbot.util.Global.sleepUntil;
+import java.util.List;
+
/**
* Utility class for inventory management in the Vale Totems minigame
*/
@@ -224,15 +228,11 @@ public static String getInventorySummary() {
* @return number of logs to withdraw for optimal inventory management
*/
public static int getOptimalLogWithdrawAmount() {
- // Keep 1 slot for knife, rest for logs
- // Consider existing bows and logs
- int availableSlots = 27; // 28 - 1 for knife
-
- // Withdraw enough for full round (25 total) but don't exceed inventory
- int targetTotal = 27;
- int needed = Math.max(0, targetTotal);
-
- return Math.min(needed, availableSlots);
+ // Reserve slots: 1 for knife, optionally 1 for stamina potion
+ int reservedSlots = isStaminaSupportEnabled() ? 2 : 1;
+ int availableSlots = 28 - reservedSlots;
+
+ return availableSlots;
}
/**
@@ -430,27 +430,27 @@ public static int getOptimalLogAmountForExtendedRoute(net.runelite.client.plugin
int currentLogs = getLogCount();
int basketLogs = getLogBasketLogCount(gameSession);
int totalCurrentLogs = currentLogs + basketLogs;
-
- // Calculate how many more logs we need
+
int logsNeeded = Math.max(0, totalLogsNeeded - totalCurrentLogs);
-
- // Don't exceed inventory capacity (leaving room for knife and basket)
- int maxCanTake = 26; // 28 slots - knife - basket
-
+
+ // Reserve slots: knife, basket, optionally stamina potion
+ int reservedSlots = isStaminaSupportEnabled() ? 3 : 2;
+ int maxCanTake = 28 - reservedSlots;
+
return Math.min(logsNeeded, maxCanTake);
}
public static int getOptimalLogBasketLogAmountForExtendedRoute(net.runelite.client.plugins.microbot.valetotems.models.GameSession gameSession) {
- int totalLogsNeeded = 40 - 26; // 8 totems * 5 logs each - we will hold 26 in inventory
+ // Reserve slots: knife, basket, optionally stamina potion
+ int reservedSlots = isStaminaSupportEnabled() ? 3 : 2;
+ int inventoryLogsCapacity = 28 - reservedSlots;
+
+ int totalLogsNeeded = 40 - inventoryLogsCapacity;
int basketLogs = getLogBasketLogCount(gameSession);
-
- // Calculate how many more logs we need
+
int logsNeeded = Math.max(0, totalLogsNeeded - basketLogs);
-
- // Don't exceed inventory capacity (leaving room for knife and basket)
- int maxCanTake = 26; // 28 slots - knife - basket
-
- return Math.min(logsNeeded, maxCanTake);
+
+ return Math.min(logsNeeded, inventoryLogsCapacity);
}
/**
@@ -503,4 +503,51 @@ public static boolean shouldEmptyLogBasket(net.runelite.client.plugins.microbot.
// This ensures we start fresh with the correct log type
return hasLogBasket() && gameSession != null && gameSession.getLogBasketLogCount() > 0;
}
+
+ public static final int VIAL_EMPTY_ID = ItemID.VIAL;
+
+ public static boolean drinkStaminaOrEnergyPotion() {
+ boolean hasStaminaBuff = Rs2Player.hasStaminaBuffActive();
+
+ if (!hasStaminaBuff && Rs2Inventory.hasItem(Rs2Potion.getStaminaPotion())) {
+ if (Rs2Inventory.interact(Rs2Potion.getStaminaPotion(), "drink")) {
+ sleepGaussian(600, 150);
+ return true;
+ }
+ }
+
+ Rs2ItemModel energyPotion = Rs2Inventory.get(Rs2Potion.getRestoreEnergyPotionsVariants().toArray(String[]::new));
+ if (energyPotion != null) {
+ if (Rs2Inventory.interact(energyPotion.getId(), "drink")) {
+ sleepGaussian(600, 150);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static void dropEmptyVials() {
+ if (Rs2Inventory.hasItem(VIAL_EMPTY_ID)) {
+ Rs2Inventory.dropAll(VIAL_EMPTY_ID);
+ sleepGaussian(300, 100);
+ }
+ }
+
+ public static boolean depositEmptyVials() {
+ if (Rs2Bank.isOpen() && Rs2Inventory.hasItem(VIAL_EMPTY_ID)) {
+ Rs2Bank.depositAll(VIAL_EMPTY_ID);
+ sleepGaussian(300, 100);
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isStaminaSupportEnabled() {
+ return config != null && config.useStaminaPotions();
+ }
+
+ public static int getStaminaThreshold() {
+ return config != null ? config.staminaThreshold() : 50;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java
index 12edcc348d..582b9a1b2d 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java
@@ -50,7 +50,7 @@
)
@Slf4j
public class AutoWoodcuttingPlugin extends Plugin {
- public static final String version = "1.7.8";
+ public static final String version = "1.8.0";
@Inject
@Getter(AccessLevel.MODULE)
public AutoWoodcuttingScript autoWoodcuttingScript;
diff --git a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/enums/WoodcuttingTree.java b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/enums/WoodcuttingTree.java
index 71134d9c81..1425fc7e42 100644
--- a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/enums/WoodcuttingTree.java
+++ b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/enums/WoodcuttingTree.java
@@ -3,7 +3,7 @@
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.runelite.api.Skill;
-import net.runelite.api.gameval.ItemID;
+import net.runelite.api.ItemID;
import net.runelite.client.plugins.microbot.util.player.Rs2Player;
@@ -11,15 +11,31 @@
@RequiredArgsConstructor
public enum WoodcuttingTree {
TREE("tree" , "Logs", ItemID.LOGS, 1, "Chop down"),
+ TIRANNWN_TREE("tree", "Logs", ItemID.LOGS, 1, "Chop down"),
+ DYING_TREE("dying tree", "Logs", ItemID.LOGS, 1, "Chop down"),
+ BURNT_TREE("burnt tree", "Charcoal", ItemID.CHARCOAL, 1, "Chop down"),
+ JUNGLE_TREE("jungle tree", "Logs", ItemID.LOGS, 1, "Chop down"),
+ ACHEY_TREE("achey tree", "Achey tree logs", ItemID.ACHEY_TREE_LOGS, 1, "Chop down"),
+ LIGHT_JUNGLE("light jungle", "Thatch spar light", ItemID.THATCH_SPAR_LIGHT, 10, "Chop down"),
OAK("oak tree", "Oak logs", ItemID.OAK_LOGS,15, "Chop down"),
+ MEDIUM_JUNGLE("medium jungle", "Thatch spar med", ItemID.THATCH_SPAR_MED, 20, "Chop down"),
WILLOW("willow tree", "Willow logs", ItemID.WILLOW_LOGS, 30, "Chop down"),
TEAK_TREE("teak tree", "Teak logs", ItemID.TEAK_LOGS, 35, "Chop down"),
+ DENSE_JUNGLE("dense jungle", "Thatch spar dense", ItemID.THATCH_SPAR_DENSE, 35, "Chop down"),
+ JATOBA_TREE("jatoba tree", "Jatoba logs", ItemID.JATOBA_LOGS, 40, "Chop down"),
+ MATURE_JUNIPER("mature juniper tree", "Juniper logs", ItemID.JUNIPER_LOGS, 42, "Chop down"),
MAPLE("maple tree", "Maple logs", ItemID.MAPLE_LOGS, 45, "Chop down"),
+ HOLLOW_TREE("hollow tree", "Bark", ItemID.BARK, 45, "Chop down"),
MAHOGANY("mahogany tree", "Mahogany logs", ItemID.MAHOGANY_LOGS, 50, "Chop down"),
+ ARCTIC_PINE("arctic pine", "Arctic pine logs", ItemID.ARCTIC_PINE_LOGS, 54, "Chop down"),
YEW("yew tree", "Yew logs", ItemID.YEW_LOGS, 60, "Chop down"),
BLISTERWOOD("blisterwood tree", "Blisterwood logs", ItemID.BLISTERWOOD_LOGS, 62, "Chop"),
+ SULLIUSCEP("sulliuscep", "Sulliuscep cap", ItemID.SULLIUSCEP_CAP, 65, "Chop down"),
+ CAMPHOR_TREE("camphor tree", "Camphor logs", ItemID.CAMPHOR_LOGS, 66, "Chop down"),
MAGIC("magic tree", "Magic logs", ItemID.MAGIC_LOGS, 75, "Chop down"),
+ IRONWOOD_TREE("ironwood tree", "Ironwood logs", ItemID.IRONWOOD_LOGS, 80, "Chop down"),
REDWOOD("redwood tree", "Redwood logs", ItemID.REDWOOD_LOGS, 90, "Cut"),
+ ROSEWOOD_TREE("rosewood tree", "Rosewood logs", ItemID.ROSEWOOD_LOGS, 92, "Chop down"),
EVERGREEN_TREE("evergreen tree" , "Logs", ItemID.LOGS, 1, "Chop down"),
DEAD_TREE("dead tree" , "Logs", ItemID.LOGS, 1, "Chop down"),
INFECTED_ROOT("infected root", "Logs", ItemID.LOGS, 80, "Chop");
@@ -32,6 +48,9 @@ public enum WoodcuttingTree {
@Override
public String toString() {
+ if (this == TIRANNWN_TREE) {
+ return "tree (tirannwn)";
+ }
return name;
}