From 2dae3e509afaeeb83acdb5cb21550395a66483a4 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 3 Jan 2026 05:52:16 +0100 Subject: [PATCH 1/6] Bump AerialFishingPlugin version to 1.1.2; update bait item references in overlay and script --- .../plugins/microbot/aerialfishing/AerialFishingOverlay.java | 2 +- .../plugins/microbot/aerialfishing/AerialFishingPlugin.java | 2 +- .../plugins/microbot/aerialfishing/AerialFishingScript.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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; } From f8762bf2aa514da35eca23b4285a30475ac1bcae Mon Sep 17 00:00:00 2001 From: chsami Date: Mon, 5 Jan 2026 06:01:48 +0100 Subject: [PATCH 2/6] Add initial implementation of trials feature with overlays and configurations --- .run/Microbot Debug.run.xml | 16 + AGENTS.md | 230 +++- CLAUDE.md | 71 ++ .../microbot/sailing/MSailingPlugin.java | 18 +- .../microbot/sailing/SailingConfig.java | 56 + .../microbot/sailing/SailingScript.java | 14 +- .../sailing/features/trials/BoatLocation.java | 24 + .../sailing/features/trials/TrialsScript.java | 1032 +++++++++++++++++ .../features/trials/data/AllSails.java | 30 + .../features/trials/data/Directions.java | 20 + .../features/trials/data/ObjectiveInfo.java | 11 + .../trials/data/ObstacleTracking.java | 32 + .../features/trials/data/PortalColors.java | 12 + .../features/trials/data/PortalDirection.java | 16 + .../features/trials/data/ToadFlagColors.java | 12 + .../trials/data/ToadFlagGameObject.java | 42 + .../features/trials/data/TrialInfo.java | 158 +++ .../features/trials/data/TrialLocations.java | 9 + .../features/trials/data/TrialRanks.java | 10 + .../features/trials/data/TrialRoute.java | 810 +++++++++++++ .../features/trials/debug/BoatPathHelper.java | 35 + .../trials/debug/BoatPathOverlay.java | 160 +++ .../trials/debug/TickMovementData.java | 20 + .../trials/overlay/TrialRouteOverlay.java | 217 ++++ .../trials/overlay/WorldPerspective.java | 375 ++++++ .../sailing/features/trials/overlay/Zone.java | 97 ++ 26 files changed, 3507 insertions(+), 20 deletions(-) create mode 100644 .run/Microbot Debug.run.xml create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/BoatLocation.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/TrialsScript.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/AllSails.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/Directions.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObjectiveInfo.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ObstacleTracking.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalColors.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/PortalDirection.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagColors.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/ToadFlagGameObject.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialInfo.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialLocations.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRanks.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRoute.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathHelper.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/BoatPathOverlay.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/debug/TickMovementData.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/TrialRouteOverlay.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/WorldPerspective.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/Zone.java 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/sailing/MSailingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/sailing/MSailingPlugin.java index 6baf7b68c6..5ba002c6a3 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 = "1.0.3"; @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..501870f1da --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/TrialsScript.java @@ -0,0 +1,1032 @@ +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.api.boat.data.Heading; +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 net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; + +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) { + if (activeRoute != null) { + log.info("Trial ended, resetting state"); + 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)) { + log.info("Starting route for {} - {}", route.Location, route.Rank); + activeRoute = route; + currentWaypointIndex = 0; + } + + WorldPoint boatPos = getBoatPosition(); + if (boatPos == null) { + log.debug("Could not determine boat position"); + return; + } + + WorldPoint target = route.Points.get(currentWaypointIndex); + int distance = boatPos.distanceTo(target); + + int nextIndex = getNextWaypointIndex(currentWaypointIndex, route.Points.size()); + WorldPoint nextWaypoint = route.Points.get(nextIndex); + + double dirToTarget = calculateDirection(boatPos, target); + double dirToNext = calculateDirection(target, nextWaypoint); + double turnAngle = calculateTurnAngle(boatPos, target, nextWaypoint); + int dynamicThreshold = calculateDynamicThreshold(turnAngle); + + System.out.println("dynamicThreshold: " + dynamicThreshold + " | turnAngle: " + turnAngle); + + String currentDir = degreesToCompass(dirToTarget); + String nextDir = degreesToCompass(dirToNext); + + + int orientation = boatCache.getLocalBoat().getOrientation(); + int currentDirection = ((orientation + 64) / 128) & 0xF; + var result = boatCache.getLocalBoat().getDirection(target); + + + // boatCache.getLocalBoat().setHeading(Heading.getHeading(result)); + + var menuEntry = new NewMenuEntry() + .option("Set-Heading") + .target("") + .identifier(result) + .type(MenuAction.SET_HEADING) + .param0(0) + .param1(0) + .forceLeftClick(false); + var worldview = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldView()); + + if (worldview == null) + { + menuEntry.setWorldViewId(Microbot.getClient().getTopLevelWorldView().getId()); + } + else + { + menuEntry.setWorldViewId(worldview.getId()); + } + Microbot.doInvoke(menuEntry, new java.awt.Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + + System.out.println("Boat heading: " + Heading.getHeading(currentDirection) + " | Target direction: " + Heading.getHeading(result)); + + + /* log.info("Navigation: wp={}/{} boat=({},{}) -> target=({},{}) [{}] dist={} | next=({},{}) [{}] | turn={}° thresh={}", + currentWaypointIndex, route.Points.size() - 1, + boatPos.getX(), boatPos.getY(), + target.getX(), target.getY(), currentDir, + distance, + nextWaypoint.getX(), nextWaypoint.getY(), nextDir, + String.format("%.1f", turnAngle), dynamicThreshold);*/ + + if (distance < dynamicThreshold) { + /* log.info(">> ADVANCING: wp {} -> {} | dist {} < thresh {} | turn {}° ({} -> {})", + currentWaypointIndex, nextIndex, + distance, dynamicThreshold, + String.format("%.1f", turnAngle), currentDir, nextDir);*/ + lastVisitedIndex = currentWaypointIndex; + currentWaypointIndex = nextIndex; + target = route.Points.get(currentWaypointIndex); + } + + final WorldPoint hintTarget = target; + Microbot.getClientThread().invoke(() -> client.setHintArrow(hintTarget)); + + if (needsTrim) { + boatCache.getLocalBoat().trimSails(); + needsTrim = false; + } + + 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()); + } + + private int getNextWaypointIndex(int currentIndex, int routeSize) { + return (currentIndex + 1) % routeSize; + } + + private double calculateTurnAngle(WorldPoint current, WorldPoint next, WorldPoint afterNext) { + if (current == null || next == null || afterNext == null) { + return 0.0; + } + + double dx1 = next.getX() - current.getX(); + double dy1 = next.getY() - current.getY(); + + double dx2 = afterNext.getX() - next.getX(); + double dy2 = afterNext.getY() - next.getY(); + + double len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); + double len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); + + if (len1 < 0.001 || len2 < 0.001) { + return 0.0; + } + + dx1 /= len1; + dy1 /= len1; + dx2 /= len2; + dy2 /= len2; + + double dot = dx1 * dx2 + dy1 * dy2; + dot = Math.max(-1.0, Math.min(1.0, dot)); + + double angleRadians = Math.acos(dot); + double angleDegrees = Math.toDegrees(angleRadians); + + return angleDegrees; + } + + private int calculateDynamicThreshold(double turnAngleDegrees) { + int baseThreshold = 1; + int additionalThreshold = (int) (turnAngleDegrees / 6.0); + int maxAdditional = 10; + return baseThreshold + Math.min(additionalThreshold, maxAdditional); + } + + private double calculateDirection(WorldPoint from, WorldPoint to) { + if (from == null || to == null) { + return 0.0; + } + double dx = to.getX() - from.getX(); + double dy = to.getY() - from.getY(); + double angle = Math.toDegrees(Math.atan2(dy, dx)); + if (angle < 0) { + angle += 360; + } + return angle; + } + + private String degreesToCompass(double degrees) { + String[] directions = {"E", "ENE", "NE", "NNE", "N", "NNW", "NW", "WNW", "W", "WSW", "SW", "SSW", "S", "SSE", "SE", "ESE"}; + int index = (int) Math.round(degrees / 22.5) % 16; + return directions[index]; + } + + @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 || route.Points == null || route.Points.isEmpty()) { + return; + } + var nextIdx = lastVisitedIndex + 1; + if (nextIdx >= route.Points.size()) { + return; // finished route + } + var target = route.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 || route.Points == null || route.Points.isEmpty() || limit <= 0) { + return Collections.emptyList(); + } + var start = Math.max(0, lastVisitedIndex); + if (start >= route.Points.size()) { + return Collections.emptyList(); + } + var out = new ArrayList(limit); + var nextPortal = route.PortalDirections.stream() + .filter(x -> x.Index >= lastVisitedIndex) + .min((a, b) -> Integer.compare(a.Index, b.Index)) + .orElse(null); + for (var i = start; i < route.Points.size() && out.size() < limit; i++) { + if (nextPortal != null && i > 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(); + } + + List out = new ArrayList<>(); + for (var idx : nextIdx) { + var real = route.Points.get(idx); + out.add(real); + } + return out; + } + + public PortalDirection getVisiblePortalDirection(TrialRoute route) { + var portalDirection = route.PortalDirections.stream() + .filter(x -> x.Index - 1 == lastVisitedIndex || x.Index == lastVisitedIndex || x.Index + 1 == lastVisitedIndex) + .min((a, b) -> Integer.compare(a.Index, 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..2e016adae1 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/data/TrialRoute.java @@ -0,0 +1,810 @@ +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.List; +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; + + public TrialRoute(TrialLocations location, TrialRanks rank, List points) { + Location = location; + Rank = rank; + Points = points == null ? new ArrayList<>() : new ArrayList<>(points); // ensure mutable + ToadOrder = Collections.emptyList(); + WindMoteIndices = Collections.emptyList(); + PortalDirections = Collections.emptyList(); + } + + // Unified extended constructor to avoid type erasure collision between different generic list overloads. + 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; + } + + @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..f0b85899c6 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/sailing/features/trials/overlay/TrialRouteOverlay.java @@ -0,0 +1,217 @@ +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 || route.Points == null || route.Points.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 (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 From 3b679583d7681ef07455f88775d38896911853e4 Mon Sep 17 00:00:00 2001 From: chsami Date: Mon, 5 Jan 2026 06:02:09 +0100 Subject: [PATCH 3/6] Bump MSailingPlugin version to 2.0.0 --- .../client/plugins/microbot/sailing/MSailingPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5ba002c6a3..cb377f5d56 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 @@ -30,7 +30,7 @@ @Slf4j public class MSailingPlugin extends Plugin { - static final String version = "1.0.3"; + static final String version = "2.0.0"; @Inject private SailingConfig config; From 78fef3506e0aa56ee81bcb30f9688d8e634eeef2 Mon Sep 17 00:00:00 2001 From: chsami Date: Mon, 5 Jan 2026 06:02:17 +0100 Subject: [PATCH 4/6] Add stamina potion support: automate withdrawal and consumption based on energy levels --- .../microbot/valetotems/ValeTotemConfig.java | 39 +++++++- .../microbot/valetotems/ValeTotemPlugin.java | 2 +- .../valetotems/handlers/BankingHandler.java | 71 +++++++++++++- .../handlers/NavigationHandler.java | 30 +++++- .../valetotems/utils/InventoryUtils.java | 93 ++++++++++++++----- 5 files changed, 204 insertions(+), 31 deletions(-) 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 From 6ffd06ddfd64de4cfe0cb4ea7b67df2b6a20c9ba Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 6 Jan 2026 19:25:26 +0100 Subject: [PATCH 5/6] Bump MSailingPlugin version to 2.0.1; enhance TrialRoute with interpolation methods for smoother navigation --- .../microbot/sailing/MSailingPlugin.java | 2 +- .../sailing/features/trials/TrialsScript.java | 190 ++++-------------- .../features/trials/data/TrialRoute.java | 86 +++++++- .../trials/overlay/TrialRouteOverlay.java | 8 +- 4 files changed, 132 insertions(+), 154 deletions(-) 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 cb377f5d56..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 @@ -30,7 +30,7 @@ @Slf4j public class MSailingPlugin extends Plugin { - static final String version = "2.0.0"; + static final String version = "2.0.1"; @Inject private SailingConfig config; 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 index 501870f1da..c10a761718 100644 --- 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 @@ -14,11 +14,9 @@ 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.api.boat.data.Heading; 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 net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; import javax.inject.Inject; import javax.inject.Singleton; @@ -127,10 +125,7 @@ public void run(SailingConfig config) { TrialInfo info = Microbot.getClientThread().invoke(() -> TrialInfo.getCurrent(client)); if (info == null) { - if (activeRoute != null) { - log.info("Trial ended, resetting state"); - resetState(); - } + resetState(); return; } @@ -141,90 +136,36 @@ public void run(SailingConfig config) { } if (!route.equals(activeRoute)) { - log.info("Starting route for {} - {}", route.Location, route.Rank); activeRoute = route; currentWaypointIndex = 0; } - WorldPoint boatPos = getBoatPosition(); - if (boatPos == null) { - log.debug("Could not determine boat position"); + List routePoints = route.getInterpolatedPoints(); + if (routePoints == null || routePoints.isEmpty()) { return; } - WorldPoint target = route.Points.get(currentWaypointIndex); - int distance = boatPos.distanceTo(target); - - int nextIndex = getNextWaypointIndex(currentWaypointIndex, route.Points.size()); - WorldPoint nextWaypoint = route.Points.get(nextIndex); - - double dirToTarget = calculateDirection(boatPos, target); - double dirToNext = calculateDirection(target, nextWaypoint); - double turnAngle = calculateTurnAngle(boatPos, target, nextWaypoint); - int dynamicThreshold = calculateDynamicThreshold(turnAngle); - - System.out.println("dynamicThreshold: " + dynamicThreshold + " | turnAngle: " + turnAngle); - - String currentDir = degreesToCompass(dirToTarget); - String nextDir = degreesToCompass(dirToNext); - - - int orientation = boatCache.getLocalBoat().getOrientation(); - int currentDirection = ((orientation + 64) / 128) & 0xF; - var result = boatCache.getLocalBoat().getDirection(target); - - - // boatCache.getLocalBoat().setHeading(Heading.getHeading(result)); - - var menuEntry = new NewMenuEntry() - .option("Set-Heading") - .target("") - .identifier(result) - .type(MenuAction.SET_HEADING) - .param0(0) - .param1(0) - .forceLeftClick(false); - var worldview = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldView()); - - if (worldview == null) - { - menuEntry.setWorldViewId(Microbot.getClient().getTopLevelWorldView().getId()); - } - else - { - menuEntry.setWorldViewId(worldview.getId()); + if (currentWaypointIndex >= routePoints.size()) { + currentWaypointIndex = 0; } - Microbot.doInvoke(menuEntry, new java.awt.Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); - - System.out.println("Boat heading: " + Heading.getHeading(currentDirection) + " | Target direction: " + Heading.getHeading(result)); + WorldPoint boatPos = getBoatPosition(); + if (boatPos == null) { + return; + } - /* log.info("Navigation: wp={}/{} boat=({},{}) -> target=({},{}) [{}] dist={} | next=({},{}) [{}] | turn={}° thresh={}", - currentWaypointIndex, route.Points.size() - 1, - boatPos.getX(), boatPos.getY(), - target.getX(), target.getY(), currentDir, - distance, - nextWaypoint.getX(), nextWaypoint.getY(), nextDir, - String.format("%.1f", turnAngle), dynamicThreshold);*/ + WorldPoint target = routePoints.get(currentWaypointIndex); + int distance = boatPos.distanceTo(target); - if (distance < dynamicThreshold) { - /* log.info(">> ADVANCING: wp {} -> {} | dist {} < thresh {} | turn {}° ({} -> {})", - currentWaypointIndex, nextIndex, - distance, dynamicThreshold, - String.format("%.1f", turnAngle), currentDir, nextDir);*/ + if (distance <= 5) { lastVisitedIndex = currentWaypointIndex; - currentWaypointIndex = nextIndex; - target = route.Points.get(currentWaypointIndex); + currentWaypointIndex = (currentWaypointIndex + 1) % routePoints.size(); + target = routePoints.get(currentWaypointIndex); } final WorldPoint hintTarget = target; Microbot.getClientThread().invoke(() -> client.setHintArrow(hintTarget)); - if (needsTrim) { - boatCache.getLocalBoat().trimSails(); - needsTrim = false; - } - if (config.autoNavigate()) { navigateToWaypoint(target); } @@ -274,68 +215,6 @@ private void resetState() { Microbot.getClientThread().invoke(() -> client.clearHintArrow()); } - private int getNextWaypointIndex(int currentIndex, int routeSize) { - return (currentIndex + 1) % routeSize; - } - - private double calculateTurnAngle(WorldPoint current, WorldPoint next, WorldPoint afterNext) { - if (current == null || next == null || afterNext == null) { - return 0.0; - } - - double dx1 = next.getX() - current.getX(); - double dy1 = next.getY() - current.getY(); - - double dx2 = afterNext.getX() - next.getX(); - double dy2 = afterNext.getY() - next.getY(); - - double len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); - double len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); - - if (len1 < 0.001 || len2 < 0.001) { - return 0.0; - } - - dx1 /= len1; - dy1 /= len1; - dx2 /= len2; - dy2 /= len2; - - double dot = dx1 * dx2 + dy1 * dy2; - dot = Math.max(-1.0, Math.min(1.0, dot)); - - double angleRadians = Math.acos(dot); - double angleDegrees = Math.toDegrees(angleRadians); - - return angleDegrees; - } - - private int calculateDynamicThreshold(double turnAngleDegrees) { - int baseThreshold = 1; - int additionalThreshold = (int) (turnAngleDegrees / 6.0); - int maxAdditional = 10; - return baseThreshold + Math.min(additionalThreshold, maxAdditional); - } - - private double calculateDirection(WorldPoint from, WorldPoint to) { - if (from == null || to == null) { - return 0.0; - } - double dx = to.getX() - from.getX(); - double dy = to.getY() - from.getY(); - double angle = Math.toDegrees(Math.atan2(dy, dx)); - if (angle < 0) { - angle += 360; - } - return angle; - } - - private String degreesToCompass(double degrees) { - String[] directions = {"E", "ENE", "NE", "NNE", "N", "NNW", "NW", "WNW", "W", "WSW", "SW", "SSW", "S", "SSE", "SE", "ESE"}; - int index = (int) Math.round(degrees / 22.5) % 16; - return directions[index]; - } - @Subscribe public void onClientTick(ClientTick clientTick) { var localPlayer = client.getLocalPlayer(); @@ -637,14 +516,18 @@ private void updateToadsThrown(TrialInfo newTrialInfo) { } public void markNextWaypointVisited(final WorldPoint player, final TrialRoute route, final int tolerance) { - if (player == null || route == null || route.Points == null || route.Points.isEmpty()) { + if (player == null || route == null) { + return; + } + var points = route.getInterpolatedPoints(); + if (points == null || points.isEmpty()) { return; } var nextIdx = lastVisitedIndex + 1; - if (nextIdx >= route.Points.size()) { - return; // finished route + if (nextIdx >= points.size()) { + return; } - var target = route.Points.get(nextIdx); + var target = points.get(nextIdx); if (target == null) { return; } @@ -655,20 +538,24 @@ public void markNextWaypointVisited(final WorldPoint player, final TrialRoute ro } public List getNextIndicesAfterLastVisited(final TrialRoute route, final int limit) { - if (route == null || route.Points == null || route.Points.isEmpty() || limit <= 0) { + 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 >= route.Points.size()) { + if (start >= points.size()) { return Collections.emptyList(); } var out = new ArrayList(limit); var nextPortal = route.PortalDirections.stream() - .filter(x -> x.Index >= lastVisitedIndex) - .min((a, b) -> Integer.compare(a.Index, b.Index)) + .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 < route.Points.size() && out.size() < limit; i++) { - if (nextPortal != null && i > nextPortal.Index) { + for (var i = start; i < points.size() && out.size() < limit; i++) { + if (nextPortal != null && i > route.getInterpolatedIndex(nextPortal.Index)) { break; } out.add(i); @@ -686,18 +573,23 @@ public List getVisibleLineForRoute(final WorldPoint player, final Tr return Collections.emptyList(); } + var points = route.getInterpolatedPoints(); List out = new ArrayList<>(); for (var idx : nextIdx) { - var real = route.Points.get(idx); - out.add(real); + 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 -> x.Index - 1 == lastVisitedIndex || x.Index == lastVisitedIndex || x.Index + 1 == lastVisitedIndex) - .min((a, b) -> Integer.compare(a.Index, b.Index)) + .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; 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 index 2e016adae1..f51b98eea3 100644 --- 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 @@ -4,7 +4,9 @@ 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 { @@ -14,17 +16,21 @@ public class TrialRoute { 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); // ensure mutable + Points = points == null ? new ArrayList<>() : new ArrayList<>(points); ToadOrder = Collections.emptyList(); WindMoteIndices = Collections.emptyList(); PortalDirections = Collections.emptyList(); } - // Unified extended constructor to avoid type erasure collision between different generic list overloads. public TrialRoute(TrialLocations location, TrialRanks rank, List points, List toadOrder, List windMoteIndices, List portalDirections) { this(location, rank, points); ToadOrder = toadOrder == null ? Collections.emptyList() : toadOrder; @@ -32,6 +38,82 @@ public TrialRoute(TrialLocations location, TrialRanks rank, List poi 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) 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 index f0b85899c6..f1ef0f5824 100644 --- 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 @@ -60,7 +60,11 @@ public Dimension render(Graphics2D graphics) { } var route = trialsScript.getActiveTrialRoute(); - if (route == null || route.Points == null || route.Points.isEmpty()) { + if (route == null) { + return null; + } + var routePoints = route.getInterpolatedPoints(); + if (routePoints == null || routePoints.isEmpty()) { return null; } @@ -151,7 +155,7 @@ private boolean isPortalIndex(int index, TrialRoute route) { return false; } for (var portal : route.PortalDirections) { - if (portal.Index == index) { + if (route.getInterpolatedIndex(portal.Index) == index) { return true; } } From 1d627fb4a0f95c89e66d3fad22d56e61cb86c9ab Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 6 Jan 2026 23:12:47 +0100 Subject: [PATCH 6/6] Bump AutoWoodcuttingPlugin version to 1.8.0; add new tree types and update item references --- .../woodcutting/AutoWoodcuttingPlugin.java | 2 +- .../woodcutting/enums/WoodcuttingTree.java | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) 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; }